commit c32381f8ed70e1d5d7f3bb189ee8a979a953190c Author: ZiJIe <17738440858@163.com> Date: Mon Sep 22 11:51:16 2025 +0800 Initial commit diff --git a/.claude/1.md b/.claude/1.md new file mode 100644 index 0000000..19111e9 --- /dev/null +++ b/.claude/1.md @@ -0,0 +1,48 @@ +--- +description: +globs: +alwaysApply: false +--- +--- +description: +globs: +alwaysApply: false +--- + +# Your rule content +#角色 +你是一名精通开发的高级工程师,拥有10年以上的应用开发经验,熟悉*等开发工具和技术栈。 +你的任务是帮助用户设计和开发易用且易于推护的 *** 应用。始终遵循最佳实践,并坚持干净代码和健壮架构的原则。 + +#目标 +你的目标是以用户容易理解的方式帮助他们完成“应用的设计和开发工作,确保应用功能完善、性能优异、用户体验良好。 + +#要求 +在理解用户需求、设计UI、编写代码、解决问题和项目选代优化时,你应该始终遵循以下原则: + + +##需求理解 +-充分理解用户需求,站在用户角度思考,分析需求是否存在缺漏,并与用户讨论完善需求; +-选择最简单的解决方案来满足用户需求,避免过度设计。 +##UI和样式设计 +-使用现代UI框架进行样式设计(例如***,这里可以根据不同开发项目仔纽展开,比如使用哪些视觉规范或者UI框架,没有的话也可以不用过多展开); +-在不同平台上实现一致的设计和响应式模式 +##代码编写 +技术选型:根据项目需求选择合适的技术栈(例如***,这里需要仔细展开,比如介招某个技术栈用在什么地方,以及要遵循什么最佳实践) +代码结构:强调代码的清晰性、模块化、可维护性,遵循最佳实践(如DRY原则、最小权限原则、响应式设计等) +-代码安全性:在编写代码时,始终考虑安全性,避免引入漏洞,确保用户输入的安全处理 +-性能优化:优化代码的性能,减少资源占用,提升加载速度,确保项目的高效运行 +-测试与文档:编写单元测试,确保代码的健壮性,并提供清晰的中文注释和文档。方便后续阅读和维护 +##问题解决 +-全面阅读相关代码,理解***应用的工作原理 +-根据用户的反馈分析问题的原因,提出解决问题的思路 +-确保每次代码变更不会破坏现有功能,且尽可能保持最小的改动 +##迭代优化 +与用户保持密切沟通,根据反读调整功能和设计,确保应用符合用户需求 +在不确定需求时,主动询问用户以澄清需求或技术细节 +##方法论 +-系统2思维:以分析严谨的方式解决问题。将需求分解为更小、可管理的部分,并在实施前仔细考虑每一步 +思维树:评估多种可能的解决方案及其后果。使用结构化的方法探索不同的路径。并选择最优的解决方案 +-选代改进:在最终确定代码之前,考虑改进、边缘情况和优化。通过潜在增强的迭代,确保最终解决方案是健壮的 + + diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..6e5be02 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,235 @@ + +--- + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +这是一个综合性的 **RuoYi-Vue 企业管理系统**,并集成了跨境电商 **ERP 功能**。项目主要包含: + +* **RuoYi-Vue 核心**:基于 Spring Boot 后端和 Vue.js 前端的企业管理系统 +* **ERP 客户端 (erp\_client\_sb)**:跨境电商 ERP 系统,JavaFX 桌面应用 +* **自定义扩展**:客户端监控、数据报表、API 集成功能 + +## 架构概览 + +### 后端架构 (Spring Boot) + +后端采用模块化 Spring Boot 架构,职责清晰: + +``` +ruoyi-admin/ # 主应用入口和 Web 控制器 +ruoyi-framework/ # 核心框架配置 (安全、Redis、MyBatis) +ruoyi-system/ # 系统领域实体、Mapper、业务逻辑 +ruoyi-common/ # 公共工具类、常量、通用功能 +ruoyi-quartz/ # 定时任务管理 +ruoyi-generator/ # 代码生成工具 +``` + +**主要技术栈:** + +* Spring Boot 2.5.15 (Java 17) +* Spring Security 5.7.12 + JWT 认证 +* MyBatis + PageHelper 分页插件 +* MySQL 数据库 + Redis 缓存与会话管理 +* Druid 1.2.23 连接池 +* Swagger 3.0.0 API 文档 +* Selenium 4.34.0 和 WebMagic 1.0.3 网页自动化 + +### 前端架构 (Vue.js) + +前端是一个基于 Vue 2.6.12 的应用,UI 使用 Element UI: + +``` +ruoyi-ui/src/ + api/ # API 服务模块 + components/ # 可复用的 Vue 组件 + layout/ # 应用布局组件 + router/ # Vue Router 配置 + store/ # Vuex 状态管理 + utils/ # 工具函数 + views/ # 页面组件,按功能组织 +``` + +**主要技术栈:** + +* Vue 2.6.12 + Vue Router 3.4.9 +* Element UI 2.15.14 +* Vuex 3.6.0 +* Axios 网络请求 + +### ERP 客户端架构 (JavaFX + Spring Boot) + +独立桌面应用,支持跨境电商业务: + +* Spring Boot 3.5.4 + JavaFX 17.0.1 UI +* SQLite 3.42.0 数据库 + JPA/Hibernate +* Playwright 1.54.0 和 WebMagic 1.0.3 数据采集 +* HutoolUtils 5.8.36 工具包 +* 七牛云存储集成 +* 多平台支持(亚马逊、日本乐天、Shopee 等) + +## 常用开发命令 + +### 后端 (Maven) + +```bash +# 构建打包 +mvn clean package + +# 启动 Spring Boot 应用 +mvn spring-boot:run + +# 从 ruoyi-admin 模块运行 +cd ruoyi-admin && mvn spring-boot:run + +# 跳过测试打包 +mvn clean package -DskipTests + +# 运行测试 +mvn test + +# Windows 运行脚本 +ry.bat +``` + +### 前端 (Vue.js) + +```bash +cd ruoyi-ui + +# 安装依赖 +npm install +# 国内镜像源安装 +npm install --registry=https://registry.npmmirror.com + +# 启动开发服务 (默认端口 80) +npm run dev + +# 生产环境构建 +npm run build:prod + +# 测试环境构建 +npm run build:stage +``` + +### ERP 客户端 (JavaFX) + +```bash +cd erp_client_sb + +# 启动应用 +mvn spring-boot:run + +# 打包可执行 JAR +mvn clean package + +# 使用 JavaFX 插件运行 +mvn javafx:run + +# 运行测试 +mvn test +``` + +## 核心配置说明 + +### 数据库 + +* **主库**:MySQL (Druid 连接池) +* **Redis**:缓存、会话、分布式锁 +* **MyBatis**:XML SQL 映射,内置分页 +* **ERP 客户端**:SQLite 本地存储 + +### 安全与认证 + +* **JWT**:无状态认证,可配置过期时间 +* **Spring Security**:基于角色的访问控制 +* **CORS**:跨域通信 +* **XSS 防护**:输入过滤 + +### API 设计 + +* **RESTful 风格**:标准 HTTP 动词和状态码 +* **客户端监控接口**:ERP 客户端状态上报 +* **匿名访问**:如 `/monitor/client/api/**` +* **统一分页**:PageHelper 插件 +* **统一响应格式**:AjaxResult 封装 +* **跨域配置**:CORS 支持 +* **Swagger 接口文档**:`/swagger-ui/index.html` + +## 开发流程 + +### 新功能开发 + +1. 后端:在 `ruoyi-system/domain/` 新建实体 +2. 数据库:在 `ruoyi-system/mapper/` 增加 Mapper +3. 服务层:实现业务逻辑 +4. 控制层:在 `ruoyi-admin/web/controller/` 增加接口 +5. 前端:开发 Vue 页面和 API 模块 +6. 集成:更新路由和菜单 + +### 客户端集成功能 + +* 实时状态跟踪 +* 错误上报与日志 +* 数据采集统计 +* API 请求监控 +* 版本分发追踪 + +### 代码生成器 + +* 访问 `/tool/gen` 使用 +* 自动生成:Java 实体、Mapper、Service、Controller +* 自动生成前端 Vue 页面和 API +* 自动生成数据库 SQL + +## 注意事项 + +### 文件上传与静态资源 + +* 上传路径在 `application.yml` 中配置 (`ruoyi.profile`) +* 支持本地存储和七牛云存储 +* 静态资源路径 `/profile/**` +* 前端开发服务器端口:80 (可在 `vue.config.js` 中修改) + +### 系统监控 + +* 系统监控接口 `/monitor/server` +* Redis 缓存监控 +* Druid 连接池监控 +* Quartz 定时任务 +* 操作日志审计 + +### 多平台支持 + +ERP 客户端支持: + +* 亚马逊 (Amazon) +* 日本乐天 (Rakuten) +* Shopee +* 阿里巴巴 1688 +* 自定义采集与自动化工具 + +### 重要配置文件 + +* **后端主配置**:`ruoyi-admin/src/main/resources/application.yml` +* **数据库配置**:`application-druid.yml` +* **前端配置**:`ruoyi-ui/vue.config.js` +* **Maven 配置**:根目录 `pom.xml`(父项目)和各模块 `pom.xml` +* **ERP 客户端配置**:`erp_client_sb/src/main/resources/application.yml` + +### 常见问题排查 + +* **端口冲突**:前端默认端口 80,可在 `vue.config.js` 修改 +* **跨域问题**:检查 `CorsConfig.java` 和 `vue.config.js` 代理配置 +* **数据库连接**:检查 `application-druid.yml` 中的数据库连接配置 +* **Maven 依赖**:使用阿里云镜像加速依赖下载 +--- + +⚠️ **额外要求**:回答时必须使用中文。 +⚠️ **代码规范要求**:代码必须遵循CLAUDE.md中的规范,代码简洁度和性能优先。 +💡 **操作提示**:在每次修改代码前,先向我说明修改的思路和方案,我确认同意后再进行代码更改。 + +--- diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..39d9804 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,213 @@ +{ + "permissions": { + "allow": [ + "Bash(mvn clean:*)", + "Bash(where mysql)", + "Bash(dir:*)", + "Bash(mvn:*)", + "Bash(npm run dev:*)", + "Bash(xmllint:*)", + "Bash(sed:*)", + "Bash(rmdir:*)", + "Bash(rm:*)", + "mcp__talktofigma__get_selection", + "mcp__talktofigma__join_channel", + "mcp__talktofigma__get_document_info", + "mcp__talktofigma__read_my_design", + "mcp__talktofigma__get_node_info", + "mcp__talktofigma__scan_text_nodes", + "mcp__talktofigma__scan_nodes_by_types", + "mcp__talktofigma__get_nodes_info", + "Bash(mvn compile:*)", + "Bash(mkdir:*)", + "Bash(copy:*)", + "Bash(cp:*)", + "Bash(mvn:*)", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\module-info.java\")", + "Bash(jar tf:*)", + "Bash(./build-simple.bat)", + "Bash(java:*)", + "Bash(./build-custom-installer.bat)", + "Bash(echo $JAVA_HOME)", + "Bash(where java)", + "Bash(./build-installer-fixed.bat)", + "Bash(fix-npm-install.bat)", + "Bash(chmod:*)", + "Bash(where mvn)", + "Bash(./install-nsis.bat)", + "Bash(./test-final.bat)", + "Bash(./build-final-installer.bat)", + "Bash(npm install)", + "Bash(./快速启动-ERP客户端.bat)", + "Bash(npm install:*)", + "Bash(curl:*)", + "Bash(rg:*)", + "Bash(timeout:*)", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js\\table-scroll-fix.js\")", + "mcp__context7-mcp__resolve-library-id", + "WebSearch", + "Bash(findstr:*)", + "Bash(move:*)", + "Bash(jar:*)", + "Bash(cat:*)", + "Bash(taskkill:*)", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\test\\SeleniumBrowserTest.java\")", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\RakutenCacheService.java\")", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\CacheService.java\")", + "Bash(git checkout:*)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views\\monitor\\key/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-common\\src\\main\\java\\com\\ruoyi\\common\\core\\redis/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\util/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)", + "Read(//c/Users/ZiJIe/Desktop/MongooCrawler-feature-monitor/**)", + "Bash(wmic process where:*)", + "Bash(del:*)", + "Bash(ren:*)", + "Bash(find:*)", + "Bash(netstat:*)", + "Bash(mysql:*)", + "Bash(rd:*)", + "Bash(git clean:*)", + "Bash(tasklist:*)", + "Bash(./fix-electron.bat)", + "Bash(npm run build:*)", + "Bash(node:*)", + "Bash(tsc)", + "Bash(npx tsc:*)", + "Bash(./create-dev-version.bat)", + "Bash(./prepare-backend.bat)", + "Bash(./start-erp.bat)", + "Bash(npm config set:*)", + "Bash(set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/)", + "Bash(npm cache clean:*)", + "Bash(npx vite:*)", + "Bash(./dev-ruoyi-erp.bat)", + "Bash(./start-desktop-app.bat)", + "Bash(cnpm install:*)", + "Bash(cnpm uninstall:*)" + ], + "deny": [], + "ask": [], + "additionalDirectories": [ + "C:\\c\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue", + "C:\\Users\\ZiJIe\\Desktop\\wox", + "C:\\c\\Users\\ZiJIe" + ] + }, + "dangerouslySkipPermissions": true +} \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..e6b8496 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..b950b6e --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fc70dbe --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e069031 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..854dc68 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,111 @@ + + + + + + + + + + { + "customColor": "", + "associatedIndex": 0 +} + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true", + "RunOnceActivity.git.unshallow": "true", + "git-widget-placeholder": "master", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + 1758509443816 + + + + + + + + + + + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74f7728 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,211 @@ + +--- + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +这是一个基于 **RuoYi-Vue 3.9.0** 的企业管理系统,集成了跨境电商 ERP 功能。项目包含: + +* **RuoYi-Vue 核心**:基于 Spring Boot 2.5.15 后端和 Vue.js 2.6.12 前端的企业管理平台 +* **ERP 客户端 (erp_client_sb)**:独立的跨境电商 ERP 桌面应用 (JavaFX + Spring Boot 3.5.4) +* **客户端监控扩展**:实时监控、数据报表、API 集成等自定义功能 + +## 项目架构 + +### 主项目模块结构 (Maven 多模块) + +``` +ruoyi-admin/ # 主应用入口,Web 控制器层 +ruoyi-framework/ # 核心框架配置 (Spring Security, Redis, MyBatis) +ruoyi-system/ # 系统核心模块 (实体类、Mapper、Service) +ruoyi-common/ # 公共工具类和常量定义 +ruoyi-quartz/ # 定时任务管理模块 +ruoyi-generator/ # 代码生成器模块 +ruoyi-ui/ # Vue.js 前端应用 +erp_client_sb/ # 独立的跨境电商 ERP 客户端 +``` + +### 技术栈详情 + +**后端 (Spring Boot 2.5.15)** +- Java 17 + Maven 3.11.0 +- Spring Security 5.7.12 + JWT 0.9.1 认证 +- MyBatis + PageHelper 1.4.7 分页 +- MySQL + Redis 缓存 +- Druid 1.2.23 连接池 +- Swagger 3.0.0 API 文档 +- FastJSON 2.0.57 JSON 处理 + +**前端 (Vue.js 2.6.12)** +- Element UI 2.15.14 组件库 +- Vue Router 3.4.9 + Vuex 3.6.0 +- Axios 0.28.1 HTTP 客户端 +- ECharts 5.4.0 图表库 +- Webpack 构建 (vue-cli-service 4.4.6) + +**ERP 客户端 (JavaFX + Spring Boot 3.5.4)** +- OpenJFX 17.0.1 桌面 UI +- SQLite 3.42.0 本地数据库 + JPA/Hibernate +- WebMagic 1.0.3 + Selenium 4.23.0 网页爬取 +- HutoolUtils 5.8.36 工具库 +- 七牛云 7.12.1 存储服务 + +## 常用开发命令 + +### 后端开发 (Maven) + +```bash +# 启动主应用 (推荐) +cd ruoyi-admin && mvn spring-boot:run + +# 从根目录启动 +mvn spring-boot:run -pl ruoyi-admin + +# 打包部署 +mvn clean package -DskipTests + +# 运行测试 +mvn test + +# 编译项目 +mvn clean compile +``` + +### 前端开发 (npm) + +```bash +cd ruoyi-ui + +# 安装依赖 (建议使用国内镜像) +npm install --registry=https://registry.npmmirror.com + +# 启动开发服务器 (端口 80) +npm run dev + +# 生产环境构建 +npm run build:prod + +# 测试环境构建 +npm run build:stage +``` + +### ERP 客户端开发 + +```bash +cd erp_client_sb + +# 启动 JavaFX 应用 +mvn spring-boot:run +# 或者 +mvn javafx:run + +# 打包可执行 JAR +mvn clean package +``` + +## 核心配置与架构要点 + +### Maven 依赖管理 + +- 使用阿里云 Maven 镜像 (maven.aliyun.com) +- 父 POM 统一管理版本号和依赖 +- 安全版本覆盖:Tomcat 9.0.106, Logback 1.2.13, Spring Framework 5.3.39 + +### 前端开发配置 + +- **开发服务器**:端口 80,支持热重载 +- **代理配置**:`vue.config.js` 中配置后端 API 代理 (`http://8.138.23.49:8080`) +- **构建优化**:Gzip 压缩、代码分割、Element UI 单独打包 + +### 数据库与缓存 + +- **MySQL**:主数据库,通过 Druid 连接池管理 +- **Redis**:会话存储、缓存、分布式锁 +- **SQLite**:ERP 客户端本地数据存储 + +### 安全与认证 + +- **JWT 无状态认证**:支持多终端 +- **RBAC 权限模型**:角色-菜单-按钮权限 +- **跨域配置**:支持前后端分离部署 +- **XSS 防护**:输入过滤和输出编码 + +## 开发工作流 + +### 新功能开发标准流程 + +1. **后端开发**: + - 在 `ruoyi-system/src/main/java/com/ruoyi/system/domain/` 创建实体类 + - 在 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/` 创建 Mapper 接口 + - 在 `ruoyi-system/src/main/resources/mapper/system/` 创建 MyBatis XML + - 在 `ruoyi-system/src/main/java/com/ruoyi/system/service/` 实现业务逻辑 + - 在 `ruoyi-admin/src/main/java/com/ruoyi/web/controller/` 创建 REST 控制器 + +2. **前端开发**: + - 在 `ruoyi-ui/src/api/` 创建 API 服务模块 + - 在 `ruoyi-ui/src/views/` 创建页面组件 + - 更新路由配置和菜单权限 + +3. **代码生成器**: + - 访问 `/tool/gen` 快速生成 CRUD 代码 + - 支持 Java、Vue、SQL 代码自动生成 + +### 客户端集成开发 + +ERP 客户端提供的核心功能: +- 多平台数据采集 (Amazon, Rakuten, Shopee, 1688) +- 实时状态监控与错误上报 +- 与主系统的 API 集成 (`/monitor/client/api/**`) + +## 重要配置文件 + +### 后端配置 +- **主配置**:`ruoyi-admin/src/main/resources/application.yml` +- **数据源配置**:`ruoyi-admin/src/main/resources/application-druid.yml` +- **Maven 配置**:根目录 `pom.xml` (父 POM) + +### 前端配置 +- **构建配置**:`ruoyi-ui/vue.config.js` +- **包管理**:`ruoyi-ui/package.json` + +### ERP 客户端配置 +- **应用配置**:`erp_client_sb/src/main/resources/application.yml` +- **Maven 配置**:`erp_client_sb/pom.xml` + +## 系统监控与工具 + +### 内置监控功能 +- 系统性能监控:`/monitor/server` +- Redis 缓存监控:缓存信息查询和命令统计 +- Druid 连接池监控:SQL 性能分析 +- 在线用户监控:当前活跃用户状态 +- 操作日志:系统操作记录和异常日志 + +### API 文档 +- Swagger UI:`/swagger-ui/index.html` +- 自动生成的接口文档 + +## 部署和环境 + +### 开发环境要求 +- **Java**:JDK 17 +- **Node.js**:>= 8.9 +- **npm**:>= 3.0.0 +- **MySQL**:5.7+ 或 8.0+ +- **Redis**:6.0+ + +### 常见问题排查 +- **端口冲突**:前端默认 80 端口,可在 `vue.config.js` 修改 +- **跨域问题**:检查 `vue.config.js` 代理配置和后端 CORS 设置 +- **Maven 依赖**:已配置阿里云镜像加速 +- **JavaFX 运行**:确保 JDK 包含 JavaFX 模块或单独安装 OpenJFX +--- + +⚠️ **额外要求**:回答时必须使用中文。 +💡 **操作提示**:在每次修改代码前,我会先向您说明修改的思路和方案,请您确认同意后再进行代码更改。 + + +--- diff --git a/README.md b/README.md new file mode 100644 index 0000000..998475c --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +

+ logo +

+

RuoYi v3.9.0

+

基于SpringBoot+Vue前后端分离的Java快速开发框架

+

+ + + +

+ +## 平台简介 + +若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 + +* 前端采用Vue、Element UI。 +* 后端采用Spring Boot、Spring Security、Redis & Jwt。 +* 权限认证使用Jwt,支持多终端认证系统。 +* 支持加载动态权限菜单,多方式轻松权限控制。 +* 高效率开发,使用代码生成器可以一键生成前后端代码。 +* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。 +* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast),Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。 +* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) +* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)   + +## 内置功能 + +1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 +2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 +3. 岗位管理:配置系统用户所属担任职务。 +4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 +5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 +6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 +7. 参数管理:对系统动态配置常用参数。 +8. 通知公告:系统通知公告信息发布维护。 +9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 +10. 登录日志:系统登录日志记录查询包含登录异常。 +11. 在线用户:当前系统中活跃用户状态监控。 +12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 +13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 +14. 系统接口:根据业务代码自动生成相关的api接口文档。 +15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 +16. 缓存监控:对系统的缓存信息查询,命令统计等。 +17. 在线构建器:拖动表单元素生成相应的HTML代码。 +18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。 + +## 在线体验 + +- admin/admin123 +- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。 + +演示地址:http://vue.ruoyi.vip +文档地址:http://doc.ruoyi.vip + +## 演示图 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +## 若依前后端分离交流群 + +QQ群: [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) 点击按钮入群。 \ No newline at end of file diff --git a/data/device.id b/data/device.id new file mode 100644 index 0000000..69e8f3d --- /dev/null +++ b/data/device.id @@ -0,0 +1 @@ +DEVICE-C705AA3904F84D998D03B5CD83EEBBD7 \ No newline at end of file diff --git a/data/erp-cache.db b/data/erp-cache.db new file mode 100644 index 0000000..3cb31f8 Binary files /dev/null and b/data/erp-cache.db differ diff --git a/data/erp-cache.db-shm b/data/erp-cache.db-shm new file mode 100644 index 0000000..ba00241 Binary files /dev/null and b/data/erp-cache.db-shm differ diff --git a/data/erp-cache.db-wal b/data/erp-cache.db-wal new file mode 100644 index 0000000..75aaf18 Binary files /dev/null and b/data/erp-cache.db-wal differ diff --git a/data/jwt_rsa_private.pem b/data/jwt_rsa_private.pem new file mode 100644 index 0000000..c021c8a --- /dev/null +++ b/data/jwt_rsa_private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCMgaMZlZOiAK7e +FWjhWuyIg+8XBIrF9/kZYGtX+3tn53T+sCPBjAIukouyP41H5g6IZDeTP1WXsNuQ +kVIvbZwRJKTSO7EiqE1T3V9jPzTXOXvq0SPnjaMhmUKxkmgRSs9DBZUqrElZ5mzw +1ua/9ISlwCLqFRrVL5Nzc9+TO8IIRjjPBPYssYCDpz4NpdQb4OQYhaTRrj1++wo4 +N+LuS+wKb0Ps8SH1+udYaUWR6NteR6hhj0+0AVK0cyqsbHYQE4z+gAJr+Aicd1ZX +pON8jsFiSNdTgGIVKCb8haVkgkXwe1inBwhyVcWk8xaz2aKa+TsNI03WfBi8DY39 ++WeisqnrAgMBAAECggEABJA3TF7rxxCvnT3jxKHv2bUzQDOhEDHwEK9tfROJXAQL +7DOrTZ9u+LVAvT7MJ2Ak67AZj/o4HO+dCfJ2UV0FexcOFVfj9mSx8j3X2cDVRgIz +cJpvSJd0i2RPYrYHFDyyQ5J8WED1NurBcgcAwo49+qYlXCXoU7EyYEcMpVsE/8C/ +xvW0GShTzXOpO5emQOkPixxNW8i6VHte23igUULn29eyePB+pvRdEuuKC+bEZ9cJ +ZXJ0p/yS2sNUxvBAmdVZTTs88E3gu/ARRJgyHE//Ax+fHmlSpPGeJlEPivntu0So +vrJan9uIA3mS7eE1i7M+JoZjIgwpHKfG07AipRmklQKBgQDEQAGgpMo3AtRMmYFi +PP3Sb5p3cETekWXJBijlhE69XBZg/VBSpNrtmD4iXuYbBb3d140nIvESowpBN3gn +uY6nNqJWSzhKzQhU8ienJS8PZPeeRE2mMI8sRxl+VnhMraa/E8k/hzqP0RCo+cfC +uu1be7AJ0+W6WB0d9g6zSj99HQKBgQC3SOV/4i1KXybuaphJJrZF5cuMoD5cE5Ck +vXTTc9MSkUtwLf5PiJGRzRWNd5amS0FMb3IuC8YfEqZ8k+vDDVbgigqDeIKEpJSQ +3K8A/F/094cyMyqrQ2aiUPg6Da57QH+Q0Ab2S7n8RstZC6h28sYxgG1acuL2jqY7 +S7HlVAZ8pwKBgQC3UqMykT1kjfwLYgn+3sKsZRyCHhn3XxMZ6esiG6oCMZemGnuB ++AWalPDV4phI/eAS71woBvfzVOIrccmIMkoT4XFb8wAuv8DcuShZdt6zHrpA2cU/ +TXUxA2nJHrVZy41MSQthkM0fs0hA0LPOMBexsaUMSSj8HXt1lXi9+sm78QKBgAsE +0udRTa++8LQ8rFMZhLPHEOmvaJBYjMWarj9YI0Rmf8aKvVNCvp2pWrZajjAJLi/O +M2sZQhv0HxY2PmJHlwWAxwkIYbBfxJ7A5bSFd69egj4+XT5WmwD/JS04TVkTk5e9 +Ke38t323M9pynPoptkibk/dwGL0B7nR6JIPI/WrZAoGBALYitr4UK/H8I/XvYaUJ +q0A93fIa9ACVHshZEAwY7o1PIZu9bLScGilkbkvdATwgkfrBZXFCDIAFu2Ta/yMF +NEFXG1szFg0aDzEN0pm8o2dndQ9dKTPswytvR5DphgCmxhuJDm9Qwwjc1UQuWePp +Q3mDFK0eVcWWdA03UbY2TEeK +-----END PRIVATE KEY----- diff --git a/data/jwt_rsa_public.pem b/data/jwt_rsa_public.pem new file mode 100644 index 0000000..0523743 --- /dev/null +++ b/data/jwt_rsa_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjIGjGZWTogCu3hVo4Vrs +iIPvFwSKxff5GWBrV/t7Z+d0/rAjwYwCLpKLsj+NR+YOiGQ3kz9Vl7DbkJFSL22c +ESSk0juxIqhNU91fYz801zl76tEj542jIZlCsZJoEUrPQwWVKqxJWeZs8Nbmv/SE +pcAi6hUa1S+Tc3PfkzvCCEY4zwT2LLGAg6c+DaXUG+DkGIWk0a49fvsKODfi7kvs +Cm9D7PEh9frnWGlFkejbXkeoYY9PtAFStHMqrGx2EBOM/oACa/gInHdWV6TjfI7B +YkjXU4BiFSgm/IWlZIJF8HtYpwcIclXFpPMWs9mimvk7DSNN1nwYvA2N/flnorKp +6wIDAQAB +-----END PUBLIC KEY----- diff --git a/electron-vue-template/LICENSE b/electron-vue-template/LICENSE new file mode 100644 index 0000000..d72f1d2 --- /dev/null +++ b/electron-vue-template/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Deluze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/electron-vue-template/README.md b/electron-vue-template/README.md new file mode 100644 index 0000000..a71b4cf --- /dev/null +++ b/electron-vue-template/README.md @@ -0,0 +1,77 @@ +
+ +# Electron Vue Template + +image + +A simple starter template for a **Vue3** + **Electron** TypeScript based application, including **ViteJS** and **Electron Builder**. +
+ +## About + +This template utilizes [ViteJS](https://vitejs.dev) for building and serving your (Vue powered) front-end process, it provides Hot Reloads (HMR) to make development fast and easy ⚡ + +Building the Electron (main) process is done with [Electron Builder](https://www.electron.build/), which makes your application easily distributable and supports cross-platform compilation 😎 + +## Getting started + +Click the green **Use this template** button on top of the repository, and clone your own newly created repository. + +**Or..** + +Clone this repository: `git clone git@github.com:Deluze/electron-vue-template.git` + + +### Install dependencies ⏬ + +```bash +npm install +``` + +### Start developing ⚒️ + +```bash +npm run dev +``` + +## Additional Commands + +```bash +npm run dev # starts application with hot reload +npm run build # builds application, distributable files can be found in "dist" folder + +# OR + +npm run build:win # uses windows as build target +npm run build:mac # uses mac as build target +npm run build:linux # uses linux as build target +``` + +Optional configuration options can be found in the [Electron Builder CLI docs](https://www.electron.build/cli.html). +## Project Structure + +```bash +- scripts/ # all the scripts used to build or serve your application, change as you like. +- src/ + - main/ # Main thread (Electron application source) + - renderer/ # Renderer thread (VueJS application source) +``` + +## Using static files + +If you have any files that you want to copy over to the app directory after installation, you will need to add those files in your `src/main/static` directory. + +Files in said directory are only accessible to the `main` process, similar to `src/renderer/assets` only being accessible to the `renderer` process. Besides that, the concept is the same as to what you're used to in your other front-end projects. + +#### Referencing static files from your main process + +```ts +/* Assumes src/main/static/myFile.txt exists */ + +import {app} from 'electron'; +import {join} from 'path'; +import {readFileSync} from 'fs'; + +const path = join(app.getAppPath(), 'static', 'myFile.txt'); +const buffer = readFileSync(path); +``` diff --git a/electron-vue-template/electron-builder.json b/electron-vue-template/electron-builder.json new file mode 100644 index 0000000..5313c62 --- /dev/null +++ b/electron-vue-template/electron-builder.json @@ -0,0 +1,39 @@ +{ + "appId": "com.electron.app", + "directories": { + "output": "dist" + }, + + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "shortcutName": "Electron App" + }, + "win": { + "target": "nsis" + }, + "linux": { + "target": ["snap"] + }, + "files": [ + { + "from": "build/main", + "to": "main", + "filter": ["**/*"] + }, + { + "from": "build/renderer", + "to": "renderer", + "filter": ["**/*"] + }, + { + "from": "src/main/static", + "to": "static", + "filter": ["**/*"] + }, + "!build", + "!dist", + "!scripts" + ] +} diff --git a/electron-vue-template/package-lock.json b/electron-vue-template/package-lock.json new file mode 100644 index 0000000..959e329 --- /dev/null +++ b/electron-vue-template/package-lock.json @@ -0,0 +1,5847 @@ +{ + "name": "electron-vue-template", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "electron-vue-template", + "version": "0.1.0", + "dependencies": { + "vue": "^3.3.8" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "electron": "^32.1.2", + "electron-builder": "^25.1.6", + "typescript": "^5.2.2", + "vite": "^4.5.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.13.tgz", + "integrity": "sha512-pY5z2qQSwbFzJsBdgfJIzXf5ElHTVMutC2dxh0FD60njknMu3n1NnTABOcQwbb5/v5soqE79m9UjaJryBf3epg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.0.tgz", + "integrity": "sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz", + "integrity": "sha512-HCQG8VDFDM7YDAdcj5QI5DvUi+r6xvo9LgvYdk7LSkUNwdpempdB5horkMSZsbdey9Ywsf5aaU8kEPw9M5kREA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", + "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz", + "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==", + "dependencies": { + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz", + "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-ssr": "3.3.8", + "@vue/reactivity-transform": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz", + "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==", + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz", + "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==", + "dependencies": { + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz", + "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==", + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz", + "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==", + "dependencies": { + "@vue/reactivity": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz", + "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==", + "dependencies": { + "@vue/runtime-core": "3.3.8", + "@vue/shared": "3.3.8", + "csstype": "^3.1.2" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz", + "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==", + "dependencies": { + "@vue/compiler-ssr": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "vue": "3.3.8" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz", + "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.6.tgz", + "integrity": "sha512-iui5q65skaawkTBcsaf2wCaegCWO+JoK5VaPnaNdIWXm2bq8a/g53W88FHjR535L9viLeGbuBp/iU1XZSe2DQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.0", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "25.1.6", + "builder-util-runtime": "9.2.9", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.6", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.6", + "electron-builder-squirrel-windows": "25.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true, + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.6.tgz", + "integrity": "sha512-BOMVCO/CLYaIwK2uh7BKJUmRy9fYhn674c4Cauxc/lSZ7CyLLNkUMZJUFCPd3OqD1FIQO06MZ/u7akKtVyXlJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.9", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.9", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz", + "integrity": "sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "optional": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.6.tgz", + "integrity": "sha512-TeKjLNKBu6ODewl36Q1FH0+Bv/Rnb76x1vzPJ0Xw1T/YlAByYl1G+1PaKoEpVEDisrLRSrM9a0k3vDj53xQi9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.6", + "builder-util": "25.1.6", + "builder-util-runtime": "9.2.9", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "32.1.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.2.tgz", + "integrity": "sha512-CXe6doFzhmh1U7daOvUzmF6Cj8hssdYWMeEPRnRO6rB9/bbwMlWctcQ7P8NJXhLQ88/vYUJQrJvlJPh8qM0BRQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.6.tgz", + "integrity": "sha512-a+w0leNGcr57u9fiQBsVvVuXtTsg/6VpMOps1Q1TPAceHtK4kWtX2wc7X2cnJQrIME8bWbdm/YR9Swr/xF1/ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.6", + "builder-util": "25.1.6", + "builder-util-runtime": "9.2.9", + "chalk": "^4.1.2", + "dmg-builder": "25.1.6", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.6.tgz", + "integrity": "sha512-g50ZPzcR8mw29O/FZoZIEsU3M+wHsxHISegxmdjftbUtLLT1UoHgPQkNIssqfpboy8ORL7PJopoumkZoMkXzFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.6", + "archiver": "^5.3.1", + "builder-util": "25.1.6", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "25.1.6", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.6.tgz", + "integrity": "sha512-Hiy/tVyu57cbkyxS0GrzEnxm7+B/9IeFcBTia2QQBCTg10zvHURdAj7Sk7XHKys8kKLr1tVNThuG165uTnNJdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "25.1.6", + "builder-util-runtime": "9.2.9", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true, + "optional": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "optional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "optional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "optional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "optional": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", + "integrity": "sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.1.tgz", + "integrity": "sha512-/FJ6/gKAXbcHtivannhecWsa43kGVFK3aHHv9Jm3x0eFiM31MoGihkAOWbm3UsvjYLRVw0zTkfARy2dI96JL1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz", + "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==", + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-sfc": "3.3.8", + "@vue/runtime-dom": "3.3.8", + "@vue/server-renderer": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/electron-vue-template/package.json b/electron-vue-template/package.json new file mode 100644 index 0000000..959161c --- /dev/null +++ b/electron-vue-template/package.json @@ -0,0 +1,33 @@ +{ + "name": "electron-vue-template", + "version": "0.1.0", + "description": "A minimal Electron + Vue application", + "main": "main/main.js", + "scripts": { + "dev": "node scripts/dev-server.js", + "build": "node scripts/build.js && electron-builder", + "build:win": "node scripts/build.js && electron-builder --win", + "build:mac": "node scripts/build.js && electron-builder --mac", + "build:linux": "node scripts/build.js && electron-builder --linux" + }, + "repository": "https://github.com/deluze/electron-vue-template", + "author": { + "name": "Deluze", + "url": "https://github.com/Deluze" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "electron": "^32.1.2", + "electron-builder": "^25.1.6", + "typescript": "^5.2.2", + "vite": "^4.5.0" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.11.3", + "exceljs": "^4.4.0", + "vue": "^3.3.8" + } +} diff --git a/electron-vue-template/public/icon/icon.png b/electron-vue-template/public/icon/icon.png new file mode 100644 index 0000000..d826be6 Binary files /dev/null and b/electron-vue-template/public/icon/icon.png differ diff --git a/electron-vue-template/public/icon/img.png b/electron-vue-template/public/icon/img.png new file mode 100644 index 0000000..54b070b Binary files /dev/null and b/electron-vue-template/public/icon/img.png differ diff --git a/electron-vue-template/public/icons/icon.ico b/electron-vue-template/public/icons/icon.ico new file mode 100644 index 0000000..6fcfdc3 Binary files /dev/null and b/electron-vue-template/public/icons/icon.ico differ diff --git a/electron-vue-template/public/icons/icon.png b/electron-vue-template/public/icons/icon.png new file mode 100644 index 0000000..d826be6 Binary files /dev/null and b/electron-vue-template/public/icons/icon.png differ diff --git a/electron-vue-template/public/image/111.png b/electron-vue-template/public/image/111.png new file mode 100644 index 0000000..aabd8cd Binary files /dev/null and b/electron-vue-template/public/image/111.png differ diff --git a/electron-vue-template/public/image/splash_screen.png b/electron-vue-template/public/image/splash_screen.png new file mode 100644 index 0000000..165407b Binary files /dev/null and b/electron-vue-template/public/image/splash_screen.png differ diff --git a/electron-vue-template/public/splash.html b/electron-vue-template/public/splash.html new file mode 100644 index 0000000..c4abdbc --- /dev/null +++ b/electron-vue-template/public/splash.html @@ -0,0 +1,33 @@ + + + + + + 正在启动... + + + + + + + +
+
+
+ + + + diff --git a/electron-vue-template/scripts/build.js b/electron-vue-template/scripts/build.js new file mode 100644 index 0000000..4abd4bc --- /dev/null +++ b/electron-vue-template/scripts/build.js @@ -0,0 +1,32 @@ +const Path = require('path'); +const Chalk = require('chalk'); +const FileSystem = require('fs'); +const Vite = require('vite'); +const compileTs = require('./private/tsc'); + +function buildRenderer() { + return Vite.build({ + configFile: Path.join(__dirname, '..', 'vite.config.js'), + base: './', + mode: 'production' + }); +} + +function buildMain() { + const mainPath = Path.join(__dirname, '..', 'src', 'main'); + return compileTs(mainPath); +} + +FileSystem.rmSync(Path.join(__dirname, '..', 'build'), { + recursive: true, + force: true, +}) + +console.log(Chalk.blueBright('Transpiling renderer & main...')); + +Promise.allSettled([ + buildRenderer(), + buildMain(), +]).then(() => { + console.log(Chalk.greenBright('Renderer & main successfully transpiled! (ready to be built with electron-builder)')); +}); diff --git a/electron-vue-template/scripts/dev-server.js b/electron-vue-template/scripts/dev-server.js new file mode 100644 index 0000000..276b254 --- /dev/null +++ b/electron-vue-template/scripts/dev-server.js @@ -0,0 +1,121 @@ +process.env.NODE_ENV = 'development'; + +const Vite = require('vite'); +const ChildProcess = require('child_process'); +const Path = require('path'); +const Chalk = require('chalk'); +const Chokidar = require('chokidar'); +const Electron = require('electron'); +const compileTs = require('./private/tsc'); +const FileSystem = require('fs'); +const { EOL } = require('os'); + +let viteServer = null; +let electronProcess = null; +let electronProcessLocker = false; +let rendererPort = 0; + +async function startRenderer() { + viteServer = await Vite.createServer({ + configFile: Path.join(__dirname, '..', 'vite.config.js'), + mode: 'development', + }); + + return viteServer.listen(); +} + +async function startElectron() { + if (electronProcess) { // single instance lock + return; + } + + try { + await compileTs(Path.join(__dirname, '..', 'src', 'main')); + } catch { + console.log(Chalk.redBright('Could not start Electron because of the above typescript error(s).')); + electronProcessLocker = false; + return; + } + + const args = [ + Path.join(__dirname, '..', 'build', 'main', 'main.js'), + rendererPort, + ]; + electronProcess = ChildProcess.spawn(Electron, args); + electronProcessLocker = false; + + electronProcess.stdout.on('data', data => { + if (data == EOL) { + return; + } + + process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString())) + }); + + electronProcess.stderr.on('data', data => + process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString())) + ); + + electronProcess.on('exit', () => stop()); +} + +function restartElectron() { + if (electronProcess) { + electronProcess.removeAllListeners('exit'); + electronProcess.kill(); + electronProcess = null; + } + + if (!electronProcessLocker) { + electronProcessLocker = true; + startElectron(); + } +} + +function copyStaticFiles() { + copy('static'); +} + +/* +The working dir of Electron is build/main instead of src/main because of TS. +tsc does not copy static files, so copy them over manually for dev server. +*/ +function copy(path) { + FileSystem.cpSync( + Path.join(__dirname, '..', 'src', 'main', path), + Path.join(__dirname, '..', 'build', 'main', path), + { recursive: true } + ); +} + +function stop() { + viteServer.close(); + process.exit(); +} + +async function start() { + console.log(`${Chalk.greenBright('=======================================')}`); + console.log(`${Chalk.greenBright('Starting Electron + Vite Dev Server...')}`); + console.log(`${Chalk.greenBright('=======================================')}`); + + const devServer = await startRenderer(); + rendererPort = devServer.config.server.port; + + copyStaticFiles(); + startElectron(); + + const path = Path.join(__dirname, '..', 'src', 'main'); + Chokidar.watch(path, { + cwd: path, + }).on('change', (path) => { + console.log(Chalk.blueBright(`[electron] `) + `Change in ${path}. reloading... 🚀`); + + if (path.startsWith(Path.join('static', '/'))) { + copy(path); + } + + restartElectron(); + }); +} + +start(); diff --git a/electron-vue-template/scripts/private/tsc.js b/electron-vue-template/scripts/private/tsc.js new file mode 100644 index 0000000..baf0aa1 --- /dev/null +++ b/electron-vue-template/scripts/private/tsc.js @@ -0,0 +1,24 @@ +const ChildProcess = require('child_process'); +const Chalk = require('chalk'); + +function compile(directory) { + return new Promise((resolve, reject) => { + const tscProcess = ChildProcess.exec('tsc', { + cwd: directory, + }); + + tscProcess.stdout.on('data', data => + process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString())) + ); + + tscProcess.on('exit', exitCode => { + if (exitCode > 0) { + reject(exitCode); + } else { + resolve(); + } + }); + }); +} + +module.exports = compile; diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts new file mode 100644 index 0000000..7b75f33 --- /dev/null +++ b/electron-vue-template/src/main/main.ts @@ -0,0 +1,199 @@ +// 主进程:创建窗口、启动后端 JAR、隐藏菜单栏 +import {app, BrowserWindow, ipcMain, session, Menu, screen} from 'electron'; +import { Socket } from 'net'; +import { existsSync } from 'fs'; +import {join, dirname} from 'path'; +import {spawn, ChildProcessWithoutNullStreams} from 'child_process'; + +// 保存后端进程与窗口引用,便于退出时清理 +let springProcess: ChildProcessWithoutNullStreams | null = null; +let mainWindow: BrowserWindow | null = null; +let splashWindow: BrowserWindow | null = null; +let appOpened = false; + +function openAppIfNotOpened() { + if (appOpened) return; + appOpened = true; + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + if (splashWindow) { splashWindow.close(); splashWindow = null; } +} + +// 启动后端 Spring Boot(使用你提供的绝对路径) +function startSpringBoot() { + const jarPath = 'C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/ruoyi-admin/target/ruoyi-admin.jar'; + + springProcess = spawn('java', ['-jar', jarPath], { + cwd: dirname(jarPath), + detached: false + }); + + // 打印后端日志,监听启动成功标志 + springProcess.stdout.on('data', (data) => { + console.log(`SpringBoot: ${data}`); + // 检测到启动成功日志立即进入主界面 + if (data.toString().includes('Started RuoYiApplication')) { + openAppIfNotOpened(); + } + }); + + // 打印后端错误,检测启动失败 + springProcess.stderr.on('data', (data) => { + console.error(`SpringBoot ERROR: ${data}`); + const errorStr = data.toString(); + // 检测到关键错误信息,直接退出 + if (errorStr.includes('APPLICATION FAILED TO START') || + errorStr.includes('Port') && errorStr.includes('already in use') || + errorStr.includes('Unable to start embedded Tomcat')) { + console.error('后端启动失败,程序即将退出'); + app.quit(); + } + }); + + // 后端退出时,前端同步退出 + springProcess.on('close', (code) => { + console.log(`SpringBoot exited with code ${code}`); + if (mainWindow) { + mainWindow.close(); + } else { + app.quit(); + } + }); +} + +// 关闭后端进程(Windows 使用 taskkill 结束整个进程树) +function stopSpringBoot() { + if (!springProcess) return; + try { + if (process.platform === 'win32') { + // Force kill the whole process tree on Windows + try { + const pid = springProcess.pid; + if (pid !== undefined && pid !== null) { + spawn('taskkill', ['/pid', String(pid), '/f', '/t']); + } else { + springProcess.kill(); + } + } catch (e) { + // Fallback + springProcess.kill(); + } + } else { + springProcess.kill('SIGTERM'); + } + } catch (e) { + console.error('Failed to stop Spring Boot process:', e); + } finally { + springProcess = null; + } +} + +// 创建主窗口(预创建但隐藏) +function createWindow () { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + show: false, + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + } + }); + + // 彻底隐藏原生菜单栏 + try { + Menu.setApplicationMenu(null); + mainWindow.setMenuBarVisibility(false); + if (typeof (mainWindow as any).setMenu === 'function') { + (mainWindow as any).setMenu(null); + } + } catch {} + + const rendererPort = process.argv[2]; + mainWindow.loadURL(`http://localhost:${rendererPort}`); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(() => { + // 预创建主窗口(隐藏) + createWindow(); + + // 显示启动页 + const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize; + const splashW = Math.min(Math.floor(sw * 0.8), 1800); + const splashH = Math.min(Math.floor(sh * 0.8), 1200); + splashWindow = new BrowserWindow({ + width: splashW, + height: splashH, + frame: false, + transparent: false, + resizable: false, + alwaysOnTop: true, + show: true, + center: true, + }); + + const candidateSplashPaths = [ + join(__dirname, '../../public', 'splash.html'), + ]; + const foundSplash = candidateSplashPaths.find(p => existsSync(p)); + if (foundSplash) { + splashWindow.loadFile(foundSplash); + } + + // 注释掉后端启动,便于快速调试前端 + // startSpringBoot(); + + // 快速调试模式 - 直接打开主窗口 + setTimeout(() => { + openAppIfNotOpened(); + }, 1000); + + // 注释掉超时机制 + /* + setTimeout(() => { + if (!appOpened) { + console.error('后端启动超时,程序即将退出'); + app.quit(); + } + }, 30000); + */ + + // 保守 CSP(仅允许自身脚本),避免引入不必要的外部脚本 + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': ['script-src \'self\''] + } + }) + }) + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', function () { + stopSpringBoot(); + if (process.platform !== 'darwin') app.quit() +}); + +app.on('before-quit', () => { + stopSpringBoot(); +}); + +ipcMain.on('message', (event, message) => { + console.log(message); +}) \ No newline at end of file diff --git a/electron-vue-template/src/main/preload.ts b/electron-vue-template/src/main/preload.ts new file mode 100644 index 0000000..128fc39 --- /dev/null +++ b/electron-vue-template/src/main/preload.ts @@ -0,0 +1,5 @@ +import {contextBridge, ipcRenderer} from 'electron'; + +contextBridge.exposeInMainWorld('electronAPI', { + sendMessage: (message: string) => ipcRenderer.send('message', message) +}) diff --git a/electron-vue-template/src/main/static/.gitkeep b/electron-vue-template/src/main/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/electron-vue-template/src/main/tsconfig.json b/electron-vue-template/src/main/tsconfig.json new file mode 100644 index 0000000..3556a5a --- /dev/null +++ b/electron-vue-template/src/main/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "../../build/main", + "allowJs": true, + "noImplicitAny": false, + }, + "exclude": ["static"] +} diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue new file mode 100644 index 0000000..73001b8 --- /dev/null +++ b/electron-vue-template/src/renderer/App.vue @@ -0,0 +1,535 @@ + + + + + diff --git a/electron-vue-template/src/renderer/api/amazon.ts b/electron-vue-template/src/renderer/api/amazon.ts new file mode 100644 index 0000000..b4995b9 --- /dev/null +++ b/electron-vue-template/src/renderer/api/amazon.ts @@ -0,0 +1,38 @@ +import { http } from './http'; + +export const amazonApi = { + // 上传Excel文件解析ASIN列表 + importAsinFromExcel(file: File) { + const formData = new FormData(); + formData.append('file', file); + return http.upload<{ code: number, data: { asinList: string[], total: number }, msg: string | null }>('/api/amazon/import/asin', formData); + }, + + getProductsBatch(asinList: string[], batchId: string) { + return http.post<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/batch', { asinList, batchId }); + }, + getLatestProducts() { + return http.get<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/latest'); + }, + getProductsByBatch(batchId: string) { + return http.get<{ products: any[] }>(`/api/amazon/products/batch/${batchId}`); + }, + updateProduct(productData: unknown) { + return http.post('/api/amazon/products/update', productData); + }, + deleteProduct(productId: string) { + return http.post('/api/amazon/products/delete', { id: productId }); + }, + exportToExcel(products: unknown[], options: Record = {}) { + return http.post('/api/amazon/export', { products, ...options }); + }, + getProductStats() { + return http.get('/api/amazon/stats'); + }, + searchProducts(searchParams: Record) { + return http.get('/api/amazon/products/search', searchParams); + }, + openGenmaiSpirit() { + return http.post('/api/genmai/open'); + }, +}; diff --git a/electron-vue-template/src/renderer/api/auth.ts b/electron-vue-template/src/renderer/api/auth.ts new file mode 100644 index 0000000..807c9bc --- /dev/null +++ b/electron-vue-template/src/renderer/api/auth.ts @@ -0,0 +1,90 @@ +import { http } from './http'; + +// 统一响应处理函数 - 适配ERP客户端格式 +function unwrap(res: any): T { + if (res && typeof res.success === 'boolean') { + if (!res.success) { + const message: string = res.message || res.msg || '请求失败'; + throw new Error(message); + } + return res as T; + } + // 兼容标准格式 + if (res && typeof res.code === 'number') { + if (res.code !== 0) { + const message: string = res.msg || '请求失败'; + throw new Error(message); + } + return (res.data as T) ?? ({} as T); + } + return res as T; +} + +// 认证相关类型定义 +interface LoginRequest { + username: string; + password: string; +} + +interface RegisterRequest { + username: string; + password: string; +} + +interface LoginResponse { + success: boolean; + token: string; + permissions: string[]; + username: string; + message?: string; +} + +interface RegisterResponse { + success: boolean; + message?: string; +} + +interface CheckUsernameResponse { + available: boolean; +} + +export const authApi = { + // 用户登录 + login(params: LoginRequest) { + return http + .post('/api/login', params) + .then(res => unwrap(res)); + }, + + // 用户注册 + register(params: RegisterRequest) { + return http + .post('/api/register', params) + .then(res => unwrap(res)); + }, + + // 检查用户名可用性 + checkUsername(username: string) { + return http + .get('/api/check-username', { username }) + .then(res => { + // checkUsername 使用标准格式 {code: 200, data: boolean} + if (res && res.code === 200) { + return { available: res.data }; + } + throw new Error(res?.msg || '检查用户名失败'); + }); + }, + + // 验证token有效性 + verifyToken(token: string) { + return http + .post('/api/verify', { token }) + .then(res => unwrap<{ success: boolean }>(res)); + }, + + // 用户登出 + logout(token: string) { + return http.postVoid('/api/logout', { token }); + }, +}; \ No newline at end of file diff --git a/electron-vue-template/src/renderer/api/device.ts b/electron-vue-template/src/renderer/api/device.ts new file mode 100644 index 0000000..0619223 --- /dev/null +++ b/electron-vue-template/src/renderer/api/device.ts @@ -0,0 +1,48 @@ +import { http } from './http' + +// 与老版保持相同的接口路径与参数 +const base = '/api/device' + +export interface DeviceQuota { + limit: number + used: number +} + +export interface DeviceItem { + deviceId: string + name?: string + status?: 'online' | 'offline' + lastActiveAt?: string +} + +export const deviceApi = { + getQuota(username: string) { + return http.get(`${base}/quota`, { username }).then((res: any) => { + if (res && typeof res.limit !== 'undefined') return res as DeviceQuota + if (res && typeof res.code === 'number') return (res.data as DeviceQuota) || { limit: 0, used: 0 } + return (res?.data as DeviceQuota) || { limit: 0, used: 0 } + }) + }, + + list(username: string) { + return http.get(`${base}/list`, { username }).then((res: any) => { + if (Array.isArray(res)) return res as DeviceItem[] + if (res && typeof res.code === 'number') return (res.data as DeviceItem[]) || [] + return (res?.data as DeviceItem[]) || [] + }) + }, + + register(payload: { username: string }) { + return http.post(`${base}/register`, payload) + }, + + remove(payload: { deviceId: string }) { + return http.postVoid(`${base}/remove`, payload) + }, + + heartbeat(payload: { username: string; deviceId: string; version?: string }) { + return http.postVoid(`${base}/heartbeat`, payload) + }, +} + + diff --git a/electron-vue-template/src/renderer/api/http.ts b/electron-vue-template/src/renderer/api/http.ts new file mode 100644 index 0000000..fe6387f --- /dev/null +++ b/electron-vue-template/src/renderer/api/http.ts @@ -0,0 +1,78 @@ +// 极简 HTTP 工具:仅封装 GET/POST,默认指向本地 8081 +export type HttpMethod = 'GET' | 'POST'; + +const BASE_URL = 'http://localhost:8081'; + +// 将对象转为查询字符串 +function buildQuery(params?: Record): string { + if (!params) return ''; + const usp = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) return; + usp.append(key, String(value)); + }); + const queryString = usp.toString(); + return queryString ? `?${queryString}` : ''; +} + +// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理 +async function request(path: string, options: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + credentials: 'omit', + cache: 'no-store', + ...options, + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(text || `HTTP ${res.status}`); + } + const contentType = res.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return (await res.json()) as T; + } + return (await res.text()) as unknown as T; +} + +export const http = { + get(path: string, params?: Record) { + return request(`${path}${buildQuery(params)}`, { method: 'GET' }); + }, + post(path: string, body?: unknown) { + return request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }); + }, + // 用于无需读取响应体的 POST(如删除/心跳等),从根源避免读取中断 + postVoid(path: string, body?: unknown) { + return fetch(`${BASE_URL}${path}`, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + credentials: 'omit', + cache: 'no-store', + headers: { 'Content-Type': 'application/json' }, + }).then(res => { + if (!res.ok) return res.text().then(t => Promise.reject(new Error(t || `HTTP ${res.status}`))); + return undefined as unknown as void; + }); + }, + // 文件上传:透传 FormData,不设置 Content-Type 让浏览器自动处理 + upload(path: string, form: FormData) { + const res = fetch(`${BASE_URL}${path}`, { + method: 'POST', + body: form, + credentials: 'omit', + cache: 'no-store', + }); + return res.then(async response => { + if (!response.ok) { + const text = await response.text().catch(() => ''); + throw new Error(text || `HTTP ${response.status}`); + } + return response.json() as Promise; + }); + }, +}; + + diff --git a/electron-vue-template/src/renderer/api/rakuten.ts b/electron-vue-template/src/renderer/api/rakuten.ts new file mode 100644 index 0000000..cfed3ba --- /dev/null +++ b/electron-vue-template/src/renderer/api/rakuten.ts @@ -0,0 +1,38 @@ +import { http } from './http'; + +function unwrap(res: any): T { + if (res && typeof res.code === 'number') { + if (res.code !== 0) { + const message: string = res.msg || '请求失败'; + throw new Error(message); + } + return (res.data as T) ?? ({} as T); + } + return res as T; +} + +export const rakutenApi = { + // 上传 Excel 或按店铺名查询 + getProducts(params: { file?: File; shopName?: string; batchId?: string }) { + const formData = new FormData(); + if (params.file) formData.append('file', params.file); + if (params.batchId) formData.append('batchId', params.batchId); + if (params.shopName) formData.append('shopName', params.shopName); + return http + .upload('/api/rakuten/products', formData) + .then(res => unwrap<{ products: any[]; total?: number; sessionId?: string }>(res)); + }, + search1688(imageUrl: string, sessionId?: string) { + const payload: Record = { imageUrl }; + if (sessionId) payload.sessionId = sessionId; + return http.post('/api/rakuten/search1688', payload).then(res => unwrap(res)); + }, + getLatestProducts() { + return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res)); + }, + exportAndSave(exportData: unknown) { + return http + .post('/api/rakuten/export-and-save', exportData) + .then(res => unwrap<{ filePath: string; fileName?: string; recordCount?: number; hasImages?: boolean }>(res)); + }, +}; diff --git a/electron-vue-template/src/renderer/api/shopee.ts b/electron-vue-template/src/renderer/api/shopee.ts new file mode 100644 index 0000000..aa17da9 --- /dev/null +++ b/electron-vue-template/src/renderer/api/shopee.ts @@ -0,0 +1,15 @@ +import { http } from './http'; + +export const shopeeApi = { + getAdHosting(params: Record = {}) { + return http.get('/api/shopee/ad-hosting', params); + }, + getReviews(params: Record = {}) { + return http.get('/api/shopee/reviews', params); + }, + exportData(exportParams: Record = {}) { + return http.post('/api/shopee/export', exportParams); + }, +}; + + diff --git a/electron-vue-template/src/renderer/api/zebra.ts b/electron-vue-template/src/renderer/api/zebra.ts new file mode 100644 index 0000000..bbec8cf --- /dev/null +++ b/electron-vue-template/src/renderer/api/zebra.ts @@ -0,0 +1,56 @@ +// 斑马订单模型(根据页面所需字段精简定义) +export interface ZebraOrder { + orderedAt?: string; + productImage?: string; + productTitle?: string; + shopOrderNumber?: string; + timeSinceOrder?: string; + priceJpy?: number; + productQuantity?: number; + shippingFeeJpy?: number; + serviceFee?: string; + productNumber?: string; + poNumber?: string; + shippingFeeCny?: number; + internationalShippingFee?: number; + poLogisticsCompany?: string; + poTrackingNumber?: string; + internationalTrackingNumber?: string; + trackInfo?: string; +} + +export interface ZebraOrdersResp { + orders: ZebraOrder[]; + total?: number; + totalPages?: number; +} + +import { http } from './http'; + +// 斑马 API:与原 zebra-api.js 对齐的接口封装 +export const zebraApi = { + getOrders(params: Record) { + return http.get('/api/banma/orders', params); + }, + getOrdersByBatch(batchId: string) { + return http.get(`/api/banma/orders/batch/${batchId}`); + }, + getLatestOrders() { + return http.get('/api/banma/orders/latest'); + }, + getShops() { + return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>('/api/banma/shops'); + }, + refreshToken() { + return http.post('/api/banma/refresh-token'); + }, + exportAndSaveOrders(exportData: unknown) { + return http.post<{ filePath: string }>('/api/banma/export-and-save', exportData); + }, + getOrderStats() { + return http.get('/api/banma/orders/stats'); + }, + searchOrders(searchParams: Record) { + return http.get('/api/banma/orders/search', searchParams); + }, +}; diff --git a/electron-vue-template/src/renderer/assets/vite.svg b/electron-vue-template/src/renderer/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/electron-vue-template/src/renderer/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/assets/vue.svg b/electron-vue-template/src/renderer/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/electron-vue-template/src/renderer/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/HelloWorld.vue b/electron-vue-template/src/renderer/components/HelloWorld.vue new file mode 100644 index 0000000..cc340bc --- /dev/null +++ b/electron-vue-template/src/renderer/components/HelloWorld.vue @@ -0,0 +1 @@ + diff --git a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue new file mode 100644 index 0000000..2b69c15 --- /dev/null +++ b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue @@ -0,0 +1,393 @@ + + + + + + + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue new file mode 100644 index 0000000..d7804c7 --- /dev/null +++ b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue @@ -0,0 +1,124 @@ + + + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue new file mode 100644 index 0000000..3706259 --- /dev/null +++ b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue @@ -0,0 +1,162 @@ + + + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/layout/NavigationBar.vue b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue new file mode 100644 index 0000000..2fa3ce1 --- /dev/null +++ b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue @@ -0,0 +1,172 @@ + + + + + \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue new file mode 100644 index 0000000..5d118ec --- /dev/null +++ b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue @@ -0,0 +1,632 @@ + + + + + + + + diff --git a/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue new file mode 100644 index 0000000..6970023 --- /dev/null +++ b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue @@ -0,0 +1,322 @@ + + + + + + + + + diff --git a/electron-vue-template/src/renderer/index.html b/electron-vue-template/src/renderer/index.html new file mode 100644 index 0000000..e2214f1 --- /dev/null +++ b/electron-vue-template/src/renderer/index.html @@ -0,0 +1,13 @@ + + + + + Vite + Vue template + + + + +
+ + + diff --git a/electron-vue-template/src/renderer/main.ts b/electron-vue-template/src/renderer/main.ts new file mode 100644 index 0000000..f922389 --- /dev/null +++ b/electron-vue-template/src/renderer/main.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import './style.css'; +import 'element-plus/dist/index.css' +import ElementPlus from 'element-plus' +import App from './App.vue' + +const app = createApp(App) +app.use(ElementPlus) +app.mount('#app') diff --git a/electron-vue-template/src/renderer/style.css b/electron-vue-template/src/renderer/style.css new file mode 100644 index 0000000..84a0050 --- /dev/null +++ b/electron-vue-template/src/renderer/style.css @@ -0,0 +1,89 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/electron-vue-template/src/renderer/tsconfig.json b/electron-vue-template/src/renderer/tsconfig.json new file mode 100644 index 0000000..041150b --- /dev/null +++ b/electron-vue-template/src/renderer/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "lib": ["esnext", "dom"], + "types": ["vite/client"] + }, + "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"], + } + diff --git a/electron-vue-template/src/renderer/typings/electron.d.ts b/electron-vue-template/src/renderer/typings/electron.d.ts new file mode 100644 index 0000000..671acd1 --- /dev/null +++ b/electron-vue-template/src/renderer/typings/electron.d.ts @@ -0,0 +1,12 @@ +/** + * Should match main/preload.ts for typescript support in renderer + */ +export default interface ElectronApi { + sendMessage: (message: string) => void +} + +declare global { + interface Window { + electronAPI: ElectronApi, + } +} diff --git a/electron-vue-template/src/renderer/typings/shims-vue.d.ts b/electron-vue-template/src/renderer/typings/shims-vue.d.ts new file mode 100644 index 0000000..b07a059 --- /dev/null +++ b/electron-vue-template/src/renderer/typings/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/electron-vue-template/vite.config.js b/electron-vue-template/vite.config.js new file mode 100644 index 0000000..fe34814 --- /dev/null +++ b/electron-vue-template/vite.config.js @@ -0,0 +1,23 @@ +const Path = require('path'); +const vuePlugin = require('@vitejs/plugin-vue') + +const { defineConfig } = require('vite'); + +/** + * https://vitejs.dev/config + */ +const config = defineConfig({ + root: Path.join(__dirname, 'src', 'renderer'), + publicDir: Path.join(__dirname, 'public'), + server: { + port: 8083, + }, + open: false, + build: { + outDir: Path.join(__dirname, 'build', 'renderer'), + emptyOutDir: true, + }, + plugins: [vuePlugin()], +}); + +module.exports = config; diff --git a/erp_client_sb/.claude/settings.local.json b/erp_client_sb/.claude/settings.local.json new file mode 100644 index 0000000..01385df --- /dev/null +++ b/erp_client_sb/.claude/settings.local.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(mvn:*)", + "Bash(del:*)", + "Bash(rm:*)", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthController.java\")", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\login.html\")", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthProxyController.java\")", + "Bash(java:*)", + "Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\shopee-platform.html\")", + "Bash(mkdir:*)", + "Bash(\"taskkill\" \"/f\" \"/pid\" \"9804\")", + "Bash(dir:*)", + "Bash(find:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/erp_client_sb/data/device.id b/erp_client_sb/data/device.id new file mode 100644 index 0000000..6a3963c --- /dev/null +++ b/erp_client_sb/data/device.id @@ -0,0 +1 @@ +DEVICE-CD31378598644EAA8C0E8153A9D80959 \ No newline at end of file diff --git a/erp_client_sb/data/erp-cache.db b/erp_client_sb/data/erp-cache.db new file mode 100644 index 0000000..8a28c25 Binary files /dev/null and b/erp_client_sb/data/erp-cache.db differ diff --git a/erp_client_sb/pom.xml b/erp_client_sb/pom.xml new file mode 100644 index 0000000..bd56eee --- /dev/null +++ b/erp_client_sb/pom.xml @@ -0,0 +1,185 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.4 + + + com.tashow.erp + erp_client_sb + 2.4.7 + erp_client_sb + erp客户端 + + UTF-8 + ${java.version} + ${java.version} + ${java.version} + 17 + 3.5.4 + yyyy-MM-dd HH:mm:ss + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + com.qiniu + qiniu-java-sdk + 7.12.1 + + + + us.codecraft + webmagic-core + 1.0.3 + + + + us.codecraft + webmagic-extension + 1.0.3 + + + + + + org.projectlombok + lombok + 1.18.38 + provided + + + + cn.hutool + hutool-all + 5.8.36 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.xerial + sqlite-jdbc + 3.42.0.0 + + + org.hibernate.orm + hibernate-community-dialects + 6.2.7.Final + + + org.seleniumhq.selenium + selenium-java + 4.23.0 + + + io.github.bonigarcia + webdrivermanager + 5.9.2 + + + + org.python + jython-standalone + 2.7.4 + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + + + aliyun-repository + aliyun repository + http://maven.aliyun.com/nexus/content/groups/public/ + + + jboss-repository + jboss repository + http://repository.jboss.org/nexus/content/groups/public-jboss/ + + + + + + + src/main/resources + true + + application*.yml + application*.properties + + + + src/main/resources + false + + application*.yml + application*.properties + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + com.tashow.erp.ErpClientSbApplication + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + + + + diff --git a/erp_client_sb/src/main/deploy/package/windows/javafx-boot-1.0.ico b/erp_client_sb/src/main/deploy/package/windows/javafx-boot-1.0.ico new file mode 100644 index 0000000..6fcfdc3 Binary files /dev/null and b/erp_client_sb/src/main/deploy/package/windows/javafx-boot-1.0.ico differ diff --git a/erp_client_sb/src/main/java/com/tashow/erp/ErpClientSbApplication.java b/erp_client_sb/src/main/java/com/tashow/erp/ErpClientSbApplication.java new file mode 100644 index 0000000..9675cd2 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/ErpClientSbApplication.java @@ -0,0 +1,37 @@ +package com.tashow.erp; + +import com.tashow.erp.utils.ErrorReporter; +import com.tashow.erp.utils.ResourcePreloader; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +@Slf4j +@SpringBootApplication +public class ErpClientSbApplication { + + public static void main(String[] args) { + // 纯 Spring Boot 启动,不再依赖 JavaFX + ConfigurableApplicationContext applicationContext = SpringApplication.run(ErpClientSbApplication.class, args); + try { + ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class); + Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { + log.error("捕获到未处理异常: " + ex.getMessage(), ex); + errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex); + }); + log.info("全局异常处理器已设置"); + } catch (Exception e) { + log.warn("未设置 ErrorReporter,继续启动: {}", e.getMessage()); + } + + // 如需预加载资源,可按需保留 + try { + ResourcePreloader.init(); + ResourcePreloader.preloadErpDashboard(); + ResourcePreloader.executePreloading(); + } catch (Throwable t) { + log.warn("资源预加载失败: {}", t.getMessage()); + } + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/CharsetKit.java b/erp_client_sb/src/main/java/com/tashow/erp/common/CharsetKit.java new file mode 100644 index 0000000..27a3498 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/CharsetKit.java @@ -0,0 +1,85 @@ +package com.tashow.erp.common; + + +import com.tashow.erp.utils.StringUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java new file mode 100644 index 0000000..dd6f255 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java @@ -0,0 +1,169 @@ +package com.tashow.erp.common; + + +import java.util.Locale; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * 系统语言 + */ + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMETER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMETER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" }; +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/Convert.java b/erp_client_sb/src/main/java/com/tashow/erp/common/Convert.java new file mode 100644 index 0000000..3f302bb --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/Convert.java @@ -0,0 +1,1021 @@ +package com.tashow.erp.common; + + + +import com.tashow.erp.utils.StringUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + if (StringUtils.isEmpty(str)) + { + return new String[] {}; + } + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + case "是": + return true; + case "false": + case "no": + case "0": + case "否": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + return new String(c); + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/DatabaseConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/DatabaseConfig.java new file mode 100644 index 0000000..1f37b0f --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/DatabaseConfig.java @@ -0,0 +1,33 @@ +package com.tashow.erp.config; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +/** + * 数据库配置,确保数据目录存在 + */ +@Slf4j +@Component +@Order(1) // 确保在其他组件之前运行 +public class DatabaseConfig implements ApplicationRunner { + @Override + public void run(ApplicationArguments args) throws Exception { + // 确保data目录存在 + Path dataDir = Paths.get("data"); + if (!Files.exists(dataDir)) { + try { + Files.createDirectories(dataDir); + log.info("创建数据目录: {}", dataDir.toAbsolutePath()); + } catch (Exception e) { + log.error("创建数据目录失败: {}", e.getMessage()); + throw e; + } + } else { + log.info("数据目录已存在: {}", dataDir.toAbsolutePath()); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java b/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java new file mode 100644 index 0000000..4b6e343 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/ErrorReportAspect.java @@ -0,0 +1,50 @@ +package com.tashow.erp.config; + +import com.tashow.erp.utils.ErrorReporter; +import com.tashow.erp.utils.JsonData; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 错误上报切面 - 自动捕获所有Controller异常并上报,同时检测错误返回值 + */ +@Aspect +@Component +public class ErrorReportAspect { + + @Autowired + private ErrorReporter errorReporter; + + /** + * 拦截所有Controller方法,自动上报异常和错误返回值 + */ + @Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)") + public Object aroundController(ProceedingJoinPoint point) throws Throwable { + String methodName = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName(); + + try { + Object result = point.proceed(); + + // 检查返回值是否表示错误 + if (result instanceof JsonData) { + JsonData jsonData = (JsonData) result; + // code != 0 表示失败(根据JsonData注释:0表示成功,-1表示失败,1表示处理中) + if (jsonData.getCode() != null && jsonData.getCode() != 0) { + // 创建一个RuntimeException来包装错误信息 + String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误"; + Exception syntheticException = new RuntimeException("业务处理失败: " + errorMsg); + errorReporter.reportBusinessError(methodName + " 返回错误", syntheticException); + } + } + + return result; + } catch (Exception e) { + // 自动上报未捕获的异常 + errorReporter.reportBusinessError(methodName + " 抛出异常", e); + throw e; + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/FxWeaverConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/FxWeaverConfig.java new file mode 100644 index 0000000..f12c405 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/FxWeaverConfig.java @@ -0,0 +1,18 @@ +// package com.tashow.erp.config; +// +// 已移除 FxWeaver 相关配置(项目改为纯 Spring Boot)。 +// 如需恢复 JavaFX 集成,请取消注释并恢复依赖。 +// +// import net.rgielen.fxweaver.core.FxWeaver; +// import net.rgielen.fxweaver.spring.SpringFxWeaver; +// import org.springframework.context.ConfigurableApplicationContext; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// +// @Configuration +// public class FxWeaverConfig { +// @Bean +// public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) { +// return new SpringFxWeaver(applicationContext); +// } +// } \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/JavaBridgeConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/JavaBridgeConfig.java new file mode 100644 index 0000000..5782d71 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/JavaBridgeConfig.java @@ -0,0 +1,13 @@ +package com.tashow.erp.config; + +import com.tashow.erp.fx.controller.JavaBridge; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JavaBridgeConfig { + @Bean + public JavaBridge javaBridge() { + return new JavaBridge(); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/MySplashScreen.java b/erp_client_sb/src/main/java/com/tashow/erp/config/MySplashScreen.java new file mode 100644 index 0000000..4e44598 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/MySplashScreen.java @@ -0,0 +1,63 @@ +// package com.tashow.erp.config; +// +// 已转为纯 Spring Boot,JavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。 +// +// import javafx.application.Platform; +// import javafx.scene.Scene; +// import javafx.scene.control.ProgressBar; +// import javafx.scene.image.Image; +// import javafx.scene.image.ImageView; +// import javafx.scene.layout.StackPane; +// import javafx.scene.layout.VBox; +// import javafx.stage.Stage; +// import javafx.stage.StageStyle; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.stereotype.Component; +// +// /** +// * 自定义启动屏幕 +// */ +// @Slf4j +// @Component +// public class MySplashScreen { +// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png"; +// private Stage splashStage; +// private ProgressBar progressBar; +// +// public void show() { +// Platform.runLater(() -> { +// try { +// splashStage = new Stage(); +// splashStage.initStyle(StageStyle.UNDECORATED); +// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE)); +// ImageView imageView = new ImageView(splashImage); +// ProgressBar progressBar = new ProgressBar(); +// progressBar.setPrefWidth(splashImage.getWidth()); +// progressBar.setMaxWidth(Double.MAX_VALUE); +// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;"); +// StackPane root = new StackPane(); +// VBox progressContainer = new VBox(); +// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER); +// progressContainer.setSpacing(0); +// progressContainer.getChildren().add(progressBar); +// root.getChildren().addAll(imageView, progressContainer); +// Scene scene = new Scene(root); +// splashStage.setScene(scene); +// splashStage.setResizable(false); +// splashStage.centerOnScreen(); +// splashStage.show(); +// } catch (Exception e) { +// log.error("显示启动屏幕失败", e); +// } +// }); +// } +// +// public void hide() { +// Platform.runLater(() -> { +// if (splashStage != null) { +// splashStage.hide(); +// log.info("启动屏幕已隐藏"); +// } +// }); +// } +// } \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/RestTemplateConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/RestTemplateConfig.java new file mode 100644 index 0000000..5d43dad --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/RestTemplateConfig.java @@ -0,0 +1,19 @@ +package com.tashow.erp.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; + +@Component +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + RestTemplateBuilder builder = new RestTemplateBuilder(); + builder.connectTimeout(Duration.ofSeconds(5)); + builder.readTimeout(Duration.ofSeconds(10)); + return builder.build(); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/WebConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/WebConfig.java new file mode 100644 index 0000000..7b24546 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/WebConfig.java @@ -0,0 +1,23 @@ +package com.tashow.erp.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web配置类 + * 配置CORS以支持JavaFX WebView的网络请求 + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:8083", "http://127.0.0.1:8083", "file://") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/config/WebMvcConfig.java b/erp_client_sb/src/main/java/com/tashow/erp/config/WebMvcConfig.java new file mode 100644 index 0000000..1ee343f --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/config/WebMvcConfig.java @@ -0,0 +1,27 @@ +package com.tashow.erp.config; + +import com.tashow.erp.security.LocalJwtAuthInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private LocalJwtAuthInterceptor localJwtAuthInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localJwtAuthInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns( + "/api/login", + "/api/register", + "/api/verify", + "/api/check-username"); + } +} + + diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/AmazonController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/AmazonController.java new file mode 100644 index 0000000..836ccce --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/AmazonController.java @@ -0,0 +1,89 @@ +package com.tashow.erp.controller; +import com.tashow.erp.repository.AmazonProductRepository; +import com.tashow.erp.service.IAmazonScrapingService; +import com.tashow.erp.utils.ExcelParseUtil; +import com.tashow.erp.utils.JsonData; +import com.tashow.erp.utils.LoggerUtil; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +@RestController +@RequestMapping("/api/amazon") +public class AmazonController { + private static final Logger logger = LoggerUtil.getLogger(AmazonController.class); + @Autowired + private IAmazonScrapingService amazonScrapingService; + @Autowired + private AmazonProductRepository amazonProductRepository; + /** + * 批量获取亚马逊产品信息 + */ + @PostMapping("/products/batch") + public JsonData batchGetProducts(@RequestBody Object request) { + @SuppressWarnings("unchecked") + Map requestMap = (Map) request; + List asinList = (List) requestMap.get("asinList"); + String batchId = (String) requestMap.get("batchId"); + return JsonData.buildSuccess(amazonScrapingService.batchGetProductInfo(asinList, batchId)); + } + + /** + * 获取最新产品数据 + */ + @GetMapping("/products/latest") + public JsonData getLatestProducts() { + List> products = amazonProductRepository.findLatestProducts() + .parallelStream() + .map(entity -> { + Map map = new HashMap<>(); + map.put("asin", entity.getAsin()); + map.put("title", entity.getTitle()); + map.put("price", entity.getPrice()); + map.put("imageUrl", entity.getImageUrl()); + map.put("productUrl", entity.getProductUrl()); + map.put("brand", entity.getBrand()); + map.put("category", entity.getCategory()); + map.put("rating", entity.getRating()); + map.put("reviewCount", entity.getReviewCount()); + map.put("availability", entity.getAvailability()); + map.put("seller", entity.getSeller()); + map.put("shipper", entity.getSeller()); + return map; + }) + .collect(Collectors.toList()); + Map result = new HashMap<>(); + result.put("products", products); + result.put("total", products.size()); + return JsonData.buildSuccess(result); + } + + /** + * 解析Excel文件获取ASIN列表 + */ + @PostMapping("/import/asin") + public JsonData importAsinFromExcel(@RequestParam("file") MultipartFile file) { + if (file.isEmpty()) { + return JsonData.buildError("上传文件为空"); + } + try { + List asinList = ExcelParseUtil.parseFirstColumn(file); + if (asinList.isEmpty()) { + return JsonData.buildError("未从文件中解析到ASIN数据"); + } + + Map result = new HashMap<>(); + result.put("asinList", asinList); + result.put("total", asinList.size()); + return JsonData.buildSuccess(result); + + } catch (Exception e) { + logger.error("解析文件失败: {}", e.getMessage(), e); + return JsonData.buildError("解析失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java new file mode 100644 index 0000000..0cdba52 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/AuthController.java @@ -0,0 +1,193 @@ +package com.tashow.erp.controller; +import com.tashow.erp.entity.AuthTokenEntity; +import com.tashow.erp.entity.CacheDataEntity; +import com.tashow.erp.repository.AuthTokenRepository; +import com.tashow.erp.repository.CacheDataRepository; +import com.tashow.erp.service.IAuthService; +import com.tashow.erp.utils.JsonData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api") +public class AuthController { + @Autowired + private IAuthService authService; + @Autowired + private AuthTokenRepository authTokenRepository; + @Autowired + private CacheDataRepository cacheDataRepository; + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map loginData) { + String username = (String) loginData.get("username"); + String password = (String) loginData.get("password"); + if (username == null || password == null) { + return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空")); + } + Map result = authService.login(username, password); + Object success = result.get("success"); + Object tokenObj = result.get("token"); + if (Boolean.TRUE.equals(success) && tokenObj instanceof String token && token != null && !token.isEmpty()) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60)); + return ResponseEntity.ok().headers(headers).body(result); + } + return ResponseEntity.ok(result); + } + + @PostMapping("/verify") + public ResponseEntity verifyToken(@RequestBody Map data) { + String token = (String) data.get("token"); + if (token == null) { + return ResponseEntity.ok(Map.of("code", 400, "message", "token不能为空")); + } + Map result = authService.verifyToken(token); + return ResponseEntity.ok(result); + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody Map registerData) { + String username = (String) registerData.get("username"); + String password = (String) registerData.get("password"); + if (username == null || password == null) { + return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空")); + } + Map result = authService.register(username, password); + Object success2 = result.get("success"); + Object tokenObj2 = result.get("token"); + if (Boolean.TRUE.equals(success2) && tokenObj2 instanceof String token && token != null && !token.isEmpty()) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60)); + return ResponseEntity.ok().headers(headers).body(result); + } + return ResponseEntity.ok(result); + } + + @GetMapping("/check-username") + public ResponseEntity checkUsername(@RequestParam String username) { + if (username == null || username.trim().isEmpty()) { + return ResponseEntity.ok(Map.of("code", 400, "message", "用户名不能为空")); + } + boolean available = authService.checkUsername(username); + return ResponseEntity.ok(Map.of( + "code", 200, + "message", "检查成功", + "data", available + )); + } + + /** + * 保存认证密钥 + */ + @PostMapping("/auth/save") + public JsonData saveAuth(@RequestBody Map data) { + String serviceName = (String) data.get("serviceName"); + String authKey = (String) data.get("authKey"); + + if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空"); + + AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity()); + entity.setServiceName(serviceName); + entity.setToken(authKey); + authTokenRepository.save(entity); + + return JsonData.buildSuccess("认证信息保存成功"); + } + + + @GetMapping("/auth/get") + public JsonData getAuth(@RequestParam String serviceName) { + return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null)); + } + + /** + * 删除认证密钥 + */ + @DeleteMapping("/auth/remove") + public JsonData removeAuth(@RequestParam String serviceName) { + authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete); + return JsonData.buildSuccess("认证信息删除成功"); + } + + /** + * 保存缓存数据 + */ + @PostMapping("/cache/save") + public JsonData saveCache(@RequestBody Map data) { + String key = (String) data.get("key"); + String value = (String) data.get("value"); + + if (key == null || value == null) return JsonData.buildError("key和value不能为空"); + + CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity()); + entity.setCacheKey(key); + entity.setCacheValue(value); + cacheDataRepository.save(entity); + + return JsonData.buildSuccess("缓存数据保存成功"); + } + + /** + * 获取缓存数据 + */ + @GetMapping("/cache/get") + public JsonData getCache(@RequestParam String key) { + return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key) + .map(CacheDataEntity::getCacheValue).orElse(null)); + } + + /** + * 删除缓存数据 + */ + @DeleteMapping("/cache/remove") + public JsonData removeCache(@RequestParam String key) { + cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete); + return JsonData.buildSuccess("缓存数据删除成功"); + } + + /** + * 删除缓存数据 - POST方式 + */ + @PostMapping("/cache/delete") + public JsonData deleteCacheByPost(@RequestParam String key) { + if (key == null || key.trim().isEmpty()) { + return JsonData.buildError("key不能为空"); + } + cacheDataRepository.deleteByCacheKey(key); + return JsonData.buildSuccess("缓存数据删除成功"); + } + + /** + * 会话引导:检查SQLite中是否存在token + */ + @GetMapping("/session/bootstrap") + public ResponseEntity sessionBootstrap() { + Optional tokenEntity = cacheDataRepository.findByCacheKey("token"); + if (tokenEntity.isEmpty()) { + return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录")); + } + String token = tokenEntity.get().getCacheValue(); + if (token == null || token.isEmpty()) { + return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录")); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60)); + return ResponseEntity.ok().headers(headers).body(Map.of("code", 200, "message", "会话已恢复")); + } + + private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("=").append(value).append(";"); + sb.append(" Path=/;"); + sb.append(" HttpOnly;"); + sb.append(" SameSite=Strict;"); + if (maxAgeSeconds > 0) { + sb.append(" Max-Age=").append(maxAgeSeconds).append(";"); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/BanmaOrderController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/BanmaOrderController.java new file mode 100644 index 0000000..d8cc465 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/BanmaOrderController.java @@ -0,0 +1,116 @@ +package com.tashow.erp.controller; +import com.tashow.erp.fx.controller.JavaBridge; +import com.tashow.erp.repository.BanmaOrderRepository; +import com.tashow.erp.service.IBanmaOrderService; +import com.tashow.erp.utils.ExcelExportUtil; +import com.tashow.erp.utils.JsonData; +import com.tashow.erp.utils.LoggerUtil; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@RestController +@RequestMapping("/api/banma") +public class BanmaOrderController { + private static final Logger logger = LoggerUtil.getLogger(BanmaOrderController.class); + @Autowired + IBanmaOrderService banmaOrderService; + @Autowired + BanmaOrderRepository banmaOrderRepository; + @Autowired + JavaBridge javaBridge; + @Autowired + RestTemplate restTemplate; + @GetMapping("/orders") + public ResponseEntity> getOrders( + @RequestParam(required = false, name = "startDate") String startDate, + @RequestParam(required = false, name = "endDate") String endDate, + @RequestParam(defaultValue = "1", name = "page") int page, + @RequestParam(defaultValue = "10", name = "pageSize") int pageSize, + @RequestParam(required = false, name = "batchId") String batchId, + @RequestParam(required = false, name = "shopIds") String shopIds) { + List shopIdList = shopIds != null ? java.util.Arrays.asList(shopIds.split(",")) : null; + Map result = banmaOrderService.getOrdersByPage(startDate, endDate, page, pageSize, batchId, shopIdList); + return ResponseEntity.ok(result); + } + /** + * 获取店铺列表 + */ + @GetMapping("/shops") + public JsonData getShops() { + try { + Map response = banmaOrderService.getShops(); + return JsonData.buildSuccess(response); + } catch (Exception e) { + logger.error("获取店铺列表失败: {}", e.getMessage(), e); + return JsonData.buildError("获取店铺列表失败: " + e.getMessage()); + } + } + + /** + * 刷新斑马认证Token + */ + @PostMapping("/refresh-token") + public JsonData refreshToken(){ + try { + banmaOrderService.refreshToken(); + return JsonData.buildSuccess("Token刷新成功"); + } catch (Exception e) { + logger.error("刷新Token失败: {}", e.getMessage(), e); + return JsonData.buildError("Token刷新失败: " + e.getMessage()); + } + } + /** + * 获取最新订单数据 + */ + @GetMapping("/orders/latest") + public JsonData getLatestOrders() { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + List> orders = banmaOrderRepository.findLatestOrders() + .parallelStream() + .map(entity -> { + try { + @SuppressWarnings("unchecked") + Map data = mapper.readValue(entity.getOrderData(), Map.class); + return data; + } catch (Exception e) { + return new HashMap(); + } + }) + .filter(order -> !order.isEmpty()) + .toList(); + + return JsonData.buildSuccess(Map.of("orders", orders, "total", orders.size())); + } + + /** + * JavaFX专用:导出并保存Excel文件到桌面 + */ + @PostMapping("/export-and-save") + public JsonData exportAndSave(@RequestBody Map body) { + try { + @SuppressWarnings("unchecked") + List> orders = (List>) body.get("orders"); + String[] headers = {"下单时间", "商品图片", "商品名称", "乐天订单号", "下单距今时间", "乐天订单金额/日元", + "购买数量", "税费/日元", "服务商回款抽点rmb", "商品番号", "1688采购订单号", + "采购金额/rmb", "国际运费/rmb", "国内物流公司", "国内物流单号", "日本物流单号", "地址状态"}; + + byte[] excelData = ExcelExportUtil.createExcelWithImages("斑马订单数据", headers, orders, 1, "productImage"); + if (excelData.length == 0) return JsonData.buildError("生成Excel文件失败"); + + String fileName = String.format("斑马订单数据_%s.xlsx", java.time.LocalDate.now().toString()); + String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName); + + return savedPath != null + ? JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName)) + : JsonData.buildError("保存文件失败,请检查权限"); + } catch (Exception e) { + logger.error("导出并保存斑马订单Excel失败: {}", e.getMessage(), e); + return JsonData.buildError("导出并保存Excel失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/ConfigController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/ConfigController.java new file mode 100644 index 0000000..21ed894 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/ConfigController.java @@ -0,0 +1,30 @@ +package com.tashow.erp.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 配置信息控制器 + */ +@RestController +@RequestMapping("/api/config") +public class ConfigController { + + @Value("${api.server.base-url}") + private String serverBaseUrl; + + /** + * 获取服务器配置 + */ + @GetMapping("/server") + public Map getServerConfig() { + return Map.of( + "baseUrl", serverBaseUrl, + "sseUrl", serverBaseUrl + "/monitor/account/events" + ); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/DeviceProxyController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/DeviceProxyController.java new file mode 100644 index 0000000..1f1c49f --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/DeviceProxyController.java @@ -0,0 +1,66 @@ +package com.tashow.erp.controller; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import com.tashow.erp.utils.ApiForwarder; +import com.tashow.erp.utils.DeviceUtils; +import java.util.Map; +import java.util.HashMap; + +/** + * 设备管理代理控制器 + * 简化职责:透传请求到后端服务 + */ +@RestController +public class DeviceProxyController { + + @Autowired + private RestTemplate restTemplate; + + @Value("${api.server.base-url}") + private String serverBaseUrl; + + /** + * 注册设备 + */ + @Autowired + private ApiForwarder apiForwarder; + + @PostMapping("/api/device/register") + public ResponseEntity deviceRegister(@RequestBody Map body, @RequestHeader(value = "Authorization", required = false) String auth) { + Map deviceData = new HashMap<>(body); + deviceData.put("deviceId", DeviceUtils.generateDeviceId()); + return apiForwarder.post("/monitor/device/register", deviceData, auth); + } + + + + @PostMapping("/api/device/remove") + public ResponseEntity deviceRemove(@RequestBody Map body, @RequestHeader(value = "Authorization", required = false) String auth) { + return apiForwarder.post("/monitor/device/remove", body, auth); + } + + /** + * 设备心跳 + */ + @PostMapping("/api/device/heartbeat") + public ResponseEntity deviceHeartbeat(@RequestBody Map body, @RequestHeader(value = "Authorization", required = false) String auth) { + return apiForwarder.post("/monitor/device/heartbeat", body, auth); + } + + @GetMapping("/api/device/quota") + public ResponseEntity deviceQuota(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) { + return apiForwarder.get("/monitor/device/quota?username=" + username, auth); + } + + @GetMapping("/api/device/list") + public ResponseEntity deviceList(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) { + return apiForwarder.get("/monitor/device/list?username=" + username, auth); + } + + +} + + diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/GenmaiController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/GenmaiController.java new file mode 100644 index 0000000..5205ed4 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/GenmaiController.java @@ -0,0 +1,25 @@ +package com.tashow.erp.controller; + +import com.tashow.erp.service.IGenmaiService; +import com.tashow.erp.utils.JsonData; +import com.tashow.erp.utils.LoggerUtil; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/genmai") +public class GenmaiController { + private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class); + + @Autowired + private IGenmaiService genmaiService; + + /** + * 打开跟卖精灵网页 + */ + @PostMapping("/open") + public void openGenmaiWebsite() { + genmaiService.openGenmaiWebsite(); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/HomeController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/HomeController.java new file mode 100644 index 0000000..ef7e85a --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/HomeController.java @@ -0,0 +1,18 @@ +package com.tashow.erp.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping("/") + public String home() { + return "redirect:/html/erp-dashboard.html"; + } + + @GetMapping("/erp") + public String erp() { + return "redirect:/html/erp-dashboard.html"; + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/ProxyController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/ProxyController.java new file mode 100644 index 0000000..a6bdfa0 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/ProxyController.java @@ -0,0 +1,116 @@ +package com.tashow.erp.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +/** + * 代理控制器,用于解决CORS跨域问题 + */ +@RestController +@RequestMapping("/api/proxy") +public class ProxyController { + + @Autowired + private RestTemplate restTemplate; + + /** + * 代理获取图片 + * @param requestBody 包含图片URL的请求体 + * @return 图片字节数组 + */ + @PostMapping("/image") + public ResponseEntity proxyImage(@RequestBody Map requestBody) { + String imageUrl = requestBody.get("imageUrl"); + if (imageUrl == null || imageUrl.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + try { + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + headers.set("Accept", "image/jpeg, image/png, image/webp, image/*"); + + HttpEntity entity = new HttpEntity<>(headers); + + // 发送请求获取图片 + ResponseEntity response = restTemplate.exchange( + imageUrl, + HttpMethod.GET, + entity, + byte[].class + ); + + // 设置响应头 + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.IMAGE_JPEG); + + return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * 通过URL参数代理获取图片(为JavaFX WebView优化) + * @param imageUrl 图片URL + * @return 图片字节数组 + */ + @GetMapping("/image-url") + public ResponseEntity proxyImageByUrl(@RequestParam("url") String imageUrl) { + if (imageUrl == null || imageUrl.isEmpty()) { + System.err.println("图片代理请求失败: 图片URL为空"); + return ResponseEntity.badRequest().build(); + } + + System.out.println("代理图片请求: " + imageUrl); + + try { + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + headers.set("Accept", "image/jpeg, image/png, image/webp, image/*"); + headers.set("Referer", "https://item.rakuten.co.jp/"); + + HttpEntity entity = new HttpEntity<>(headers); + + // 发送请求获取图片 + ResponseEntity response = restTemplate.exchange( + imageUrl, + HttpMethod.GET, + entity, + byte[].class + ); + + System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes"); + + // 设置响应头,支持缓存以提升JavaFX WebView性能 + HttpHeaders responseHeaders = new HttpHeaders(); + + // 尝试从原始响应中获取Content-Type + String contentType = response.getHeaders().getFirst("Content-Type"); + if (contentType != null && contentType.startsWith("image/")) { + responseHeaders.setContentType(MediaType.parseMediaType(contentType)); + } else { + responseHeaders.setContentType(MediaType.IMAGE_JPEG); + } + + // 设置缓存头以提升性能 + responseHeaders.setCacheControl("max-age=3600"); + responseHeaders.set("Access-Control-Allow-Origin", "*"); + + return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK); + } catch (Exception e) { + System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage()); + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java new file mode 100644 index 0000000..e7a6f66 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java @@ -0,0 +1,163 @@ +package com.tashow.erp.controller; + +import com.tashow.erp.model.RakutenProduct; +import com.tashow.erp.model.SearchResult; +import com.tashow.erp.repository.RakutenProductRepository; +import com.tashow.erp.service.Alibaba1688Service; +import com.tashow.erp.service.IRakutenCacheService; +import com.tashow.erp.service.RakutenScrapingService; +import com.tashow.erp.service.impl.Alibaba1688ServiceImpl; +import com.tashow.erp.utils.DataReportUtil; +import com.tashow.erp.utils.ExcelParseUtil; +import com.tashow.erp.utils.JsonData; +import com.tashow.erp.utils.QiniuUtil; +import com.tashow.erp.fx.controller.JavaBridge; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.client.RestTemplate; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; +import java.util.stream.Collectors; +import java.util.Base64; + +@RestController +@RequestMapping("/api/rakuten") +@Slf4j +public class RakutenController { + @Autowired + private RakutenScrapingService rakutenScrapingService; + @Autowired + private Alibaba1688Service alibaba1688Service; + @Autowired + private IRakutenCacheService rakutenCacheService; + @Autowired + private JavaBridge javaBridge; + @Autowired + private DataReportUtil dataReportUtil; + /** + * 获取乐天商品数据(支持单个店铺名或 Excel 文件上传) + * + * @param file 可选,Excel 文件(首列为店铺名) + * @param shopName 可选,单个店铺名 + * @param batchId 可选,批次号 + * @return JsonData 响应 + */ + @PostMapping(value = "/products") + public JsonData getProducts(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(value = "shopName", required = false) String shopName, @RequestParam(value = "batchId", required = false) String batchId) { + try { + // 1. 获取店铺名集合(优先 shopName,其次 Excel) + List shopNames = Optional.ofNullable(shopName).filter(s -> !s.trim().isEmpty()).map(s -> List.of(s.trim())).orElseGet(() -> file != null ? ExcelParseUtil.parseFirstColumn(file) : new ArrayList<>()); + + if (CollectionUtils.isEmpty(shopNames)) { + return JsonData.buildError("未从 Excel 中解析到店铺名,且 shopName 参数为空"); + } + + List allProducts = new ArrayList<>(); + List skippedShops = new ArrayList<>(); + + // 2. 遍历店铺,优先缓存,缺失则爬取 + for (String currentShopName : shopNames) { + if (rakutenCacheService.hasRecentData(currentShopName)) { + // 从缓存获取 + List cached = rakutenCacheService.getProductsByShopName(currentShopName).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList(); + rakutenCacheService.updateSpecificProductsSessionId(cached, batchId); + allProducts.addAll(cached); + skippedShops.add(currentShopName); + log.info("使用缓存数据,店铺: {},数量: {}", currentShopName, cached.size()); + } else { + // 爬取新数据 + log.info("采集新数据: {}", currentShopName); + List fresh = rakutenScrapingService.scrapeProductsWithSearch(currentShopName); + fresh.forEach(p -> p.setOriginalShopName(currentShopName)); + allProducts.addAll(fresh); + log.info("采集完成: {}, 数量: {}", currentShopName, fresh.size()); + } + } + + // 3. 处理新采集的数据,存储并生成 sessionId + List newProducts = allProducts.stream().filter(p -> !skippedShops.contains(p.getOriginalShopName())).toList(); + + if (!newProducts.isEmpty()) { + // 使用已有 sessionId 保存 + rakutenCacheService.saveProductsWithSessionId(newProducts, batchId); + } + // 4. 上报缓存数据使用情况 + int cachedCount = allProducts.size() - newProducts.size(); + if (cachedCount > 0) { + dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0"); + } + + // 5. 如果是单店铺查询,只返回该店铺的商品 + List finalProducts = (shopName != null && !shopName.trim().isEmpty()) ? allProducts.stream().filter(p -> shopName.trim().equals(p.getOriginalShopName())).toList() : allProducts; + + return JsonData.buildSuccess(Map.of("products", finalProducts, "total", finalProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size())); + } catch (Exception e) { + log.error("获取乐天商品失败", e); + return JsonData.buildError("获取乐天商品失败: " + e.getMessage()); + } + } + + /** + * 1688识图搜索API - 自动保存1688搜索结果 + */ + @PostMapping("/search1688") + public JsonData search1688(@RequestBody Map params) { + String imageUrl = (String) params.get("imageUrl"); + String sessionId = (String) params.get("sessionId"); + try { + SearchResult result = alibaba1688Service.get1688Detail(imageUrl); + rakutenScrapingService.update1688DataByImageUrl(result, sessionId, imageUrl); + return JsonData.buildSuccess(result); + } catch (Exception e) { + log.error("1688识图搜索失败", e); + return JsonData.buildError("搜索失败: " + e.getMessage()); + } + } + + + @GetMapping("/products/latest") + public JsonData getLatestProducts() { + try { + List> products = rakutenScrapingService.getLatestProductsForDisplay(); + return JsonData.buildSuccess(Map.of("products", products, "total", products.size())); + } catch (Exception e) { + + e.printStackTrace(); + log.info("获取最新商品数据失败", e); + return JsonData.buildError("获取最新数据失败: " + e.getMessage()); + } + } + @PostMapping("/export-and-save") + public JsonData exportAndSave(@RequestBody Map body) { + try { + @SuppressWarnings("unchecked") List> products = (List>) body.get("products"); + if (CollectionUtils.isEmpty(products)) return JsonData.buildError("没有可导出的数据"); + + boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false); + String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx"); + + String[] headers = {"店铺名", "商品图片", "商品链接", "排名", "商品标题", "价格", "1688识图链接", "1688价格", "1688重量"}; + byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages("乐天商品数据", headers, products, skipImages ? -1 : 1, skipImages ? null : "imgUrl"); + + if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败"); + + String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName); + if (savedPath == null) return JsonData.buildError("保存文件失败"); + + log.info("导出Excel: {}, 记录数: {}", fileName, products.size()); + return JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName, "recordCount", products.size(), "hasImages", !skipImages)); + + } catch (Exception e) { + log.error("导出Excel失败", e); + return JsonData.buildError("导出Excel失败: " + e.getMessage()); + } + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/UpdateController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/UpdateController.java new file mode 100644 index 0000000..4be22ff --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/UpdateController.java @@ -0,0 +1,603 @@ +package com.tashow.erp.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.entity.UpdateStatusEntity; +import com.tashow.erp.repository.UpdateStatusRepository; +import com.tashow.erp.service.IAuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +/** + * Web版本更新控制器 + * + * @author Claude + */ +@RestController +@RequestMapping("/api/update") +public class UpdateController { + @Value("${project.version:2.3.6}") + private String currentVersion; + @Value("${project.build.time:}") + private String buildTime; + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private IAuthService authService; + + @Autowired + private UpdateStatusRepository updateStatusRepository; + + // 下载进度跟踪 + private volatile int downloadProgress = 0; + private volatile long downloadedBytes = 0; + private volatile long totalBytes = 0; + private volatile String downloadStatus = "ready"; // ready, downloading, completed, failed, cancelled + private volatile String downloadSpeed = "0 KB/s"; + private volatile long downloadStartTime = 0; + private volatile boolean downloadCancelled = false; + + /** + * 获取当前版本号 + * + * @return 当前版本号 + */ + @GetMapping("/version") + public Map getVersion() { + Map result = new HashMap<>(); + result.put("success", true); + result.put("currentVersion", currentVersion); + result.put("buildTime", buildTime); + return result; + } + + /** + * 检查版本更新 + * + * @return 版本更新信息 + */ + @GetMapping("/check") + public Map checkUpdate() { + Map result = new HashMap<>(); + try { + String response = authService.checkVersion(currentVersion); + // 解析JSON响应 + JsonNode responseNode = objectMapper.readTree(response); + + result.put("success", true); + result.put("currentVersion", currentVersion); + + // 检查响应格式并提取数据 + JsonNode dataNode = null; + dataNode = responseNode.get("data"); + + // 直接使用服务器返回的needUpdate字段 + boolean needUpdate = dataNode.has("needUpdate") && + dataNode.get("needUpdate").asBoolean(); + // 添加跳过版本信息,让前端自己判断 + String skippedVersion = updateStatusRepository.findByKeyName("skippedUpdateVersion") + .map(entity -> entity.getValueData()).orElse(null); + result.put("skippedVersion", skippedVersion); + + result.put("needUpdate", needUpdate); + + // 获取最新版本号 + if (dataNode.has("latestVersion")) { + result.put("latestVersion", dataNode.get("latestVersion").asText()); + } + + if (needUpdate) { + if (dataNode.has("downloadUrl")) { + String downloadUrl = dataNode.get("downloadUrl").asText(); + result.put("downloadUrl", downloadUrl); + saveUpdateInfo("downloadUrl", downloadUrl); + } + if (dataNode.has("updateTime")) { + // 转换时间戳为可读格式 + long updateTime = dataNode.get("updateTime").asLong(); + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String releaseDate = sdf.format(new java.util.Date(updateTime)); + result.put("releaseDate", releaseDate); + saveUpdateInfo("releaseDate", releaseDate); + } + if (dataNode.has("updateNotes")) { + String updateNotes = dataNode.get("updateNotes").asText(); + result.put("updateNotes", updateNotes); + saveUpdateInfo("updateNotes", updateNotes); + } + if (dataNode.has("fileSize")) { + String fileSize = dataNode.get("fileSize").asText(); + result.put("fileSize", fileSize); + saveUpdateInfo("fileSize", fileSize); + } + if (dataNode.has("latestVersion")) { + String latestVersion = dataNode.get("latestVersion").asText(); + saveUpdateInfo("latestVersion", latestVersion); + } + } else { + // 如果不需要更新,清理之前保存的更新信息 + clearUpdateInfo(); + } + + // 从SQLite读取之前保存的更新信息 + result.putAll(getStoredUpdateInfo()); + + + } catch (Exception e) { + result.put("success", false); + result.put("message", "版本检查失败:" + e.getMessage()); + authService.reportError("UPDATE_CHECK_ERROR", "Web版本检查失败", e); + } + + return result; + } + + /** + * 保存更新信息到SQLite + */ + private void saveUpdateInfo(String key, String value) { + try { + UpdateStatusEntity entity = updateStatusRepository.findByKeyName(key) + .orElse(new UpdateStatusEntity()); + entity.setKeyName(key); + entity.setValueData(value); + updateStatusRepository.save(entity); + } catch (Exception e) { + System.err.println("保存更新信息失败: " + key + " = " + value + ", 错误: " + e.getMessage()); + } + } + + /** + * 从SQLite获取存储的更新信息 + */ + private Map getStoredUpdateInfo() { + Map info = new HashMap<>(); + try { + String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"}; + for (String key : keys) { + updateStatusRepository.findByKeyName(key).ifPresent(entity -> + info.put(key, entity.getValueData()) + ); + } + } catch (Exception e) { + System.err.println("读取更新信息失败: " + e.getMessage()); + } + return info; + } + + /** + * 清理更新信息 + */ + private void clearUpdateInfo() { + try { + String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"}; + for (String key : keys) { + updateStatusRepository.findByKeyName(key).ifPresent(entity -> + updateStatusRepository.delete(entity) + ); + } + } catch (Exception e) { + System.err.println("清理更新信息失败: " + e.getMessage()); + } + } + + + /** + * 获取下载进度 + * + * @return 下载进度信息 + */ + @GetMapping("/progress") + public Map getDownloadProgress() { + Map result = new HashMap<>(); + result.put("success", true); + result.put("progress", downloadProgress); + result.put("status", downloadStatus); + result.put("downloadedBytes", downloadedBytes); + result.put("totalBytes", totalBytes); + result.put("downloadSpeed", downloadSpeed); + result.put("downloadedMB", String.format("%.1f", downloadedBytes / 1024.0 / 1024.0)); + result.put("totalMB", String.format("%.1f", totalBytes / 1024.0 / 1024.0)); + + return result; + } + + /** + * 重置下载状态 + * + * @return 重置结果 + */ + @PostMapping("/reset") + public Map resetDownloadStatus() { + Map result = new HashMap<>(); + downloadProgress = 0; + downloadedBytes = 0; + totalBytes = 0; + downloadStatus = "ready"; + downloadSpeed = "0 KB/s"; + downloadStartTime = 0; + downloadCancelled = false; + tempUpdateFilePath = null; + + result.put("success", true); + result.put("message", "下载状态已重置"); + + return result; + } + + /** + * 取消下载 + * + * @return 取消结果 + */ + @PostMapping("/cancel") + public Map cancelDownload() { + Map result = new HashMap<>(); + + if ("downloading".equals(downloadStatus)) { + downloadCancelled = true; + downloadStatus = "cancelled"; + downloadSpeed = "已取消"; + result.put("success", true); + result.put("message", "下载已取消"); + } else if ("completed".equals(downloadStatus)) { + // 下载完成时点击取消,不删除文件,只是标记为稍后更新 + result.put("success", true); + result.put("message", "已设置为稍后更新,文件保留"); + System.out.println("用户选择稍后更新,文件路径: " + tempUpdateFilePath); + } else { + result.put("success", false); + result.put("message", "无效的操作状态"); + } + + return result; + } + + /** + * 完全清除更新文件和状态 + * + * @return 清除结果 + */ + @PostMapping("/clear") + public Map clearUpdateFiles() { + Map result = new HashMap<>(); + + if (tempUpdateFilePath != null) { + try { + java.io.File tempFile = new java.io.File(tempUpdateFilePath); + if (tempFile.exists()) { + tempFile.delete(); + System.out.println("已删除更新文件: " + tempUpdateFilePath); + } + } catch (Exception e) { + System.err.println("删除临时文件失败: " + e.getMessage()); + } + } + + // 重置状态 + downloadProgress = 0; + downloadedBytes = 0; + totalBytes = 0; + downloadStatus = "ready"; + downloadSpeed = "0 KB/s"; + downloadCancelled = false; + tempUpdateFilePath = null; + result.put("success", true); + result.put("message", "更新文件和状态已清除"); + + return result; + } + /** + * 验证更新文件是否存在 + * + * @return 验证结果 + */ + @GetMapping("/verify-file") + public Map verifyUpdateFile() { + Map result = new HashMap<>(); + + try { + boolean fileExists = false; + String filePath = ""; + + if (tempUpdateFilePath != null) { + java.io.File updateFile = new java.io.File(tempUpdateFilePath); + fileExists = updateFile.exists(); + filePath = tempUpdateFilePath; + + if (fileExists) { + result.put("fileSize", updateFile.length()); + result.put("lastModified", new java.util.Date(updateFile.lastModified())); + } + } + + result.put("success", true); + result.put("fileExists", fileExists); + result.put("filePath", filePath); + result.put("downloadStatus", downloadStatus); + + System.out.println("验证更新文件: " + filePath + ", 存在: " + fileExists); + + } catch (Exception e) { + result.put("success", false); + result.put("message", "验证文件失败:" + e.getMessage()); + result.put("fileExists", false); + } + + return result; + } + + /** + * 用户确认后执行安装 + * + * @return 安装结果 + */ + @PostMapping("/install") + public Map installUpdate() { + Map result = new HashMap<>(); + + if (tempUpdateFilePath == null || !new java.io.File(tempUpdateFilePath).exists()) { + result.put("success", false); + result.put("message", "更新文件不存在,请重新下载"); + return result; + } + + try { + result.put("success", true); + result.put("message", "开始安装更新..."); + + // 异步执行安装,避免阻塞响应 + new Thread(() -> { + String updateScript = createUpdateScript(null, tempUpdateFilePath); + if (updateScript != null) { + executeUpdateAndExit(updateScript); + } + }).start(); + + } catch (Exception e) { + result.put("success", false); + result.put("message", "启动安装失败:" + e.getMessage()); + } + + return result; + } + + /** + * 自动更新:下载、替换、重启 + * + * @return 更新结果 + */ + @PostMapping("/auto-update") + public Map autoUpdate(@RequestBody Map request) { + Map result = new HashMap<>(); + + try { + String downloadUrl = request.get("downloadUrl"); + result.put("success", true); + result.put("message", "开始自动更新..."); + result.put("downloadUrl", downloadUrl); + String finalDownloadUrl = downloadUrl; + new Thread(() -> { + performUpdate(finalDownloadUrl); + }).start(); + + } catch (Exception e) { + result.put("success", false); + result.put("message", "启动更新失败:" + e.getMessage()); + } + + return result; + } + + /** + * 执行更新过程 + */ + private void performUpdate(String downloadUrl) { + String tempUpdateFile = downloadUpdate(downloadUrl); + if (tempUpdateFile == null) { + downloadStatus = "failed"; + return; + } + + // 下载完成后不自动重启,等待用户确认 + downloadStatus = "completed"; + System.out.println("下载完成,等待用户确认安装..."); + + // 将下载文件路径保存,供后续安装使用 + this.tempUpdateFilePath = tempUpdateFile; + } + + private String tempUpdateFilePath = null; + + + /** + * 下载更新文件 + */ + private String downloadUpdate(String downloadUrl) { + try { + downloadStatus = "downloading"; + downloadProgress = 0; + downloadedBytes = 0; + downloadStartTime = System.currentTimeMillis(); + + URL url = new URL(downloadUrl); + URLConnection connection = url.openConnection(); + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + connection.setConnectTimeout(10000); + connection.setReadTimeout(30000); + + // 获取文件大小 + totalBytes = connection.getContentLength(); + if (totalBytes <= 0) { + totalBytes = 70 * 1024 * 1024; // 默认70MB + } + + String tempDir = System.getProperty("java.io.tmpdir"); + String updateFileName = "erp_update_" + System.currentTimeMillis() + ".exe"; + String tempUpdatePath = Paths.get(tempDir, updateFileName).toString(); + + System.out.println("开始下载更新文件,大小: " + (totalBytes / 1024 / 1024) + "MB"); + + try (InputStream inputStream = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(tempUpdatePath)) { + + byte[] buffer = new byte[8192]; + int bytesRead; + long lastSpeedUpdate = System.currentTimeMillis(); + long lastDownloadedBytes = 0; + while ((bytesRead = inputStream.read(buffer)) != -1) { + if (downloadCancelled) { + System.out.println("下载已取消,停止下载进程"); + downloadStatus = "cancelled"; + downloadSpeed = "已取消"; + outputStream.close(); + try { + java.io.File partialFile = new java.io.File(tempUpdatePath); + if (partialFile.exists()) { + partialFile.delete(); + } + } catch (Exception e) { + System.err.println("删除部分下载文件失败: " + e.getMessage()); + } + return null; + } + outputStream.write(buffer, 0, bytesRead); + downloadedBytes += bytesRead; + + // 计算下载进度 + downloadProgress = (int) ((downloadedBytes * 100) / totalBytes); + // 每秒计算一次下载速度 + long currentTime = System.currentTimeMillis(); + if (currentTime - lastSpeedUpdate >= 1000) { + long speedBytes = downloadedBytes - lastDownloadedBytes; + double speedKB = speedBytes / 1024.0; + + if (speedKB >= 1024) { + downloadSpeed = String.format("%.1f MB/s", speedKB / 1024.0); + } else { + downloadSpeed = String.format("%.1f KB/s", speedKB); + } + + lastSpeedUpdate = currentTime; + lastDownloadedBytes = downloadedBytes; + + System.out.println(String.format("下载进度: %d%% (%d/%d MB) 速度: %s", + downloadProgress, + downloadedBytes / 1024 / 1024, + totalBytes / 1024 / 1024, + downloadSpeed)); + } + + // 防止阻塞UI线程 + if (downloadedBytes % (1024 * 1024) == 0) { // 每MB休息一下 + Thread.sleep(10); + } + } + + downloadStatus = "completed"; + downloadProgress = 100; + downloadSpeed = "完成"; + System.out.println("下载完成: " + (downloadedBytes / 1024 / 1024) + "MB"); + } + + return tempUpdatePath; + } catch (Exception e) { + downloadStatus = "failed"; + downloadSpeed = "失败"; + System.err.println("下载失败: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 创建更新脚本 + */ + private String createUpdateScript(String currentExePath, String tempUpdateFile) { + try { + String tempDir = System.getProperty("java.io.tmpdir"); + String scriptPath = Paths.get(tempDir, "update_erp.bat").toString(); + + File currentDir = new File(System.getProperty("user.dir")); + File targetExe = new File(currentDir, "erpClient.exe"); + String targetPath = targetExe.getAbsolutePath(); + + StringBuilder script = new StringBuilder(); + script.append("@echo off\r\n"); + script.append("chcp 65001 > nul\r\n"); + script.append("echo 正在等待程序完全退出...\r\n"); + script.append("timeout /t 1 /nobreak > nul\r\n"); + script.append("echo 开始更新程序文件...\r\n"); + script.append("if exist \"").append(targetPath).append("\" ("); + script.append(" move \"").append(targetPath).append("\" \"").append(targetPath).append(".backup\""); + script.append(" )\r\n"); + script.append("move \"").append(tempUpdateFile).append("\" \"").append(targetPath).append("\"\r\n"); + script.append("if exist \"").append(targetPath).append("\" (\r\n"); + script.append(" echo 更新成功!程序将在1秒后重新启动...\r\n"); + script.append(" timeout /t 1 /nobreak > nul\r\n"); + script.append(" start \"\" \"").append(targetPath).append("\"\r\n"); + script.append(" if exist \"").append(targetPath).append(".backup\" del \"").append(targetPath).append(".backup\"\r\n"); + script.append(") else (\r\n"); + script.append(" echo 更新失败!正在恢复原版本...\r\n"); + script.append(" if exist \"").append(targetPath).append(".backup\" (\r\n"); + script.append(" move \"").append(targetPath).append(".backup\" \"").append(targetPath).append("\"\r\n"); + script.append(" echo 已恢复原版本,程序将在1秒后重新启动...\r\n"); + script.append(" timeout /t 1 /nobreak > nul\r\n"); + script.append(" start \"\" \"").append(targetPath).append("\"\r\n"); + script.append(" )\r\n"); + script.append(")\r\n"); + script.append("echo 更新操作完成!\r\n"); + script.append("timeout /t 1 /nobreak > nul\r\n"); + script.append("(goto) 2>nul & del \"%~f0\" & exit\r\n"); + + Files.write(Paths.get(scriptPath), script.toString().getBytes("GBK")); + return scriptPath; + } catch (Exception e) { + return null; + } + } + + /** + * 执行更新脚本并退出程序 + */ + private void executeUpdateAndExit(String scriptPath) { + try { + System.out.println("下载完成!正在准备更新..."); + ProcessBuilder pb = new ProcessBuilder("cmd", "/c", scriptPath); + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); + pb.start(); + System.out.println("更新程序已启动,当前程序即将退出..."); + Thread.sleep(1000); + Runtime.getRuntime().halt(0); + } catch (Exception e) { + Runtime.getRuntime().halt(1); + } + } + + /** + * 保存跳过的版本 + */ + @PostMapping("/skip-version") + public Map saveSkippedVersion(@RequestBody Map request) { + Map result = new HashMap<>(); + try { + saveUpdateInfo("skippedUpdateVersion", request.get("version")); + result.put("success", true); + } catch (Exception e) { + result.put("success", false); + } + return result; + } + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/Alibaba1688ProductEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/Alibaba1688ProductEntity.java new file mode 100644 index 0000000..2fda257 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/Alibaba1688ProductEntity.java @@ -0,0 +1,59 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 1688产品实体类 + */ +@Entity +@Table(name = "alibaba_1688_products") +@Data +public class Alibaba1688ProductEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "product_id", unique = true, nullable = false) + private String productId; + + @Column(name = "title", length = 1000) + private String title; + + @Column(name = "price") + private String price; + + @Column(name = "image_url", length = 1000) + private String imageUrl; + + @Column(name = "product_url", length = 1000) + private String productUrl; + + @Column(name = "supplier") + private String supplier; + + @Column(name = "category") + private String category; + + @Column(name = "min_order") + private String minOrder; + + @Column(name = "trade_assurance") + private String tradeAssurance; + + @Column(name = "session_id") + private String sessionId; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/AmazonProductEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/AmazonProductEntity.java new file mode 100644 index 0000000..272d18e --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/AmazonProductEntity.java @@ -0,0 +1,64 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * Amazon产品缓存实体类 + */ +@Entity +@Table(name = "amazon_products") +@Data +public class AmazonProductEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true, nullable = false) + private String asin; + + @Column(name = "title", length = 1000) + private String title; + + @Column(name = "price") + private String price; + + @Column(name = "image_url", length = 1000) + private String imageUrl; + + @Column(name = "product_url", length = 1000) + private String productUrl; + + @Column(name = "brand") + private String brand; + + @Column(name = "category") + private String category; + + @Column(name = "rating") + private String rating; + + @Column(name = "review_count") + private String reviewCount; + + @Column(name = "availability") + private String availability; + + @Column(name = "seller") + private String seller; + + @Column(name = "session_id") + private String sessionId; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/AuthTokenEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/AuthTokenEntity.java new file mode 100644 index 0000000..96512b2 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/AuthTokenEntity.java @@ -0,0 +1,38 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 认证令牌实体类 + */ +@Entity +@Table(name = "auth_tokens") +@Data +public class AuthTokenEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "service_name", unique = true, nullable = false) + private String serviceName; + + @Column(name = "token", nullable = false) + private String token; + + @Column(name = "expire_time") + private LocalDateTime expireTime; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/BanmaOrderEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/BanmaOrderEntity.java new file mode 100644 index 0000000..307214b --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/BanmaOrderEntity.java @@ -0,0 +1,38 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 斑马订单实体类 + */ +@Entity +@Table(name = "banma_orders") +@Data +public class BanmaOrderEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "tracking_number", unique = true, nullable = false) + private String trackingNumber; + + @Column(name = "order_data", columnDefinition = "TEXT") + private String orderData; + + @Column(name = "session_id") + private String sessionId; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/CacheDataEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/CacheDataEntity.java new file mode 100644 index 0000000..2865618 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/CacheDataEntity.java @@ -0,0 +1,38 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 缓存数据实体类 + */ +@Entity +@Table(name = "cache_data") +@Data +public class CacheDataEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "cache_key", unique = true, nullable = false) + private String cacheKey; + + @Column(name = "cache_value", columnDefinition = "TEXT") + private String cacheValue; + + @Column(name = "expire_time") + private LocalDateTime expireTime; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/RakutenProductEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/RakutenProductEntity.java new file mode 100644 index 0000000..9aa3889 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/RakutenProductEntity.java @@ -0,0 +1,86 @@ +package com.tashow.erp.entity; + +import com.fasterxml.jackson.annotation.JsonRawValue; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 乐天产品缓存实体类 + */ +@Entity +@Table(name = "rakuten_products") +@Data +public class RakutenProductEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "original_shop_name") + private String originalShopName; + + @Column(name = "shop_name") + private String shopName; + + @Column(name = "product_url", length = 1000) + private String productUrl; + + @Column(name = "img_url", length = 1000) + private String imgUrl; + + @Column(name = "product_title", length = 500) + private String productTitle; + + @Column(name = "product_name", length = 500) + private String productName; + + @Column(name = "price") + private String price; + + @Column(name = "ranking") + private String ranking; + + @Column(name = "price_1688") + private String price1688; + + + @Column(name = "detail_url_1688", length = 1000) + private String detailUrl1688; + + @Column(name = "image_1688_url", length = 1000) + private String image1688Url; + + + @Column(name = "map_recognition_link", length = 1000) + private String mapRecognitionLink; // 1688识图链接 + + @Column(name = "freight") + private Double freight; // 运费 + + @Column(name = "median") + private Double median; // 中位价格 + + @Column(name = "weight") + private String weight; // 重量 + @Column(name = "sku_price_json", columnDefinition = "JSON") + private String skuPriceJson; // SKU价格JSON字符串 + + @Column(name = "session_id") + private String sessionId; // 用于标识一次导入会话 + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/UpdateStatusEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/UpdateStatusEntity.java new file mode 100644 index 0000000..caac5d9 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/UpdateStatusEntity.java @@ -0,0 +1,81 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * 更新状态实体 + */ +@Entity +@Table(name = "update_status") +public class UpdateStatusEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "key_name", unique = true) + private String keyName; + + @Column(name = "value_data", columnDefinition = "TEXT") + private String valueData; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getKeyName() { + return keyName; + } + + public void setKeyName(String keyName) { + this.keyName = keyName; + } + + public String getValueData() { + return valueData; + } + + public void setValueData(String valueData) { + this.valueData = valueData; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/entity/ZebraOrderEntity.java b/erp_client_sb/src/main/java/com/tashow/erp/entity/ZebraOrderEntity.java new file mode 100644 index 0000000..eff0882 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/entity/ZebraOrderEntity.java @@ -0,0 +1,65 @@ +package com.tashow.erp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 斑马订单缓存实体类 + */ +@Entity +@Table(name = "zebra_orders") +@Data +public class ZebraOrderEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "order_id") + private String orderId; + + @Column(name = "product_name", length = 500) + private String productName; + + @Column(name = "quantity") + private Integer quantity; + + @Column(name = "price_jpy") + private String priceJpy; + + @Column(name = "price_cny") + private String priceCny; + + @Column(name = "image_url", length = 1000) + private String imageUrl; + + @Column(name = "customer_name") + private String customerName; + + @Column(name = "order_status") + private String orderStatus; + + @Column(name = "order_date") + private String orderDate; + + @Column(name = "shipping_address", length = 1000) + private String shippingAddress; + + @Column(name = "tracking_number") + private String trackingNumber; + + @Column(name = "session_id") + private String sessionId; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/JavaBridge.java b/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/JavaBridge.java new file mode 100644 index 0000000..1ff68b3 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/JavaBridge.java @@ -0,0 +1,79 @@ +package com.tashow.erp.fx.controller; + +import lombok.extern.slf4j.Slf4j; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +@Slf4j +public class JavaBridge { + + /** + * 直接保存字节数组为Excel文件到桌面(纯 Spring Boot 环境,无文件对话框) + */ + public String saveExcelFileToDesktop(byte[] data, String fileName) { + try { + if (data == null || data.length == 0) { + log.warn("文件数据为空,无法保存文件"); + return null; + } + + String userHome = System.getProperty("user.home"); + File desktop = new File(userHome, "Desktop"); + if (!desktop.exists()) { + // 回退到用户目录 + desktop = new File(userHome); + } + + File file = new File(desktop, fileName); + int counter = 1; + if (fileName != null && fileName.contains(".")) { + String baseName = fileName.substring(0, fileName.lastIndexOf('.')); + String extension = fileName.substring(fileName.lastIndexOf('.')); + while (file.exists()) { + file = new File(desktop, baseName + "_" + counter + extension); + counter++; + } + } else { + while (file.exists()) { + file = new File(desktop, fileName + "_" + counter); + counter++; + } + } + + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(data); + fos.flush(); + } + + String filePath = file.getAbsolutePath(); + log.info("Excel文件已保存: {}", filePath); + return filePath; + } catch (IOException e) { + log.error("保存Excel文件失败: {}", e.getMessage(), e); + return null; + } + } + + /** + * 复制文本到系统剪贴板 + */ + public boolean copyToClipboard(String text) { + try { + if (text == null || text.trim().isEmpty()) { + return false; + } + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + StringSelection selection = new StringSelection(text); + clipboard.setContents(selection, null); + return true; + } catch (Exception e) { + log.error("复制到剪贴板失败: {}", e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/MainCtrl.java b/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/MainCtrl.java new file mode 100644 index 0000000..376702c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/fx/controller/MainCtrl.java @@ -0,0 +1,43 @@ +// package com.tashow.erp.fx.controller; +// +// 已转为纯 Spring Boot,不再包含 JavaFX 控制器与视图逻辑。如需恢复 FX,请取消注释并恢复依赖。 +// +// import javafx.application.Platform; +// import javafx.fxml.FXML; +// import javafx.fxml.Initializable; +// import javafx.scene.layout.BorderPane; +// import javafx.scene.web.WebEngine; +// import javafx.scene.web.WebView; +// import lombok.extern.slf4j.Slf4j; +// import net.rgielen.fxweaver.core.FxmlView; +// import org.springframework.stereotype.Component; +// import netscape.javascript.JSObject; +// +// import java.net.URL; +// import java.util.ResourceBundle; +// +// @Slf4j +// @Component +// @FxmlView("/static/fxml/Main.fxml") +// public class MainCtrl implements Initializable { +// @FXML +// public BorderPane rootPane; +// @FXML +// public WebView webView; +// private WebEngine webEngine; +// @Override +// public void initialize(URL location, ResourceBundle resources) { +// if (Platform.isFxApplicationThread()) { +// initWebView(); +// } else { +// Platform.runLater(this::initWebView); +// } +// } +// +// private void initWebView() { +// webEngine = webView.getEngine(); +// webEngine.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."); +// webEngine.setJavaScriptEnabled(true); +// webEngine.load("http://localhost:8081/html/erp-dashboard.html"); +// } +// } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/fx/view/MainView.java b/erp_client_sb/src/main/java/com/tashow/erp/fx/view/MainView.java new file mode 100644 index 0000000..eeacd8a --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/fx/view/MainView.java @@ -0,0 +1,11 @@ +// package com.tashow.erp.fx.view; +// +// 已转为纯 Spring Boot,移除 FX 视图。 +// import javafx.scene.Parent; +// import net.rgielen.fxweaver.core.FxmlView; +// import org.springframework.stereotype.Component; +// +// @Component +// @FxmlView("/static/fxml/Main.fxml") +// public class MainView { +// } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/model/ClientAlibaba1688Monitor.java b/erp_client_sb/src/main/java/com/tashow/erp/model/ClientAlibaba1688Monitor.java new file mode 100644 index 0000000..ef9011b --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/model/ClientAlibaba1688Monitor.java @@ -0,0 +1,25 @@ +package com.tashow.erp.model; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 1688爬取风控监控数据表实体类 + */ +@Data +public class ClientAlibaba1688Monitor { + private Long id; + + private String clientId; + + private String ipAddress; + + private String eventType; + + private Long eventTime; + + private Long duration; + + private LocalDateTime createTime; + + private String remark; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/model/MonitoringData.java b/erp_client_sb/src/main/java/com/tashow/erp/model/MonitoringData.java new file mode 100644 index 0000000..ea69993 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/model/MonitoringData.java @@ -0,0 +1,23 @@ +package com.tashow.erp.model; + +import lombok.Data; + +/** + * 监控数据类 + */ +@Data +public class MonitoringData { + private String ipAddress; + private MonitorEventType eventType; + private long eventTime; + private long duration; + + /** + * 风控事件类型 + */ + public enum MonitorEventType { + MOBILE_FIRST_ACCESS, // 移动端首次访问 + MOBILE_BLOCKED, // 移动端被风控 + DESKTOP_BLOCKED // 电脑端被风控 + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/model/ProductInfo.java b/erp_client_sb/src/main/java/com/tashow/erp/model/ProductInfo.java new file mode 100644 index 0000000..0b498c8 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/model/ProductInfo.java @@ -0,0 +1,16 @@ +package com.tashow.erp.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 商品信息类 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductInfo { + private String price; + private String weight; +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/model/RakutenProduct.java b/erp_client_sb/src/main/java/com/tashow/erp/model/RakutenProduct.java new file mode 100644 index 0000000..feddb2c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/model/RakutenProduct.java @@ -0,0 +1,49 @@ +package com.tashow.erp.model; + +import lombok.Data; +import java.util.List; + +/** + * 乐天商品模型类 + */ +@Data +public class RakutenProduct { + + private String productUrl; + private String shopName; + private String imgUrl; + private String productTitle; + private String productName; + private String price; + private String ranking; + + // 1688相关字段 + private String price1688; + private String weight1688; + private String detailUrl1688; + + // 新增:用于导入导出与展示 + private String originalShopName; + private String image1688Url; + private String mapRecognitionLink; + private Double freight; + private Double median; + private String weight; + + @Override + public String toString() { + return "RakutenProduct{" + + "productName='" + productName + '\'' + + ", productTitle='" + productTitle + '\'' + + ", price='" + price + '\'' + + ", imageUrl1='" + imgUrl+ '\'' + + ", productUrl='" + productUrl + '\'' + + ", shopName='" + shopName + '\'' + + ", price1688='" + price1688 + '\'' + + ", weight1688='" + weight1688 + '\'' + + ", detailUrl1688='" + detailUrl1688 + '\'' + + '}'; + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/model/SearchResult.java b/erp_client_sb/src/main/java/com/tashow/erp/model/SearchResult.java new file mode 100644 index 0000000..1b44e70 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/model/SearchResult.java @@ -0,0 +1,29 @@ +package com.tashow.erp.model; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + + + +/** + * 1688识图搜索结果 + */ +@Data +public class SearchResult { + + + //运费 + private Double freight; + //识图链接 + private String mapRecognitionLink; + //sku价格 + Map skuPrice; + + //价格中间值 + private Double median; + String weight; + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/Alibaba1688ProductRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/Alibaba1688ProductRepository.java new file mode 100644 index 0000000..f9c969e --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/Alibaba1688ProductRepository.java @@ -0,0 +1,58 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.Alibaba1688ProductEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 1688产品Repository + */ +@Repository +public interface Alibaba1688ProductRepository extends JpaRepository { + + /** + * 根据产品ID查找产品 + */ + Optional findByProductId(String productId); + + /** + * 根据会话ID查找产品(分页) + */ + Page findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable); + + /** + * 根据会话ID查找产品 + */ + List findBySessionIdOrderByCreatedAtDesc(String sessionId); + + /** + * 根据会话ID删除产品 + */ + @Modifying + @Transactional + void deleteBySessionId(String sessionId); + + /** + * 清理过期数据(7天前的数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM Alibaba1688ProductEntity a WHERE a.createdAt < :expireTime") + void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime); + + /** + * 获取最新的会话ID + */ + @Query("SELECT DISTINCT a.sessionId FROM Alibaba1688ProductEntity a ORDER BY a.createdAt DESC") + List findRecentSessionIds(Pageable pageable); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/AmazonProductRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/AmazonProductRepository.java new file mode 100644 index 0000000..94f8ac6 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/AmazonProductRepository.java @@ -0,0 +1,73 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.AmazonProductEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * Amazon产品Repository + */ +@Repository +public interface AmazonProductRepository extends JpaRepository { + + /** + * 根据ASIN查找产品(取最新的一条) + */ + @Query(value = "SELECT * FROM amazon_products WHERE asin = :asin ORDER BY created_at DESC LIMIT 1", nativeQuery = true) + Optional findByAsin(@Param("asin") String asin); + + /** + * 根据会话ID查找产品(分页) + */ + Page findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable); + + /** + * 根据会话ID查找产品 + */ + List findBySessionIdOrderByCreatedAtDesc(String sessionId); + + /** + * 根据会话ID删除产品 + */ + @Modifying + @Transactional + void deleteBySessionId(String sessionId); + + /** + * 清理过期数据(7天前的数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM AmazonProductEntity a WHERE a.createdAt < :expireTime") + void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime); + + /** + * 获取最新的会话ID + */ + @Query("SELECT DISTINCT a.sessionId FROM AmazonProductEntity a ORDER BY a.createdAt DESC") + List findRecentSessionIds(Pageable pageable); + + /** + * 获取最新会话的产品数据(只返回最后一次采集的结果) + */ + @Query(value = "SELECT * FROM amazon_products WHERE session_id = (SELECT session_id FROM amazon_products ORDER BY created_at DESC LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true) + List findLatestProducts(); + + /** + * 删除指定ASIN在指定时间后的数据(用于清理12小时内重复) + */ + @Modifying + @Transactional + @Query("DELETE FROM AmazonProductEntity a WHERE a.asin = :asin AND a.createdAt >= :cutoffTime") + void deleteByAsinAndCreatedAtAfter(@Param("asin") String asin, @Param("cutoffTime") LocalDateTime cutoffTime); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/AuthTokenRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/AuthTokenRepository.java new file mode 100644 index 0000000..407d5b9 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/AuthTokenRepository.java @@ -0,0 +1,44 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.AuthTokenEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 认证令牌Repository + */ +@Repository +public interface AuthTokenRepository extends JpaRepository { + + /** + * 根据服务名查找有效的认证令牌 + */ + Optional findByServiceNameAndExpireTimeAfter(String serviceName, LocalDateTime currentTime); + + /** + * 根据服务名查找认证令牌(不考虑过期时间) + */ + Optional findByServiceName(String serviceName); + + /** + * 清理过期令牌 + */ + @Modifying + @Transactional + @Query("DELETE FROM AuthTokenEntity a WHERE a.expireTime < :expireTime") + void deleteExpiredTokens(@Param("expireTime") LocalDateTime expireTime); + + /** + * 根据服务名删除令牌 + */ + @Modifying + @Transactional + void deleteByServiceName(String serviceName); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/BanmaOrderRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/BanmaOrderRepository.java new file mode 100644 index 0000000..783da28 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/BanmaOrderRepository.java @@ -0,0 +1,89 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.BanmaOrderEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 斑马订单Repository + */ +@Repository +public interface BanmaOrderRepository extends JpaRepository { + + /** + * 根据追踪号查找订单 + */ + Optional findByTrackingNumber(String trackingNumber); + + /** + * 根据会话ID查找订单(分页) + */ + Page findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable); + + /** + * 根据会话ID查找订单 + */ + List findBySessionIdOrderByCreatedAtDesc(String sessionId); + + /** + * 根据会话ID删除订单 + */ + @Modifying + @Transactional + void deleteBySessionId(String sessionId); + + /** + * 清理过期数据(7天前的数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM BanmaOrderEntity b WHERE b.createdAt < :expireTime") + void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime); + + /** + * 获取最新的会话ID + */ + @Query("SELECT DISTINCT b.sessionId FROM BanmaOrderEntity b ORDER BY b.createdAt DESC") + List findRecentSessionIds(Pageable pageable); + + /** + * 查找所有有效订单(按更新时间倒序) + */ + List findAllByOrderByUpdatedAtDesc(); + + /** + * 获取最新会话的订单数据(只返回最后一次采集的结果) + */ + @Query(value = "SELECT * FROM banma_orders WHERE session_id = (SELECT session_id FROM banma_orders ORDER BY created_at DESC LIMIT 1) ORDER BY updated_at ASC, id ASC", nativeQuery = true) + List findLatestOrders(); + + /** + * 删除指定追踪号在指定时间后的数据(用于清理12小时内重复) + */ + @Modifying + @Transactional + @Query("DELETE FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber AND b.createdAt >= :cutoffTime") + void deleteByTrackingNumberAndCreatedAtAfter(@Param("trackingNumber") String trackingNumber, @Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 检查追踪号是否有12小时内的缓存数据 + */ + @Query("SELECT CASE WHEN COUNT(b) > 0 THEN true ELSE false END FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber AND b.createdAt > :cutoffTime") + boolean existsByTrackingNumberAndCreatedAtAfter(@Param("trackingNumber") String trackingNumber, @Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 根据追踪号获取最新的订单数据 + */ + @Query("SELECT b FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber ORDER BY b.createdAt DESC") + Optional findLatestByTrackingNumber(@Param("trackingNumber") String trackingNumber); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/CacheDataRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/CacheDataRepository.java new file mode 100644 index 0000000..9a20fa0 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/CacheDataRepository.java @@ -0,0 +1,33 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.CacheDataEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 缓存数据Repository + */ +@Repository +public interface CacheDataRepository extends JpaRepository { + + + /** + * 根据缓存键查找缓存数据(不考虑过期时间) + */ + Optional findByCacheKey(String cacheKey); + + /** + * 根据缓存键删除数据 + */ + @Modifying + @Transactional + void deleteByCacheKey(String cacheKey); + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/RakutenProductRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/RakutenProductRepository.java new file mode 100644 index 0000000..4a6fade --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/RakutenProductRepository.java @@ -0,0 +1,99 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.RakutenProductEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface RakutenProductRepository extends JpaRepository { + + /** + * 根据会话ID查找产品(分页) + */ + Page findBySessionIdOrderByCreatedAtAscIdAsc(String sessionId, Pageable pageable); + + /** + * 根据会话ID查找产品 + */ + List findBySessionIdOrderByCreatedAtAscIdAsc(String sessionId); + + /** + * 根据会话ID删除产品 + */ + @Modifying + @Transactional + void deleteBySessionId(String sessionId); + + /** + * 清理过期数据(7天前的数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM RakutenProductEntity r WHERE r.createdAt < :expireTime") + void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime); + + /** + * 根据原始店铺名查找产品(按创建时间和ID升序排列,与最新数据查询一致) + */ + List findByOriginalShopNameOrderByCreatedAtAscIdAsc(String originalShopName); + + /** + * 检查指定店铺在指定时间后是否有数据 + */ + boolean existsByOriginalShopNameAndCreatedAtAfter(String originalShopName, LocalDateTime sinceTime); + + /** + * 获取最新的会话ID + */ + @Query("SELECT DISTINCT r.sessionId FROM RakutenProductEntity r ORDER BY r.createdAt DESC") + List findRecentSessionIds(Pageable pageable); + + /** + * 获取最新会话的产品数据(只返回最后一次采集的结果) + */ + @Query(value = "SELECT * FROM rakuten_products WHERE session_id = (SELECT session_id FROM rakuten_products ORDER BY created_at LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true) + List findLatestProducts(); + + /** + * 删除指定商品URL在指定时间后的数据(用于清理12小时内重复) + */ + @Modifying + @Transactional + @Query("DELETE FROM RakutenProductEntity r WHERE r.productUrl = :productUrl AND r.createdAt >= :cutoffTime") + void deleteByProductUrlAndCreatedAtAfter(@Param("productUrl") String productUrl, @Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 删除指定店铺在指定时间后的数据(用于清理12小时内重复) + */ + @Modifying + @Transactional + @Query("DELETE FROM RakutenProductEntity r WHERE r.originalShopName = :shopName AND r.createdAt >= :cutoffTime") + void deleteByOriginalShopNameAndCreatedAtAfter(@Param("shopName") String shopName, @Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 删除指定店铺在指定时间前的数据(用于清理12小时之前的旧数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM RakutenProductEntity r WHERE r.originalShopName = :shopName AND r.createdAt < :cutoffTime") + void deleteByOriginalShopNameAndCreatedAtBefore(@Param("shopName") String shopName, @Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 根据产品URL查找产品(用于1688数据更新) + */ + List findByProductUrl(String productUrl); + + /** + * 根据产品URL列表查找产品 + */ + List findByProductUrlIn(List productUrls); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/UpdateStatusRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/UpdateStatusRepository.java new file mode 100644 index 0000000..0090a4f --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/UpdateStatusRepository.java @@ -0,0 +1,15 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.UpdateStatusEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UpdateStatusRepository extends JpaRepository { + + Optional findByKeyName(String keyName); + + void deleteByKeyName(String keyName); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/repository/ZebraOrderRepository.java b/erp_client_sb/src/main/java/com/tashow/erp/repository/ZebraOrderRepository.java new file mode 100644 index 0000000..fc9f907 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/repository/ZebraOrderRepository.java @@ -0,0 +1,58 @@ +package com.tashow.erp.repository; + +import com.tashow.erp.entity.ZebraOrderEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 斑马订单Repository + */ +@Repository +public interface ZebraOrderRepository extends JpaRepository { + + /** + * 根据会话ID查找订单(分页) + */ + Page findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable); + + /** + * 根据会话ID查找订单 + */ + List findBySessionIdOrderByCreatedAtDesc(String sessionId); + + /** + * 根据会话ID删除订单 + */ + @Modifying + @Transactional + void deleteBySessionId(String sessionId); + + /** + * 清理过期数据(7天前的数据) + */ + @Modifying + @Transactional + @Query("DELETE FROM ZebraOrderEntity z WHERE z.createdAt < :expireTime") + void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime); + + /** + * 获取最新的会话ID + */ + @Query("SELECT DISTINCT z.sessionId FROM ZebraOrderEntity z ORDER BY z.createdAt DESC") + List findRecentSessionIds(Pageable pageable); + + /** + * 获取最新会话的所有订单 + */ + @Query("SELECT z FROM ZebraOrderEntity z WHERE z.sessionId = (SELECT z2.sessionId FROM ZebraOrderEntity z2 ORDER BY z2.createdAt DESC LIMIT 1) ORDER BY z.createdAt DESC") + List findLatestOrders(); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/security/JwksService.java b/erp_client_sb/src/main/java/com/tashow/erp/security/JwksService.java new file mode 100644 index 0000000..75d259a --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/security/JwksService.java @@ -0,0 +1,71 @@ +package com.tashow.erp.security; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class JwksService { + + @Value("${api.server.base-url}") + private String serverBaseUrl; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final Map cache = new ConcurrentHashMap<>(); + + public JwksService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public RSAPublicKey getKeyById(String kid) { + RSAPublicKey cached = cache.get(kid); + if (cached != null) return cached; + loadJwks(); + return cache.get(kid); + } + + public RSAPublicKey[] getAllPublicKeys() { + loadJwks(); + return cache.values().toArray(new RSAPublicKey[0]); + } + + private void loadJwks() { + try { + String url = serverBaseUrl + "/.well-known/jwks.json"; + ResponseEntity resp = restTemplate.getForEntity(url, String.class); + JsonNode root = objectMapper.readTree(resp.getBody()); + JsonNode keys = root.get("keys"); + if (keys == null || !keys.isArray()) return; + for (JsonNode key : keys) { + String id = key.path("kid").asText(null); + String n = key.path("n").asText(null); + String e = key.path("e").asText(null); + if (id == null || n == null || e == null) continue; + if (!cache.containsKey(id)) { + cache.put(id, buildRsaPublicKey(n, e)); + } + } + } catch (Exception ignored) {} + } + + private RSAPublicKey buildRsaPublicKey(String nB64u, String eB64u) throws Exception { + BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(nB64u)); + BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(eB64u)); + RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e); + return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec); + } +} + + diff --git a/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java b/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java new file mode 100644 index 0000000..2296d19 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/security/LocalJwtAuthInterceptor.java @@ -0,0 +1,80 @@ +package com.tashow.erp.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.service.IAuthService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; +import java.util.Base64; + + /** + * 本地拦截器 + */ +@Component +public class LocalJwtAuthInterceptor implements HandlerInterceptor { + + private final IAuthService authService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public LocalJwtAuthInterceptor(IAuthService authService) { + this.authService = authService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String uri = request.getRequestURI(); + if (uri.startsWith("/libs/") || uri.startsWith("/static/") || uri.startsWith("/favicon") + || uri.startsWith("/api/cache") || uri.startsWith("/api/update")) { + return true; + } + String auth = request.getHeader("Authorization"); + if ((auth == null || !auth.startsWith("Bearer ")) && request.getCookies() != null) { + for (jakarta.servlet.http.Cookie c : request.getCookies()) { + if ("FX_TOKEN".equals(c.getName()) && c.getValue() != null && !c.getValue().isEmpty()) { + auth = "Bearer " + c.getValue(); + break; + } + } + } + // 移除本地token查找逻辑,改为仅依赖内存token或cookie + if (auth == null || !auth.startsWith("Bearer ")) { + return true; // 放行,无token场景由后端处理 + } + + String token = auth.substring(7).trim(); + String[] parts = token.split("\\."); + if (parts.length != 3) { + return true; // 放行,由后端处理 + } + + try { + // 仅解析payload来确保基本格式,不进行设备强校验,避免前端大量401 + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1])); + objectMapper.readTree(payloadJson); + } catch (Exception e) { + return true; // 放行,由后端处理 + } + return true; + } + + private void writeJson(HttpServletResponse response, int code, String msg) throws IOException { + response.setStatus(code == 401 ? 401 : 403); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":" + code + ",\"msg\":\"" + msg + "\"}"); + } + + private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("=").append(value).append(";"); + sb.append(" Path=/;"); + sb.append(" HttpOnly;"); + sb.append(" SameSite=Strict;"); + if (maxAgeSeconds > 0) { + sb.append(" Max-Age=").append(maxAgeSeconds).append(";"); + } + return sb.toString(); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/Alibaba1688Service.java b/erp_client_sb/src/main/java/com/tashow/erp/service/Alibaba1688Service.java new file mode 100644 index 0000000..aa3542c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/Alibaba1688Service.java @@ -0,0 +1,31 @@ +package com.tashow.erp.service; + +import com.tashow.erp.model.SearchResult; + +import java.util.List; +import java.util.Map; + +/** + * 1688识图搜索服务接口 + */ +public interface Alibaba1688Service { + + + + /** + * 上传base64图片并获取imageId + * @param base64Image base64编码的图片数据 + * @return 1688的imageId + */ + String uploadImageBase64(String base64Image); + + + /** + * 获取图片的1688详情数据 + * @param imageUrl + * @return + */ + SearchResult get1688Detail(String imageUrl); + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/IAmazonScrapingService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/IAmazonScrapingService.java new file mode 100644 index 0000000..d21b0c8 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/IAmazonScrapingService.java @@ -0,0 +1,27 @@ +package com.tashow.erp.service; + +import java.util.List; +import java.util.Map; + +/** + * 亚马逊数据采集服务接口 + * + * @author ruoyi + */ +public interface IAmazonScrapingService { + + /** + * 批量获取亚马逊产品信息 + * + * @param asinList ASIN列表 + * @param batchId 批次ID + * @return 产品信息列表 + */ + Map batchGetProductInfo(List asinList, String batchId); + + + + + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/IAuthService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/IAuthService.java new file mode 100644 index 0000000..16ef0d2 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/IAuthService.java @@ -0,0 +1,50 @@ +package com.tashow.erp.service; + +import java.util.Map; + +/** + * 认证服务接口 + */ +public interface IAuthService { + + /** + * 客户端登录认证 + */ + Map login(String username, String password); + + /** + * 获取客户端信息 + */ + Map getClientInfo(); + + /** + * 上报错误 + */ + void reportError(String errorType, String errorMessage, Exception e); + + /** + * 检查版本更新 + */ + String checkVersion(String currentVersion); + + /** + * 获取客户端ID + */ + String getClientId(); + + /** + * 验证token + */ + Map verifyToken(String token); + + + /** + * 注册新账号 + */ + Map register(String username, String password); + + /** + * 检查用户名是否可用 + */ + boolean checkUsername(String username); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/IBanmaOrderService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/IBanmaOrderService.java new file mode 100644 index 0000000..5ae5e19 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/IBanmaOrderService.java @@ -0,0 +1,35 @@ +package com.tashow.erp.service; + +import java.util.List; +import java.util.Map; + +/** + * 斑马订单服务接口 + * + * @author ruoyi + */ +public interface IBanmaOrderService { + + /** + * 刷新认证Token + */ + void refreshToken(); + + /** + * 获取店铺列表 + * @return 店铺列表数据 + */ + Map getShops(); + + /** + * 分页获取订单数据(支持batchId) + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param page 页码(从1开始) + * @param pageSize 每页大小 + * @param batchId 批次ID + * @param shopIds 店铺ID列表 + * @return 订单数据列表和总数 + */ + Map getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List shopIds); +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/ICacheService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/ICacheService.java new file mode 100644 index 0000000..c25ac7a --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/ICacheService.java @@ -0,0 +1,12 @@ +package com.tashow.erp.service; + +/** + * 缓存服务接口 + */ +public interface ICacheService { + + /** + * 保存认证令牌 + */ + void saveAuthToken(String service, String token, long expireTimeMillis); +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/IGenmaiService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/IGenmaiService.java new file mode 100644 index 0000000..eb2ef84 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/IGenmaiService.java @@ -0,0 +1,18 @@ +package com.tashow.erp.service; + +/** + * 跟卖精灵服务接口 + * + * @author ruoyi + */ +public interface IGenmaiService { + + /** + * 使用Playwright打开跟卖精灵网页 + * + * @return 是否成功打开 + */ + void openGenmaiWebsite(); + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/IRakutenCacheService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/IRakutenCacheService.java new file mode 100644 index 0000000..c80acdb --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/IRakutenCacheService.java @@ -0,0 +1,50 @@ +package com.tashow.erp.service; + +import com.tashow.erp.model.RakutenProduct; + +import java.util.List; + +/** + * 乐天产品缓存服务接口 + */ +public interface IRakutenCacheService { + + /** + * 保存产品数据 + */ + String saveProducts(List products); + + /** + * 使用指定sessionId保存产品数据 + */ + void saveProductsWithSessionId(List products, String sessionId); + + + + /** + * 检查店铺是否有最近的数据(12小时内) + */ + boolean hasRecentData(String shopName); + + /** + * 根据店铺名获取已有数据 + */ + List getProductsByShopName(String shopName); + + /** + * 更新指定店铺的产品sessionId + */ + void updateProductsSessionId(String shopName, String newSessionId); + + /** + * 更新指定产品列表的sessionId + */ + void updateSpecificProductsSessionId(List products, String newSessionId); + + /** + * 清理指定店铺12小时内的重复数据,为新采集做准备 + */ + void cleanRecentDuplicateData(String shopName); + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/RakutenScrapingService.java b/erp_client_sb/src/main/java/com/tashow/erp/service/RakutenScrapingService.java new file mode 100644 index 0000000..c70a349 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/RakutenScrapingService.java @@ -0,0 +1,40 @@ +package com.tashow.erp.service; + +import com.tashow.erp.model.RakutenProduct; +import com.tashow.erp.model.SearchResult; + +import java.util.List; +import java.util.Map; + +/** + * 乐天商品爬取服务接口 + */ +public interface RakutenScrapingService { + + + + + + + + /** + * 商品爬取,包含1688识图搜索,支持实时回调 + * + * @param shopName 店铺名 + * @return 包含1688搜索结果的商品列表 + */ + List scrapeProductsWithSearch(String shopName); + + + /** + * 根据图片URL更新1688搜索结果的所有字段 + */ + void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl); + + /** + * 获取最新产品数据并转换为前端格式 + */ + List> getLatestProductsForDisplay(); + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java new file mode 100644 index 0000000..6033099 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java @@ -0,0 +1,307 @@ +package com.tashow.erp.service.impl; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.model.SearchResult; +import com.tashow.erp.service.Alibaba1688Service; +import com.tashow.erp.utils.Alibaba1688CookieUtil; +import com.tashow.erp.utils.ErrorReporter; +import com.tashow.erp.utils.QiniuUtil; +import com.tashow.erp.utils.PersistentDriverManager; +import org.openqa.selenium.chrome.ChromeDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 1688识图搜索服务 + */ +@Service +public class Alibaba1688ServiceImpl implements Alibaba1688Service { + private static final Logger logger = LoggerFactory.getLogger(Alibaba1688ServiceImpl.class); + private final Random random = new Random(); + ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate = new RestTemplate(); + private final RestTemplate noSslRestTemplate = createNoSslRestTemplate(); + @Autowired + private ErrorReporter errorReporter; + @Autowired + private PersistentDriverManager driverManager; + private RestTemplate createNoSslRestTemplate() { + try { + TrustManager[] trustManagers = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return null; } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() { + @Override + protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { + super.prepareConnection(connection, httpMethod); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory()); + ((HttpsURLConnection) connection).setHostnameVerifier((hostname, session) -> true); + } + } + }; + return new RestTemplate(requestFactory); + } catch (Exception e) { + logger.warn("创建忽略SSL证书的RestTemplate失败,使用默认RestTemplate", e); + return new RestTemplate(); + } + } + + + /** + * 通过1688 API获取商品详情链接,价格 + * @return + */ + @Override + public SearchResult get1688Detail(String uploadedUrl) { + String fileName = "temp_" + System.currentTimeMillis() + ".png"; + List detailUrls = new ArrayList<>(); + SearchResult result = new SearchResult(); + List prices = new ArrayList<>(); + try { + String imageUrl = QiniuUtil.uploadFromUrl(uploadedUrl, fileName); + String token = Alibaba1688CookieUtil.getToken(); + long timestamp = System.currentTimeMillis(); + // Step 1: 获取推荐的 offerId 列表 + String jsonData = "{\"appId\":\"32517\",\"params\":\"{\\\"categoryId\\\":-1,\\\"imageAddress\\\":\\\"" + imageUrl + "\\\",\\\"interfaceName\\\":\\\"imageOfferSearchService\\\",\\\"needYolocrop\\\":false,\\\"pageIndex\\\":\\\"1\\\",\\\"pageSize\\\":\\\"40\\\",\\\"searchScene\\\":\\\"image\\\",\\\"snAppAb\\\":true,\\\"appName\\\":\\\"ios\\\",\\\"scene\\\":\\\"seoSearch\\\"}\"}"; + String sign = Alibaba1688CookieUtil.generateSign(token, String.valueOf(timestamp), jsonData); + String url = "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/" + "?jsv=2.6.1&appKey=" + Alibaba1688CookieUtil.APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&v=2.0&type=originaljson&isSec=0&timeout=10000" + "&api=mtop.relationrecommend.WirelessRecommend.recommend&ignoreLogin=true&prefix=h5api&dataType=json"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Cookie", Alibaba1688CookieUtil.getCookieString()); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("data", jsonData); + HttpEntity> requestEntity = new HttpEntity<>(formData, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + JsonNode root = objectMapper.readTree(response.getBody()); + Iterator offerIterator = root.path("data").path("offerList").path("offers").elements(); + //运费 + Set freight = new HashSet<>(); + for (int i = 0; i < 10 && offerIterator.hasNext(); i++) { + JsonNode offer = offerIterator.next(); + String offerId = offer.path("id").asText(); + String freightProvFirstFee = offer.path("freightProvFirstFee").asText(); + Optional.ofNullable(freightProvFirstFee) + .map(s -> s.split(";", 2)[0]) + .map(s -> s.split(":", 2)) + .filter(parts -> parts.length == 2 && !parts[1].isBlank()) + .map(parts -> Double.parseDouble(parts[1]) / 100.0) + .ifPresent(freight::add); + prices.add(offer.path("normalPrice").asDouble()); + detailUrls.add(offerId); + } + Collections.sort(prices); + List freightFee = new ArrayList<>(freight); + Collections.sort(freightFee); + //中间值 + Double median=0.0; + if(!prices.isEmpty()){ + median = prices.get(prices.size() / 2 - 1); + } + //sku价格 + Map skuPrices = new TreeMap<>(); + for (String offerId : detailUrls) { + skuPrices.clear(); + skuPrices.putAll(getSkuPrice(offerId)); + if (skuPrices.size() >= 3) { + break; + } + } + + result.setSkuPrice(skuPrices); + result.setMedian( median); + result.setMapRecognitionLink( uploadImageBase64(imageUrl)); + System.out.println("运费"+freightFee); + result.setFreight(freightFee.isEmpty() ? 0.0 :freightFee.get(freightFee.size()/2-1)); + // String weight = getWeight(detailUrls); + // result.setWeight(weight); + return result; + } catch (Exception e) { + errorReporter.reportDataCollectError("Alibaba1688Service", e); + logger.error("通过1688 API获取商品详情链接失败: {}", e.getMessage(), e); + return result; + } + } + + + /** + * 获取sku价格 + * + * @param offerId + * @return + */ + private Map getSkuPrice(String offerId) { + TreeMap skuPrice = new TreeMap<>(); + HttpHeaders headers = new HttpHeaders(); + TreeMap resultMap = new TreeMap<>(); + try { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Cookie", Alibaba1688CookieUtil.getCookieString()); + String token = Alibaba1688CookieUtil.getToken(); + long timestamp = System.currentTimeMillis(); + Map payload = Map.of("offerId", offerId); + String requestJson = objectMapper.writeValueAsString(payload); + String requestSign = Alibaba1688CookieUtil.generateSign(token, String.valueOf(timestamp), requestJson); + String skuUrl = "https://h5api.m.1688.com/h5/mtop.1688.pc.plugin.od.sku.query/1.0/" + "?jsv=2.6.3&appKey=" + Alibaba1688CookieUtil.APP_KEY + "&t=" + timestamp + "&api=mtop.1688.pc.plugin.od.sku.query&sign=" + requestSign + "&v=1.0&type=originaljson&isSec=0&dataType=json"; + MultiValueMap skuForm = new LinkedMultiValueMap<>(); + skuForm.add("data", requestJson); + HttpEntity> skuRequestEntity = new HttpEntity<>(skuForm, headers); + ResponseEntity skuResponse = restTemplate.exchange(skuUrl, HttpMethod.POST, skuRequestEntity, String.class); + JsonNode skuRoot = objectMapper.readTree(skuResponse.getBody()); + JsonNode skuMap = skuRoot.path("data"); + if (skuMap.isEmpty()) { + skuPrice.put(-1.0, "无sku"); + return skuPrice; + } + if (skuMap.isObject()) { + for (Iterator> it = skuMap.fields(); it.hasNext(); ) { + Map.Entry entry = it.next(); + JsonNode skuInfo = entry.getValue(); + String attributes = skuInfo.path("attributes").asText(); + double price = extractPriceFromSku(skuInfo); + skuPrice.put(price, attributes); + } + } + List keys = new ArrayList<>(skuPrice.keySet()); + Map.Entry midEntry = Map.entry(keys.get(keys.size() / 2), skuPrice.get(keys.get(keys.size() / 2))); + resultMap.put(skuPrice.firstKey(), skuPrice.firstEntry().getValue()); + resultMap.put(midEntry.getKey(), midEntry.getValue()); + resultMap.put(skuPrice.lastKey(), skuPrice.lastEntry().getValue()); + + } catch (JsonProcessingException e) { + logger.error("获取sku价格失败: {}", e.getMessage(), e); + return new HashMap<>(); + } + return resultMap; + } + + /** + * 从SKU信息中提取价格 + * 处理1688 API中各种复杂的价格结构 + * + * @param skuInfo SKU节点信息 + * @return 提取到的价格,如果无法提取则返回0.0 + */ + private double extractPriceFromSku(JsonNode skuInfo) { + try { + JsonNode priceNode = skuInfo.path("price"); + // 如果price是数值且大于0,直接返回 + if (priceNode.isNumber() && priceNode.asDouble() > 0) { + return priceNode.asDouble(); + } + + // 如果price是数组且有元素,检查第一个元素 + if (priceNode.isArray() && priceNode.size() > 0) { + JsonNode firstPrice = priceNode.get(0); + if (firstPrice.isNumber() && firstPrice.asDouble() > 0) { + return firstPrice.asDouble(); + } + if (firstPrice.isObject() && firstPrice.has("price") && firstPrice.path("price").asDouble() > 0) { + return firstPrice.path("price").asDouble(); + } + } + return 0.0; + } catch (Exception e) { + logger.error("提取SKU价格时发生异常: {}", e.getMessage(), e); + return 0.0; + } + } + + /** + * 上传图片并获取图片ID + * @return + */ + public String uploadImageBase64(String imageUrl) { + String token = Alibaba1688CookieUtil.getToken(); + long timestamp = System.currentTimeMillis(); + byte[] imageBytes = noSslRestTemplate.getForObject(imageUrl, byte[].class); + String base64Image = Base64.getEncoder().encodeToString(imageBytes); + String jsonData = "{\"appId\":32517,\"params\":\"{\\\"searchScene\\\":\\\"imageEx\\\",\\\"interfaceName\\\":\\\"imageBase64ToImageId\\\",\\\"serviceParam.extendParam[imageBase64]\\\":\\\"" + base64Image + "\\\",\\\"subChannel\\\":\\\"pc_image_search_image_id\\\"}\"}"; + String sign = Alibaba1688CookieUtil.generateSign(token, String.valueOf(timestamp), jsonData); + String url = "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/?jsv=2.7.4&appKey=" + Alibaba1688CookieUtil.APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&api=mtop.relationrecommend.wirelessrecommend.recommend&v=2.0&type=originaljson&timeout=20000&dataType=jsonp"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Cookie", Alibaba1688CookieUtil.getCookieString()); + headers.set("authority", "h5api.m.1688.com"); + headers.set("accept", "application/json"); + headers.set("origin", "https://www.1688.com"); + headers.set("referer", "https://www.1688.com/"); + headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("data", jsonData); + HttpEntity> requestEntity = new HttpEntity<>(formData, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + Alibaba1688CookieUtil.setCookie(response); + JSONObject json = JSON.parseObject(response.getBody()); + return "https://s.1688.com/youyuan/index.htm?tab=imageSearch&imageId=" + json.getJSONObject("data").getString("imageId"); + } + + + private String getWeight(List ids) { + Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:weight|unitWeight)\":\\s*([1-9]\\d*(?:\\.\\d+)?)"); + List weightList = new ArrayList<>(); + ChromeDriver driver = driverManager.getCurrentDriver(); + Set weightSet = new HashSet<>(); + try { + for (int i = 0; weightSet.size() <= 2; i++) { + driver.get("https://detail.1688.com/offer/" + ids.get(i) + ".html"); + if(i==0) driver.navigate().refresh(); + if (Objects.equals(driver.getTitle(), "验证码拦截")) { + driver = driverManager.switchToHeadful(); + while (Objects.equals(driver.getTitle(), "验证码拦截")) { + Thread.sleep(1000); + } + }else if(Objects.equals(driver.getTitle(), "淘宝网 - 淘!我喜欢")){ + + } + System.out.println("标题"+driver.getTitle()); + String source = driver.getPageSource(); + Matcher weightMatcher = WEIGHT_PATTERN.matcher(source); + if (weightMatcher.find()) { + String weightValue = weightMatcher.group(1); + String width = weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g"; + weightSet.add(width); + System.out.println("重量"+width); + } + Thread.sleep(2000+random.nextInt(3000)); + } + } catch (InterruptedException e) { + e.printStackTrace(); + errorReporter.reportDataCollectError("获取重量出错",e); + } + weightList.addAll(weightSet); + weightList.sort((a, b) -> Integer.compare(Integer.parseInt(a.replace("g", "")), Integer.parseInt(b.replace("g", "")))); + System.out.println("weightList: " +ids+"::::::"+ weightList); + return weightList.get(1); + } + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java new file mode 100644 index 0000000..83f86c9 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java @@ -0,0 +1,163 @@ +package com.tashow.erp.service.impl; +import com.tashow.erp.entity.AmazonProductEntity; +import com.tashow.erp.repository.AmazonProductRepository; +import com.tashow.erp.service.IAmazonScrapingService; +import com.tashow.erp.utils.DataReportUtil; +import com.tashow.erp.utils.RakutenProxyUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import us.codecraft.webmagic.Page; +import us.codecraft.webmagic.Site; +import us.codecraft.webmagic.Spider; +import us.codecraft.webmagic.processor.PageProcessor; +import us.codecraft.webmagic.selector.Html; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 亚马逊数据采集服务实现类 + * + * @author ruoyi + */ +@Service +public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PageProcessor { + private static final Logger logger = LoggerFactory.getLogger(AmazonScrapingServiceImpl.class); + @Autowired + private AmazonProductRepository amazonProductRepository; + @Autowired + private DataReportUtil dataReportUtil; + private final Random random = new Random(); + private static volatile Spider activeSpider = null; + private static final Object spiderLock = new Object(); + private final Map> resultCache = new ConcurrentHashMap<>(); + private final Site site = Site.me().setRetryTimes(3).setSleepTime(2000 + random.nextInt(2000)) + .setTimeOut(15000).setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/128.0.0.0 Safari/537.36").addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").addHeader("accept-language", "ja,en;q=0.9,zh-CN;q=0.8,zh;q=0.7").addHeader("cache-control", "max-age=0").addHeader("upgrade-insecure-requests", "1").addHeader("sec-ch-ua", "\"Chromium\";v=\"128\", \"Not=A?Brand\";v=\"24\"").addHeader("sec-ch-ua-mobile", "?0").addHeader("sec-ch-ua-platform", "\"Windows\"").addHeader("sec-fetch-site", "none").addHeader("sec-fetch-mode", "navigate").addHeader("sec-fetch-user", "?1").addHeader("sec-fetch-dest", "document").addCookie("i18n-prefs", "JPY").addCookie("session-id", "358-1261309-0483141").addCookie("session-id-time", "2082787201l").addCookie("i18n-prefs", "JPY").addCookie("lc-acbjp", "zh_CN").addCookie("ubid-acbjp", "357-8224002-9668932"); + /** + * 处理亚马逊页面数据提取 + */ + @Override + public void process(Page page) { + Html html = page.getHtml(); + Map resultMap = new HashMap<>(); + String priceSymbol = html.xpath("//span[@class='a-price-symbol']/text()").toString(); + String priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString(); + String price = priceSymbol + priceWhole; + if (price.isEmpty()) { + price = html.xpath("//span[@class='a-price-range']/text()").toString(); + } + + String seller = html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString(); + if (seller == null || seller.isEmpty()) { + seller = html.xpath("//span[@class='a-size-small offer-display-feature-text-message']/text()").toString(); + } + resultMap.put("seller", seller); + if (price != null || seller != null) { + resultMap.put("price", price); + } else { + throw new RuntimeException("Retry this page"); + } + + String asin = html.xpath("//input[@id='ASIN']/@value").toString(); + if (asin == null || asin.isEmpty()) { + String[] parts = page.getUrl().toString().split("/dp/"); + if (parts.length > 1) asin = parts[1].split("/")[0].split("\\?")[0]; + } + String title = html.xpath("//span[@id='productTitle']/text()").toString(); + if (title == null || title.isEmpty()) + title = html.xpath("//h1[@class='a-size-large a-spacing-none']/text()").toString(); + resultMap.put("asin", asin != null ? asin : ""); + resultMap.put("title", (title == null || title.isEmpty()) ? "未获取" : title.trim()); + + resultCache.put(asin, resultMap); + page.putField("resultMap", resultMap); + } + + /** + * 获取WebMagic站点配置 + */ + @Override + public Site getSite() { + return site; + } + + /** + * 批量获取产品信息 + */ + @Override + public Map batchGetProductInfo(List asinList, String batchId) { + String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID(); + List> products = new ArrayList<>(); + + for (String asin : asinList) { + if (asin == null || asin.trim().isEmpty()) continue; + String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", ""); + Map result = new HashMap<>(); + + amazonProductRepository.findByAsin(cleanAsin).ifPresentOrElse(entity -> { + if (entity.getCreatedAt().isAfter(LocalDateTime.now().minusHours(1))) { + result.put("asin", entity.getAsin()); + result.put("title", entity.getTitle()); + result.put("price", entity.getPrice()); + result.put("seller", entity.getSeller()); + result.put("imageUrl", entity.getImageUrl()); + result.put("productUrl", entity.getProductUrl()); + result.put("brand", entity.getBrand()); + result.put("category", entity.getCategory()); + result.put("rating", entity.getRating()); + result.put("reviewCount", entity.getReviewCount()); + result.put("availability", entity.getAvailability()); + products.add(result); + } + }, () -> { + // 数据库没有或过期 -> 爬取 + String url = "https://www.amazon.co.jp/dp/" + cleanAsin; + RakutenProxyUtil proxyUtil = new RakutenProxyUtil(); + synchronized (spiderLock) { + activeSpider = Spider.create(this) + .addUrl(url) + .setDownloader(proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(url))) + .thread(1); + activeSpider.run(); + activeSpider = null; + } + result.putAll(resultCache.getOrDefault(cleanAsin, Map.of("asin", cleanAsin, "price", "", "seller", "", "title", ""))); + + // 存库 + AmazonProductEntity entity = new AmazonProductEntity(); + entity.setAsin(cleanAsin); + entity.setTitle((String) result.get("title")); + entity.setPrice((String) result.get("price")); + entity.setSeller((String) result.get("seller")); + entity.setImageUrl((String) result.get("imageUrl")); + entity.setProductUrl((String) result.get("productUrl")); + entity.setBrand((String) result.get("brand")); + entity.setCategory((String) result.get("category")); + entity.setRating((String) result.get("rating")); + entity.setReviewCount((String) result.get("reviewCount")); + entity.setAvailability((String) result.get("availability")); + entity.setSessionId(sessionId); + entity.setCreatedAt(LocalDateTime.now()); + try { + amazonProductRepository.save(entity); + dataReportUtil.reportDataCollection("AMAZON", 1, "0"); + } catch (Exception e) { + logger.warn("保存商品数据失败: {}", cleanAsin); + } + products.add(result); + }); + } + + long failedCount = products.stream().filter(p -> p.get("price").toString().isEmpty()).count(); + return Map.of( + "products", products, + "total", products.size(), + "success", true, + "failedCount", failedCount + ); + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..efe9f59 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java @@ -0,0 +1,246 @@ +package com.tashow.erp.service.impl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.service.IAuthService; +import com.tashow.erp.utils.ApiForwarder; +import com.tashow.erp.utils.DeviceUtils; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import java.util.HashMap; +import java.util.Map; +import jakarta.annotation.PostConstruct; + +/** + * 认证服务实现类 - 简化版 + * 提供基础的登录、注册、token验证功能 + */ +@Service +public class AuthServiceImpl implements IAuthService { + + @Value("${api.server.base-url}") + private String serverApiUrl; + + @Value("${project.version:2.1.0}") + private String appVersion; + + private final ObjectMapper objectMapper = new ObjectMapper(); + @org.springframework.beans.factory.annotation.Autowired + private ApiForwarder apiForwarder; + + @org.springframework.beans.factory.annotation.Autowired + private com.tashow.erp.repository.CacheDataRepository cacheDataRepository; + + @Getter + private String clientId = DeviceUtils.generateDeviceId(); + private String accessToken; + private String refreshToken; + + /** + * 应用启动时从SQLite恢复token + */ + @PostConstruct + private void initTokenFromCache() { + try { + cacheDataRepository.findByCacheKey("token").ifPresent(entity -> + accessToken = entity.getCacheValue() + ); + } catch (Exception ignored) {} + } + + /** + * 保存token到SQLite + */ + private void saveTokenToCache(String token) { + try { + com.tashow.erp.entity.CacheDataEntity entity = cacheDataRepository.findByCacheKey("token") + .orElse(new com.tashow.erp.entity.CacheDataEntity()); + entity.setCacheKey("token"); + entity.setCacheValue(token); + cacheDataRepository.save(entity); + } catch (Exception ignored) {} + } + + /** + * 客户端登录认证 + */ + @Override + public Map login(String username, String password) { + Map result = new HashMap<>(); + Map loginData = new HashMap<>(); + loginData.put("username", username); + loginData.put("password", password); + loginData.putAll(getClientInfo()); + + JsonNode response = sendPostRequest("/monitor/account/login", loginData); + if (response.has("code") && response.get("code").asInt() == 200) { + JsonNode data = response.has("data") ? response.get("data") : response; + accessToken = data.has("accessToken") ? data.get("accessToken").asText() : + data.has("token") ? data.get("token").asText() : null; + refreshToken = data.has("refreshToken") ? data.get("refreshToken").asText() : null; + + if (accessToken != null) { + saveTokenToCache(accessToken); + } + + result.put("success", true); + result.put("message", "登录成功"); + result.put("token", accessToken); + result.put("permissions", data.has("permissions") ? data.get("permissions").asText() : null); + } else { + result.put("success", false); + result.put("message", response.has("msg") ? response.get("msg").asText() : "登录失败"); + } + return result; + } + + /** + * 获取客户端基本信息 + */ + @Override + public Map getClientInfo() { + Map info = new HashMap<>(); + info.put("clientId", clientId); + info.put("appVersion", appVersion); + info.put("osName", System.getProperty("os.name")); + return info; + } + + /** + * 上报错误信息 + */ + @Override + public void reportError(String errorType, String errorMessage, Exception e) { + try { + Map errorData = new HashMap<>(); + errorData.put("clientId", clientId); + errorData.put("errorType", errorType); + errorData.put("errorMessage", errorMessage); + sendPostRequest("/monitor/error", errorData); + } catch (Exception ignored) {} + } + + /** + * 检查版本更新 + */ + @Override + public String checkVersion(String currentVersion) { + JsonNode response = sendGetRequest("/monitor/version?currentVersion=" + currentVersion); + return response.toString(); + } + + + /** + * 发送POST请求 + */ + private JsonNode sendPostRequest(String path, Map data){ + if (data != null) { + data.putIfAbsent("_clientUser", System.getProperty("user.name")); + data.putIfAbsent("_clientOs", System.getProperty("os.name")); + } + org.springframework.http.ResponseEntity resp = apiForwarder.post(path, data, buildAuthHeader()); + return objectMapper.valueToTree(resp.getBody()); + } + + /** + * 发送GET请求 + */ + private JsonNode sendGetRequest(String path){ + org.springframework.http.ResponseEntity resp = apiForwarder.get(path, buildAuthHeader()); + return objectMapper.valueToTree(resp.getBody()); + } + + /** + * 构建认证头 + */ + private String buildAuthHeader(){ + return (accessToken == null || accessToken.isEmpty()) ? null : ("Bearer " + accessToken.trim()); + } + + /** + * 验证token + */ + @Override + public Map verifyToken(String token) { + Map result = new HashMap<>(); + Map verifyData = new HashMap<>(); + verifyData.put("token", token); + JsonNode response = sendPostRequest("/monitor/account/verify", verifyData); + + if (response.has("code") && response.get("code").asInt() == 200) { + JsonNode dataNode = response.has("data") ? response.get("data") : response; + result.put("success", true); + result.put("username", dataNode.get("username").asText()); + result.put("permissions", dataNode.get("permissions").asText()); + } else { + result.put("success", false); + result.put("message", "token验证失败"); + } + return result; + } + + /** + * 注册新账号 + */ + @Override + public Map register(String username, String password) { + Map result = new HashMap<>(); + try { + Map registerData = new HashMap<>(); + registerData.put("accountName", username); + registerData.put("username", username); + registerData.put("password", password); + registerData.putAll(getClientInfo()); + JsonNode response = sendPostRequest("/monitor/account/register", registerData); + if (response.has("code") && response.get("code").asInt() == 200) { + JsonNode data = response.has("data") ? response.get("data") : response; + String newAccessToken = data.has("accessToken") ? data.get("accessToken").asText() : null; + String newRefreshToken = data.has("refreshToken") ? data.get("refreshToken").asText() : null; + + if (newAccessToken != null) { + accessToken = newAccessToken; + refreshToken = newRefreshToken; + saveTokenToCache(newAccessToken); + } + + result.put("success", true); + result.put("message", "注册成功"); + } else { + String errorMessage = response.has("msg") ? response.get("msg").asText() : + (response.has("message") ? response.get("message").asText() : "注册失败"); + result.put("success", false); + result.put("message", errorMessage); + } + } catch (Exception e) { + result.put("success", false); + result.put("message", "注册失败:" + e.getMessage()); + } + return result; + } + + /** + * 检查用户名是否可用 + */ + @Override + public boolean checkUsername(String username) { + try { + JsonNode response = sendGetRequest("/monitor/account/check-username?username=" + username); + if (response.has("code") && response.get("code").asInt() == 200) { + JsonNode dataNode = response.has("data") ? response.get("data") : response; + return dataNode.asBoolean(); + } + } catch (Exception ignored) {} + return false; + } + + /** + * 退出登录,清除本地token + */ + public void logout() { + try { + accessToken = null; + refreshToken = null; + cacheDataRepository.deleteByCacheKey("token"); + } catch (Exception ignored) {} + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java new file mode 100644 index 0000000..48c6608 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java @@ -0,0 +1,292 @@ +package com.tashow.erp.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.entity.BanmaOrderEntity; +import com.tashow.erp.repository.BanmaOrderRepository; +import com.tashow.erp.service.ICacheService; +import com.tashow.erp.service.IBanmaOrderService; +import com.tashow.erp.utils.DataReportUtil; +import com.tashow.erp.utils.LoggerUtil; +import com.tashow.erp.utils.SagawaExpressSdk; +import com.tashow.erp.utils.StringUtils; +import org.slf4j.Logger; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +/** + * 斑马订单服务实现类 - 极简版 + * 所有功能统一到核心方法,彻底消除代码分散 + * + * @author ruoyi + */ +@Service +public class BanmaOrderServiceImpl implements IBanmaOrderService { + private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class); + private static final String SERVICE_NAME = "banma"; + private static final String LOGIN_URL = "https://banma365.cn/api/login"; + private static final String LOGIN_USERNAME = "大赢家网络科技(主账号)"; + private static final String LOGIN_PASSWORD = "banma123456"; + private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d"; + private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d"; + private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s"; + private static final long TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000; + private RestTemplate restTemplate; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ICacheService cacheService; + private final BanmaOrderRepository banmaOrderRepository; + private final DataReportUtil dataReportUtil; + private String currentAuthToken; + // 当前批量采集的sessionId + private String currentBatchSessionId = null; + // 物流信息缓存,避免重复查询 + private final Map trackingInfoCache = new ConcurrentHashMap<>(); + public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, ICacheService cacheService, DataReportUtil dataReportUtil) { + this.banmaOrderRepository = banmaOrderRepository; + this.cacheService = cacheService; + this.dataReportUtil = dataReportUtil; + RestTemplateBuilder builder = new RestTemplateBuilder(); + builder.connectTimeout(Duration.ofSeconds(5)); + builder.readTimeout(Duration.ofSeconds(10)); + restTemplate = builder.build(); + initializeAuthToken(); + } + /** + * 初始化认证令牌 + */ + private void initializeAuthToken() { + refreshToken(); + /* currentAuthToken = cacheService.getAuthToken(SERVICE_NAME); + if (currentAuthToken == null) { + + } else { + logger.info("从缓存加载斑马认证令牌成功"); + }*/ + } + + /** + * 刷新斑马认证令牌 + */ + @Override + public void refreshToken() { + Map params = new HashMap<>(); + params.put("username", LOGIN_USERNAME); + params.put("password", LOGIN_PASSWORD); + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + + ResponseEntity response = restTemplate.postForEntity(LOGIN_URL, new HttpEntity<>(params, headers), Map.class); + Optional.ofNullable(response.getBody()) + .filter(body -> Integer.valueOf(0).equals(body.get("code"))) + .map(body -> (Map) body.get("data")) + .map(data -> (String) data.get("token")) + .filter(StringUtils::isNotEmpty) + .ifPresent(token -> { + currentAuthToken = "Bearer " + token; + cacheService.saveAuthToken(SERVICE_NAME, currentAuthToken, System.currentTimeMillis() + TOKEN_EXPIRE_TIME); + }); + } + + /** + * 获取店铺列表 + */ + @Override + public Map getShops() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", currentAuthToken); + HttpEntity httpEntity = new HttpEntity<>(headers); + + String url = "https://banma365.cn/api/shop/list?_t=" + System.currentTimeMillis(); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); + + return response.getBody() != null ? response.getBody() : new HashMap<>(); + } + + /** + * 分页获取订单数据 + */ + @Override + public Map getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List shopIds) { + if (page == 1) { + currentBatchSessionId = batchId; + trackingInfoCache.clear(); + } + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", currentAuthToken); + HttpEntity httpEntity = new HttpEntity<>(headers); + + String shopIdsParam = ""; + if (shopIds != null && !shopIds.isEmpty()) { + List validShopIds = shopIds.stream() + .filter(id -> id != null && !id.trim().isEmpty()) + .collect(Collectors.toList()); + if (!validShopIds.isEmpty()) { + shopIdsParam = "shopIds[]=" + String.join("&shopIds[]=", validShopIds) + "&"; + } + } + + String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate)) + ? String.format(API_URL, shopIdsParam, page, pageSize, System.currentTimeMillis()) + : String.format(API_URL_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis()); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); + if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) { + Map errorResult = new HashMap<>(); + errorResult.put("success", false); + errorResult.put("message", "获取订单数据失败,请点击'刷新认证'按钮重试"); + return errorResult; + } + + Map dataMap = (Map) response.getBody().get("data"); + int total = ((Number) dataMap.getOrDefault("total", 0)).intValue(); + + List orders = Optional.ofNullable(dataMap.get("list")) + .map(list -> (List>) list) + .orElse(Collections.emptyList()) + .stream() + .map(this::processOrderData) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!orders.isEmpty()) dataReportUtil.reportDataCollection("BANMA", orders.size(), "0"); + + Map result = new HashMap<>(); + result.put("orders", orders); + result.put("total", total); + result.put("page", page); + result.put("pageSize", pageSize); + result.put("totalPages", (int) Math.ceil((double) total / pageSize)); + result.put("success", true); + return result; + } + + + /** + * 处理订单数据 + */ + private Map processOrderData(Map order) { + String trackingNumber = (String) order.get("internationalTrackingNumber"); + if (StringUtils.isNotEmpty(trackingNumber)) { + LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1); + if (banmaOrderRepository.existsByTrackingNumberAndCreatedAtAfter(trackingNumber, cutoffTime)) { + return banmaOrderRepository.findLatestByTrackingNumber(trackingNumber) + .map(entity -> { + if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) { + entity.setSessionId(currentBatchSessionId); + entity.setCreatedAt(LocalDateTime.now()); + entity.setUpdatedAt(LocalDateTime.now()); + banmaOrderRepository.save(entity); + } + try { + return objectMapper.readValue(entity.getOrderData(), Map.class); + } catch (Exception e) { + return new HashMap<>(); + } + }) + .orElse(null); + } else { + banmaOrderRepository.findByTrackingNumber(trackingNumber) + .ifPresent(banmaOrderRepository::delete); + } + } + + // 构建新订单数据 + Map result = new HashMap<>(); + result.put("internationalTrackingNumber", trackingNumber); + result.put("internationalShippingFee", order.get("internationalShippingFee")); + result.put("trackInfo", trackingInfoCache.computeIfAbsent(trackingNumber, this::fetchTrackingInfo)); + + Optional.ofNullable(order.get("subOrders")) + .map(sub -> (List>) sub) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .ifPresent(subOrder -> extractSubOrderFields(result, subOrder)); + + BanmaOrderEntity entity = new BanmaOrderEntity(); + String entityTrackingNumber = (String) result.get("internationalTrackingNumber"); + + if (StringUtils.isEmpty(entityTrackingNumber)) { + String shopOrderNumber = (String) result.get("shopOrderNumber"); + String productTitle = (String) result.get("productTitle"); + if (StringUtils.isNotEmpty(shopOrderNumber)) { + entityTrackingNumber = "ORDER_" + shopOrderNumber; + } else if (StringUtils.isNotEmpty(productTitle)) { + entityTrackingNumber = "PRODUCT_" + Math.abs(productTitle.hashCode()); + } else { + entityTrackingNumber = "UNKNOWN_" + System.currentTimeMillis(); + } + } + + entity.setTrackingNumber(entityTrackingNumber); + try { + entity.setOrderData(objectMapper.writeValueAsString(result)); + } catch (Exception e) { + entity.setOrderData("{}"); + } + + // 生成会话ID + String sessionId = currentBatchSessionId != null ? currentBatchSessionId : + Optional.ofNullable((String) result.get("orderedAt")) + .filter(orderedAt -> orderedAt.length() >= 10) + .map(orderedAt -> "SESSION_" + orderedAt.substring(0, 10)) + .orElse("SESSION_" + java.time.LocalDate.now().toString()); + + entity.setSessionId(sessionId); + entity.setCreatedAt(LocalDateTime.now()); + entity.setUpdatedAt(LocalDateTime.now()); + + try { + banmaOrderRepository.save(entity); + } catch (Exception e) { + logger.warn("保存订单数据失败,跳过: {}", entityTrackingNumber); + } + + return result; + } + + /** + * 提取子订单字段 + */ + private void extractSubOrderFields(Map simplifiedOrder, Map subOrder) { + String[] basicFields = {"orderedAt", "timeSinceOrder", "createdAt", "poTrackingNumber"}; + String[] productFields = {"productTitle", "shopOrderNumber", "priceJpy", "productQuantity", "shippingFeeJpy", "productNumber", "serviceFee", "productImage"}; + String[] purchaseFields = {"poNumber", "shippingFeeCny", "poLogisticsCompany"}; + Arrays.stream(basicFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field))); + Arrays.stream(productFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field))); + Arrays.stream(purchaseFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field))); + } + + + + + /** + * 从API获取物流信息 + */ + private String fetchTrackingInfo(String trackingNumber) { + // 优先尝试佐川物流 + Map trackInfoMap = (Map) new SagawaExpressSdk().getTrackingInfo(trackingNumber).get("trackInfo"); + if (trackInfoMap != null) { + return trackInfoMap.get("dateTime") + " " + trackInfoMap.get("office") + " " + trackInfoMap.get("status"); + } + + // 斑马API + ResponseEntity response = restTemplate.getForEntity(String.format(TRACKING_URL, trackingNumber), Map.class); + return Optional.ofNullable(response.getBody()) + .filter(body -> Integer.valueOf(0).equals(body.get("code"))) + .map(body -> (List>) body.get("data")) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .map(track -> (String) track.get("track")) + .orElse("暂无物流信息"); + } + + + + + +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/CacheServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/CacheServiceImpl.java new file mode 100644 index 0000000..5e176fe --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/CacheServiceImpl.java @@ -0,0 +1,34 @@ +package com.tashow.erp.service.impl; + +import com.tashow.erp.entity.AuthTokenEntity; +import com.tashow.erp.repository.AuthTokenRepository; +import com.tashow.erp.service.ICacheService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +public class CacheServiceImpl implements ICacheService { + private static final Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class); + + @Autowired + private AuthTokenRepository authTokenRepository; + + @Override + public void saveAuthToken(String service, String token, long expireTimeMillis) { + try { + Optional existing = authTokenRepository.findByServiceName(service); + AuthTokenEntity entity = existing.orElse(new AuthTokenEntity()); + entity.setServiceName(service); + entity.setToken(token); + entity.setExpireTime(LocalDateTime.now().plusSeconds(expireTimeMillis / 1000)); + authTokenRepository.save(entity); + } catch (Exception e) { + logger.error("保存认证令牌失败: {}", service, e); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java new file mode 100644 index 0000000..04389bd --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java @@ -0,0 +1,95 @@ +package com.tashow.erp.service.impl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qiniu.util.UrlUtils; +import io.github.bonigarcia.wdm.WebDriverManager; +import lombok.SneakyThrows; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import com.tashow.erp.service.IGenmaiService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import java.util.HashMap; +import java.util.Map; +/** + * 跟卖精灵服务实现类 + * + * @author ruoyi + */ +@Service +public class GenmaiServiceImpl implements IGenmaiService { + @Value("${api.server.base-url}") + private String serverApiUrl; + @Value("${api.server.paths.getGenmaijlToken}") + private String genmaijlToken; + @Value("${api.server.paths.updateGenmaijlToken}") + private String updateGenmaijlToken; + private final RestTemplate restTemplate = new RestTemplate(); + ObjectMapper objectMapper = new ObjectMapper(); + @Override + @SneakyThrows + public void openGenmaiWebsite() { + // 先关闭所有Chrome进程 + Runtime.getRuntime().exec("taskkill /f /im chrome.exe"); + WebDriverManager.chromedriver().setup(); + ChromeOptions options = new ChromeOptions(); + String username = System.getProperty("user.name", "user"); + String safeUsername; + char firstChar = username.charAt(0); + char flippedFirstChar = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar); + safeUsername = flippedFirstChar + username.substring(1); + String chromeUserData = System.getProperty("user.home").replace(username, UrlUtils.urlEncode(safeUsername) ) + + "\\AppData\\Local\\Google\\Chrome\\User Data"; + options.addArguments("user-data-dir=" + chromeUserData.toLowerCase()); + options.addArguments("profile-directory=Default"); + WebDriver driver = new ChromeDriver(options); + driver.get("https://www.genmaijl.com/#/profile"); + JavascriptExecutor js = (JavascriptExecutor) driver; + js.executeScript(String.format("localStorage.setItem('token','%s')", checkTokenExpired())); + driver.navigate().refresh(); + } + /** + * 获取token验证token是否过期 + */ + public String checkTokenExpired() { + ResponseEntity exchange = restTemplate.exchange(serverApiUrl+genmaijlToken, HttpMethod.GET, null, String.class); + String token = exchange.getBody(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Token", token); + HttpEntity entity = new HttpEntity<>(null, headers); + try { + ResponseEntity response = restTemplate.exchange("https://www.genmaijl.com/manage/user/balance", HttpMethod.GET, entity, String.class); + String body = response.getBody(); + JsonNode root = objectMapper.readTree(body); + return root.get("code").asInt() == 0 ? token : login(); + } catch (Exception e) { + return login(); + } + } + /** + * 登录 + */ + @SneakyThrows + public String login() { + Map requestBody = new HashMap<>(); + requestBody.put("nickname", "changzhu-4"); + requestBody.put("password", "123456QWe@"); + HttpEntity> entity = new HttpEntity<>(requestBody); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + ResponseEntity response = restTemplate.exchange("https://www.genmaijl.com/manage/user/subLogin", HttpMethod.POST, entity, String.class); + String body = response.getBody(); + JsonNode root = objectMapper.readTree(body); + requestBody.clear(); + String token= root.get("result").asText(); + HttpEntity updateEntity = new HttpEntity<>(token,headers); + ResponseEntity exchange = restTemplate.exchange(serverApiUrl + updateGenmaijlToken, HttpMethod.POST, updateEntity, String.class); + System.out.println(exchange.getBody()); + return token; + } + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java new file mode 100644 index 0000000..43c5960 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java @@ -0,0 +1,153 @@ +package com.tashow.erp.service.impl; + +import com.alibaba.fastjson.JSON; +import com.tashow.erp.entity.RakutenProductEntity; +import com.tashow.erp.model.RakutenProduct; +import com.tashow.erp.repository.RakutenProductRepository; +import com.tashow.erp.service.IRakutenCacheService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 乐天产品缓存服务实现类 + * 负责乐天商品数据的缓存存储、检索和会话管理 + */ +@Slf4j +@Service +public class RakutenCacheServiceImpl implements IRakutenCacheService { + + @Autowired + private RakutenProductRepository repository; + + /** + * 保存产品列表并生成新的会话ID + */ + @Override + @Transactional + public String saveProducts(List products) { + String sessionId = UUID.randomUUID().toString(); + List entities = products.stream() + .map(product -> { + RakutenProductEntity entity = new RakutenProductEntity(); + BeanUtils.copyProperties(product, entity); + entity.setSessionId(sessionId); + return entity; + }) + .collect(Collectors.toList()); + repository.saveAll(entities); + log.info("保存产品数据,sessionId: {},数量: {}", sessionId, products.size()); + return sessionId; + } + + /** + * 使用指定会话ID保存产品列表 + */ + @Override + @Transactional + public void saveProductsWithSessionId(List products, String sessionId) { + List entities = products.stream() + .map(product -> { + RakutenProductEntity entity = new RakutenProductEntity(); + BeanUtils.copyProperties(product, entity); + entity.setSessionId(sessionId); + return entity; + }) + .collect(Collectors.toList()); + + repository.saveAll(entities); + log.info("保存产品数据,sessionId: {},数量: {}", sessionId, products.size()); + } + + /** + * 检查店铺是否有1小时内的缓存数据 + */ + @Override + public boolean hasRecentData(String shopName) { + return repository.existsByOriginalShopNameAndCreatedAtAfter(shopName, LocalDateTime.now().minusHours(1)); + } + + /** + * 根据店铺名获取所有产品数据 + */ + @Override + public List getProductsByShopName(String shopName) { + return repository.findByOriginalShopNameOrderByCreatedAtAscIdAsc(shopName).stream() + .map(entity -> { + RakutenProduct product = new RakutenProduct(); + BeanUtils.copyProperties(entity, product); + return product; + }) + .collect(Collectors.toList()); + } + + /** + * 更新指定店铺的所有产品的会话ID + */ + @Override + @Transactional + public void updateProductsSessionId(String shopName, String newSessionId) { + List entities = repository.findByOriginalShopNameOrderByCreatedAtAscIdAsc(shopName); + entities.forEach(entity -> { + if (!newSessionId.equals(entity.getSessionId())) { + entity.setSessionId(newSessionId); + entity.setUpdatedAt(LocalDateTime.now()); + } + }); + repository.saveAll(entities); + log.info("更新店铺产品sessionId: {} -> {}", shopName, newSessionId); + } + + /** + * 更新指定产品列表的会话ID,只更新这些具体的产品 + */ + @Override + @Transactional + public void updateSpecificProductsSessionId(List products, String newSessionId) { + if (products == null || products.isEmpty()) { + return; + } + + // 根据产品的唯一标识(如productUrl)来查找并更新对应的数据库记录 + List productUrls = products.stream() + .map(RakutenProduct::getProductUrl) + .filter(url -> url != null && !url.trim().isEmpty()) + .collect(Collectors.toList()); + + if (productUrls.isEmpty()) { + return; + } + + List entities = repository.findByProductUrlIn(productUrls); + entities.forEach(entity -> { + if (!newSessionId.equals(entity.getSessionId())) { + entity.setSessionId(newSessionId); + entity.setUpdatedAt(LocalDateTime.now()); + } + }); + + repository.saveAll(entities); + log.info("更新特定产品sessionId,数量: {} -> {}", entities.size(), newSessionId); + } + + /** + * 清理指定店铺1小时之前的旧数据,保留1小时内的缓存 + */ + @Override + @Transactional + public void cleanRecentDuplicateData(String shopName) { + LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1); + repository.deleteByOriginalShopNameAndCreatedAtBefore(shopName, cutoffTime); + log.info("清理店铺1小时之前的旧数据: {}", shopName); + } + + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java new file mode 100644 index 0000000..739646e --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java @@ -0,0 +1,158 @@ +package com.tashow.erp.service.impl; + +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.entity.RakutenProductEntity; +import com.tashow.erp.model.RakutenProduct; +import com.tashow.erp.model.SearchResult; +import com.tashow.erp.repository.RakutenProductRepository; +import com.tashow.erp.service.RakutenScrapingService; +import com.tashow.erp.utils.RakutenProxyUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import us.codecraft.webmagic.Page; +import us.codecraft.webmagic.Site; +import us.codecraft.webmagic.Spider; +import us.codecraft.webmagic.processor.PageProcessor; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 乐天商品爬取服务实现类 + * 负责从乐天网站采集商品数据,包括商品搜索、1688链接更新和数据展示 + */ +@Slf4j +@Service +public class RakutenScrapingServiceImpl implements RakutenScrapingService { + + @Autowired + private RakutenProductRepository rakutenProductRepository; + @Autowired + ObjectMapper objectMapper; + + /** + * 根据店铺名称搜索并采集乐天商品数据 + */ + @Override + public List scrapeProductsWithSearch(String shopName) { + String url = "https://ranking.rakuten.co.jp/search?stx=" + URLEncoder.encode(shopName, StandardCharsets.UTF_8); + List products = new ArrayList<>(); + + Spider spider = Spider.create(new RakutenPageProcessor(products)).addUrl(url).setDownloader(new RakutenProxyUtil().createProxyDownloader(new RakutenProxyUtil().detectSystemProxy(url))).thread(1); + spider.run(); + + log.info("采集完成,店铺: {},数量: {}", shopName, products.size()); + return products; + } + + private class RakutenPageProcessor implements PageProcessor { + private final List products; + + RakutenPageProcessor(List products) { + this.products = products; + } + + @Override + public void process(Page page) { + List rankings = page.getHtml().xpath("//div[@class='srhRnk']/span[@class='icon']/text()").all(); + List productUrls = page.getHtml().xpath("//div[@class='srhPic']//div[@class='rnkRanking_bigImageBox']/a/@href").all(); + List imageUrls = page.getHtml().xpath("//div[@class='srhPic']//div[@class='rnkRanking_bigImageBox']/a/img/@src").all(); + List titles = page.getHtml().xpath("//div[@class='srhItm']/a/text()").all(); + List prices = page.getHtml().xpath("//div[@class='srhPri']/text()").all(); + + int count = Stream.of(rankings, productUrls, imageUrls, titles, prices).mapToInt(List::size).min().orElse(0); + + if (count == 0) return; + + for (int i = 0; i < count; i++) { + RakutenProduct product = new RakutenProduct(); + String productUrl = productUrls.get(i); + product.setProductUrl(productUrl); + // 提取商店名称 + String[] parts = productUrl.split("/"); + String shopName = parts.length > 3 ? parts[3] : ""; + product.setShopName(shopName); + product.setOriginalShopName(shopName); // 设置原始店铺名称 + product.setImgUrl(imageUrls.get(i)); + String title = titles.get(i).trim(); + product.setProductTitle(title); + product.setProductName(title); + product.setPrice(prices.get(i).replaceAll("[^0-9]", "")); + product.setRanking(rankings.get(i).trim()); + // 设置默认的1688相关字段 + product.setMapRecognitionLink(""); + product.setImage1688Url(""); + product.setDetailUrl1688(""); + product.setFreight(null); + product.setMedian(null); + product.setWeight(""); + products.add(product); + } + } + + @Override + public Site getSite() { + Site site = Site.me().setRetryTimes(3).setCycleRetryTimes(3).setTimeOut(15000).setSleepTime(2000 + new Random().nextInt(3000)); + site.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + site.addHeader("Accept", "text/html"); + site.addHeader("Cache-Control", "no-cache"); + return site; + } + } + + /** + * 根据图片URL更新1688搜索结果的所有字段 + */ + @Override + @Transactional + public void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl) { + List matchingProducts = rakutenProductRepository.findBySessionIdOrderByCreatedAtAscIdAsc(sessionId).stream().filter(product -> imageUrl.equals(product.getImgUrl())).peek(product -> { + product.setMapRecognitionLink(searchResult.getMapRecognitionLink()); + product.setFreight(searchResult.getFreight()); + product.setSkuPriceJson(searchResult.getSkuPrice().toString()); + product.setMedian(searchResult.getMedian()); + product.setWeight(searchResult.getWeight()); + product.setSkuPriceJson(JSON.toJSONString(searchResult.getSkuPrice())); + + + }).collect(Collectors.toList()); + if (!matchingProducts.isEmpty()) { + rakutenProductRepository.saveAll(matchingProducts); + } + } + + /** + * 获取最新的产品数据用于前端展示 + */ + @Override + public List> getLatestProductsForDisplay() { + return rakutenProductRepository.findLatestProducts().stream().map(entity -> { + Map result = new HashMap<>(); + result.put("originalShopName", Optional.ofNullable(entity.getOriginalShopName()).orElse("")); + result.put("productUrl", Optional.ofNullable(entity.getProductUrl()).orElse("")); + result.put("imgUrl", Optional.ofNullable(entity.getImgUrl()).orElse("")); + result.put("productTitle", Optional.ofNullable(entity.getProductTitle()).orElse("")); + result.put("price", Optional.ofNullable(entity.getPrice()).orElse("")); + result.put("ranking", Optional.ofNullable(entity.getRanking()).orElse("")); + result.put("image1688Url", Optional.ofNullable(entity.getImage1688Url()).orElse("")); + result.put("detailUrl1688", Optional.ofNullable(entity.getDetailUrl1688()).orElse("")); + // 使用正确的实体字段 + result.put("mapRecognitionLink", Optional.ofNullable(entity.getMapRecognitionLink()).orElse("")); + result.put("freight", entity.getFreight()); + result.put("median", entity.getMedian()); + result.put("weight", Optional.ofNullable(entity.getWeight()).orElse("")); + result.put("skuPrice",entity.getSkuPriceJson()); + return result; + }).collect(Collectors.toList()); + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java b/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java new file mode 100644 index 0000000..33c45cb --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/test/SeleniumWithProfile.java @@ -0,0 +1,58 @@ +package com.tashow.erp.test; + +import com.tashow.erp.utils.StealthSelenium; +import org.openqa.selenium.chrome.ChromeDriver; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SeleniumWithProfile { + private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)"); + + public static void main(String[] args) { + String uuid = ""; + try { + Process process = Runtime.getRuntime().exec( + new String[]{"wmic", "csproduct", "get", "UUID"}); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) { + uuid = line; + break; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("电脑序列号: " + uuid); + +// ChromeDriver driver = StealthSelenium.createDriver(); +// try { +// +// // 访问目标网站 +// System.out.println("正在访问目标网站..."); +// driver.get("https://detail.1688.com/offer/600366775654.html"); +// // driver.navigate().refresh(); +// String source = driver.getPageSource(); +// String weight = ""; +// Matcher weightMatcher = WEIGHT_PATTERN.matcher(source); +// if (weightMatcher.find()) { +// String weightValue = weightMatcher.group(1); +// weight = " Weight: " + (weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g"); +// } +// System.out.println(driver.getTitle()); +// } catch (Exception e) { +// System.err.println("运行出错: " + e.getMessage()); +// e.printStackTrace(); +// }finally { +// driver.quit(); +// +// } + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/Alibaba1688CookieUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/Alibaba1688CookieUtil.java new file mode 100644 index 0000000..b84be1f --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/Alibaba1688CookieUtil.java @@ -0,0 +1,182 @@ +package com.tashow.erp.utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 阿里巴巴1688 API Cookie工具类 + */ +public class Alibaba1688CookieUtil { + private static final Logger log = LoggerFactory.getLogger(Alibaba1688CookieUtil.class); + private static Map cookieCache = new HashMap<>(); + private static final long COOKIE_EXPIRE_TIME = 10 * 60 * 1000; + private static long lastUpdateTime = 0; + public static final String APP_KEY = "12574478"; + private static final String M_H5_TK = "_m_h5_tk"; + private static final String M_H5_TK_ENC = "_m_h5_tk_enc"; + static RestTemplate restTemplate=new RestTemplateBuilder().build(); + /** + * 获取1688 API的Token (从_m_h5_tk cookie中提取) + * + * @return Token字符串,失败返回空字符串 + */ + public static String getToken() { + String tokenCookie = getCookie(M_H5_TK); + if (tokenCookie != null && !tokenCookie.isEmpty()) { + return tokenCookie.split("_")[0]; + } + return ""; + } + /** + * 获取指定名称的Cookie值 + * + * @param cookieName Cookie名称 + * @return Cookie值,未找到返回null + */ + public static String getCookie(String cookieName) { + if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) { + refreshCookies(); + } + return cookieCache.get(cookieName); + } + + /** + * 获取完整的Cookie字符串,用于HTTP请求头 + * @return 完整的Cookie字符串 + */ + public static String getCookieString() { + if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) { + refreshCookies(); + } + StringBuilder cookieBuilder = new StringBuilder(); + for (Map.Entry entry : cookieCache.entrySet()) { + if (cookieBuilder.length() > 0) { + cookieBuilder.append("; "); + } + cookieBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + } + + return cookieBuilder.toString(); + } + /** + * 刷新Cookie + */ + public static synchronized void refreshCookies() { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"); + headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"); + headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange("https://www.1688.com/", HttpMethod.GET, entity, String.class); + List cookies = response.getHeaders().get("Set-Cookie"); + if (cookies != null) { + for (String cookie : cookies) { + parseCookie(cookie); + } + } + headers = new HttpHeaders(); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"); + headers.set("Accept", "application/json"); + headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + headers.set("Referer", "https://www.1688.com/"); + + if (!cookieCache.isEmpty()) { + StringBuilder cookieBuilder = new StringBuilder(); + for (Map.Entry entry : cookieCache.entrySet()) { + if (cookieBuilder.length() > 0) { + cookieBuilder.append("; "); + } + cookieBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + } + headers.set("Cookie", cookieBuilder.toString()); + } + + entity = new HttpEntity<>(headers); + + response = restTemplate.exchange( + "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/?jsv=2.7.4&appKey=12574478&api=mtop.relationrecommend.wirelessrecommend.recommend&v=2.0&type=originaljson&dataType=json", + HttpMethod.GET, entity, String.class); + cookies = response.getHeaders().get("Set-Cookie"); + if (cookies != null) { + for (String cookie : cookies) { + parseCookie(cookie); + } + } + log.info("获取到的1688 API token: {}", cookieCache.getOrDefault(M_H5_TK, "未获取到")); + log.info("获取到的1688 API token_enc: {}", cookieCache.getOrDefault(M_H5_TK_ENC, "未获取到")); + + lastUpdateTime = System.currentTimeMillis(); + } catch (Exception e) { + log.error("刷新1688 API Cookie失败", e); + } + } + + /** + * 解析Cookie字符串并更新缓存 + * + * @param cookieStr Cookie字符串 + */ + private static void parseCookie(String cookieStr) { + if (cookieStr == null || cookieStr.isEmpty()) { + return; + } + String[] parts = cookieStr.split(";")[0].split("=", 2); + if (parts.length == 2) { + String name = parts[0].trim(); + String value = parts[1].trim(); + cookieCache.put(name, value); + } + } + /** + * 手动设置Cookie + * + */ + public static void setCookie(ResponseEntity response) { + + List newCookies = response.getHeaders().get("Set-Cookie"); + if (newCookies != null && !newCookies.isEmpty()) { + for (String cookie : newCookies) { + String[] parts = cookie.split(";")[0].split("=", 2); + if (parts.length == 2) { + cookieCache.put(parts[0], parts[1]); + } + } + } + lastUpdateTime = System.currentTimeMillis(); + } + + + /** + * 清除所有Cookie缓存 + */ + public static void clearCookies() { + cookieCache.clear(); + lastUpdateTime = 0; + } + + public static String generateSign(String token, String timestamp, String data) { + try { + String signStr = token + "&" + timestamp + "&" + APP_KEY + "&" + data; + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(signStr.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + return ""; + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ApiForwarder.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ApiForwarder.java new file mode 100644 index 0000000..0fca77d --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ApiForwarder.java @@ -0,0 +1,58 @@ +package com.tashow.erp.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Component +public class ApiForwarder { + + private final RestTemplate restTemplate; + + @Value("${api.server.base-url}") + private String serverBaseUrl; + + public ApiForwarder(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public ResponseEntity post(String path, Map body, String auth) { + String url = serverBaseUrl + path; + HttpHeaders headers = buildHeaders(auth); + headers.set("X-Client-User", System.getProperty("user.name")); + headers.set("X-Client-OS", System.getProperty("os.name")); + HttpEntity> entity = new HttpEntity<>(body, headers); + return restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + } + + public ResponseEntity get(String pathWithQuery, String auth) { + String url = serverBaseUrl + pathWithQuery; + HttpHeaders headers = buildHeaders(auth); + headers.set("X-Client-User", System.getProperty("user.name")); + headers.set("X-Client-OS", System.getProperty("os.name")); + HttpEntity entity = new HttpEntity<>(headers); + return restTemplate.exchange(url, HttpMethod.GET, entity, Map.class); + } + + public ResponseEntity sse(String pathWithQuery) { + String url = serverBaseUrl + pathWithQuery; + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(java.util.Collections.singletonList(MediaType.TEXT_EVENT_STREAM)); + HttpEntity entity = new HttpEntity<>(headers); + return restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + } + + private HttpHeaders buildHeaders(String auth) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + if (auth != null && !auth.isEmpty()) { + headers.set("Authorization", auth); + } + return headers; + } +} + + diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java new file mode 100644 index 0000000..1329d3c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java @@ -0,0 +1,63 @@ +package com.tashow.erp.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据采集上报工具类 + * 统一处理各服务的数据上报逻辑 + */ +@Component +public class DataReportUtil { + private static final Logger logger = LoggerFactory.getLogger(DataReportUtil.class); + + @Autowired + private ApiForwarder apiForwarder; + + @Value("${api.server.base-url}") + private String serverApiUrl; + + @Value("${api.server.paths.data}") + private String monitorApiPath; + + /** + * 上报数据采集事件 + */ + public void reportDataCollection(String dataType, int dataCount, String status) { + try { + Map reportData = new HashMap<>(); + reportData.put("clientId", generateClientId(dataType)); + reportData.put("dataType", dataType); + reportData.put("dataCount", dataCount); + reportData.put("status", status); + + sendReportData(reportData); + logger.debug("数据上报成功: {} - {} 条", dataType, dataCount); + } catch (Exception e) { + logger.warn("数据上报失败: {}", e.getMessage()); + } + } + + private void sendReportData(Map reportData) { + apiForwarder.post(monitorApiPath, reportData, null); + } + + /** + * 生成客户端ID + */ + private String generateClientId(String serviceType) { + try { + String hostname = java.net.InetAddress.getLocalHost().getHostName(); + String username = System.getProperty("user.name", "user"); + return hostname + "_" + username + "_" + serviceType.toLowerCase(); + } catch (Exception e) { + return serviceType.toLowerCase() + "_client_" + System.currentTimeMillis(); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java new file mode 100644 index 0000000..f37cd12 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/DeviceUtils.java @@ -0,0 +1,33 @@ +package com.tashow.erp.utils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.UUID; + +/** + * 设备工具类 + */ +public class DeviceUtils { + + /** + * 生成设备ID + * 优先使用硬件UUID,失败则使用随机UUID + */ + public static String generateDeviceId() { + try { + Process process = Runtime.getRuntime().exec(new String[]{"wmic", "csproduct", "get", "UUID"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) { + return line; + } + } + } + } catch (Exception e) { + // 静默处理异常 + } + return UUID.randomUUID().toString(); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java new file mode 100644 index 0000000..066f9dd --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java @@ -0,0 +1,110 @@ +package com.tashow.erp.utils; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.io.StringWriter; +import java.io.PrintWriter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import com.tashow.erp.service.IAuthService; +/** + * 错误上报工具类 - 立即上报所有错误 + * + * @author ERP + */ +@Component +public class ErrorReporter { + + @Value("${server.monitor.url:http://localhost:8080}") + private String serverUrl; + + @Autowired + private IAuthService authService; + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 上报启动失败错误 + */ + public void reportStartupError(String component, Exception ex) { + reportError("STARTUP_ERROR", component + " 启动失败", ex); + } + + /** + * 上报业务处理错误 + */ + public void reportBusinessError(String operation, Exception ex) { + reportError("BUSINESS_ERROR", operation + " 处理失败", ex); + } + + /** + * 上报数据采集错误 + */ + public void reportDataCollectError(String platform, Exception ex) { + reportError("DATA_COLLECT_ERROR", platform + " 数据采集失败", ex); + } + /** + * 上报网络连接错误 + */ + public void reportNetworkError(String url, Exception ex) { + reportError("NETWORK_ERROR", "网络请求失败: " + url, ex); + } + + /** + * 上报系统异常 + */ + public void reportSystemError(String message, Exception ex) { + reportError("SYSTEM_ERROR", message, ex); + } + + /** + * 通用错误上报方法 + */ + private void reportError(String errorType, String errorMessage, Exception ex) { + CompletableFuture.runAsync(() -> { + try { + Map errorData = new HashMap<>(); + errorData.put("clientId", authService.getClientId()); + errorData.put("username", System.getProperty("user.name", "user")); + errorData.put("errorType", errorType); + errorData.put("errorMessage", errorMessage); + errorData.put("stackTrace", getStackTrace(ex)); + + // 添加系统信息 + errorData.put("osName", System.getProperty("os.name")); + errorData.put("osVersion", System.getProperty("os.version")); + errorData.put("appVersion", System.getProperty("project.version", "unknown")); + + String url = serverUrl + "/monitor/client/api/error"; + restTemplate.postForObject(url, errorData, Map.class); + + } catch (Exception e) { + System.err.println("错误上报失败: " + e.getMessage()); + } + }); + } + + /** + * 获取异常堆栈信息 + */ + private String getStackTrace(Exception ex) { + if (ex == null) return ""; + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + ex.printStackTrace(pw); + return sw.toString(); + } + /** + * 设置全局异常处理器 + */ + public void setupGlobalExceptionHandler() { + Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { + reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex); + }); + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelExportUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelExportUtil.java new file mode 100644 index 0000000..d48998e --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelExportUtil.java @@ -0,0 +1,310 @@ +package com.tashow.erp.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.*; +import java.util.concurrent.*; + +/** + * Excel导出工具类,支持图片嵌入 + */ +@Slf4j +public class ExcelExportUtil { + + /** + * 创建包含图片的Excel文件 + * @param sheetName 工作表名称 + * @param headers 表头 + * @param data 数据列表 + * @param imageColumnIndex 图片列索引 + * @param imageUrlKey 图片URL的字段名 + * @return Excel文件字节数组 + */ + public static byte[] createExcelWithImages(String sheetName, String[] headers, + List> data, int imageColumnIndex, String imageUrlKey) { + try { + XSSFWorkbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet(sheetName); + + // 创建标题行 + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + } + + // 创建绘图容器 + Drawing drawing = sheet.createDrawingPatriarch(); + + // 批量下载图片 + Map imageCache = new HashMap<>(); + downloadImagesInBatches(data, imageCache, imageUrlKey); + + // 填充数据 + for (int rowIndex = 0; rowIndex < data.size(); rowIndex++) { + Map rowData = data.get(rowIndex); + Row dataRow = sheet.createRow(rowIndex + 1); + dataRow.setHeightInPoints(80); + + // 填充各列数据 + fillRowData(dataRow, rowData, headers, rowIndex); + + // 处理图片列 + if (imageColumnIndex >= 0) { + String imgUrl = getStringValue(rowData, imageUrlKey); + embedImage(workbook, drawing, imageCache, imgUrl, imageColumnIndex, rowIndex + 1, dataRow); + } + } + + // 调整列宽 + adjustColumnWidths(sheet, headers, imageColumnIndex); + + // 输出文件 + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + workbook.close(); + return out.toByteArray(); + + } catch (Exception e) { + log.error("创建Excel文件失败: {}", e.getMessage(), e); + return new byte[0]; + } + } + + /** + * 填充行数据 - 需要子类实现具体逻辑 + */ + private static void fillRowData(Row dataRow, Map rowData, String[] headers, int rowIndex) { + // 这里可以根据不同的数据类型进行扩展 + int colIndex = 0; + for (String header : headers) { + if (!header.equals("商品图片")) { // 跳过图片列 + String key = getKeyByHeader(header); + if (key != null) { + Object value = rowData.get(key); + dataRow.createCell(colIndex).setCellValue(formatValue(value, header)); + } + } + colIndex++; + } + } + + /** + * 根据表头获取对应的数据字段名 + */ + private static String getKeyByHeader(String header) { + Map headerKeyMap = new HashMap<>(); + headerKeyMap.put("下单时间", "orderedAt"); + headerKeyMap.put("商品名称", "productTitle"); + headerKeyMap.put("乐天订单号", "shopOrderNumber"); + headerKeyMap.put("下单距今时间", "timeSinceOrder"); + headerKeyMap.put("乐天订单金额/日元", "priceJpy"); + headerKeyMap.put("购买数量", "productQuantity"); + headerKeyMap.put("税费/日元", "shippingFeeJpy"); + headerKeyMap.put("服务商回款抽点rmb", "serviceFee"); + headerKeyMap.put("商品番号", "productNumber"); + headerKeyMap.put("1688采购订单号", "poNumber"); + headerKeyMap.put("采购金额/rmb", "shippingFeeCny"); + headerKeyMap.put("国际运费/rmb", "internationalShippingFee"); + headerKeyMap.put("国内物流公司", "poLogisticsCompany"); + headerKeyMap.put("国内物流单号", "poTrackingNumber"); + headerKeyMap.put("日本物流单号", "internationalTrackingNumber"); + headerKeyMap.put("地址状态", "trackInfo"); + + // 乐天商品相关 + headerKeyMap.put("店铺名", "originalShopName"); + headerKeyMap.put("商品链接", "productUrl"); + headerKeyMap.put("排名", "ranking"); + headerKeyMap.put("商品标题", "productTitle"); // 添加缺失的商品标题映射 + headerKeyMap.put("价格", "price"); + headerKeyMap.put("1688识图链接", "image1688Url"); + headerKeyMap.put("1688价格", "median"); + headerKeyMap.put("1688重量", "weight"); + + return headerKeyMap.get(header); + } + + /** + * 格式化值 + */ + private static String formatValue(Object value, String header) { + if (value == null) return ""; + + if (header.contains("日元")) { + try { + double amount = Double.parseDouble(value.toString()); + return String.format("¥%.0f", amount); + } catch (NumberFormatException e) { + return value.toString(); + } + } else if (header.contains("rmb")) { + try { + double amount = Double.parseDouble(value.toString()); + return String.format("¥%.2f", amount); + } catch (NumberFormatException e) { + return value.toString(); + } + } else if (value instanceof List) { + List list = (List) value; + return list.isEmpty() ? "" : list.get(0).toString(); + } + + return value.toString(); + } + + /** + * 嵌入图片 + */ + private static void embedImage(XSSFWorkbook workbook, Drawing drawing, Map imageCache, + String imgUrl, int imageColumnIndex, int rowIndex, Row dataRow) { + if (imgUrl != null && !imgUrl.isEmpty()) { + byte[] imageBytes = imageCache.get(imgUrl); + if (imageBytes != null && imageBytes.length > 0) { + try { + int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_JPEG); + ClientAnchor anchor = workbook.getCreationHelper().createClientAnchor(); + anchor.setCol1(imageColumnIndex); + anchor.setRow1(rowIndex); + anchor.setCol2(imageColumnIndex + 1); + anchor.setRow2(rowIndex + 1); + Picture picture = drawing.createPicture(anchor, pictureIdx); + picture.resize(0.8); + } catch (Exception e) { + log.warn("嵌入图片失败: {}, 错误: {}", imgUrl, e.getMessage()); + dataRow.createCell(imageColumnIndex).setCellValue("图片嵌入失败"); + } + } else { + dataRow.createCell(imageColumnIndex).setCellValue("图片加载失败"); + } + } else { + dataRow.createCell(imageColumnIndex).setCellValue("无图片"); + } + } + + /** + * 调整列宽 + */ + private static void adjustColumnWidths(Sheet sheet, String[] headers, int imageColumnIndex) { + for (int i = 0; i < headers.length; i++) { + if (i != imageColumnIndex) { + sheet.autoSizeColumn(i); + } else { + sheet.setColumnWidth(i, 25 * 256); + } + } + } + + /** + * 批量下载图片 + */ + private static void downloadImagesInBatches(List> data, Map imageCache, String imageUrlKey) { + final int BATCH_SIZE = 10; // 减少批次大小提升并发效率 + final int MAX_RETRIES = 3; // 增加重试次数提高成功率 + + Set uniqueUrls = new HashSet<>(); + for (Map item : data) { + String imgUrl = getStringValue(item, imageUrlKey); + if (imgUrl != null && !imgUrl.isEmpty()) { + uniqueUrls.add(imgUrl); + } + } + + List urlList = new ArrayList<>(uniqueUrls); + log.info("开始批量下载图片,共 {} 个唯一图片", urlList.size()); + + for (int i = 0; i < urlList.size(); i += BATCH_SIZE) { + int endIndex = Math.min(i + BATCH_SIZE, urlList.size()); + List batch = urlList.subList(i, endIndex); + + log.info("处理第 {}/{} 批图片,本批 {} 个", + (i / BATCH_SIZE) + 1, (urlList.size() + BATCH_SIZE - 1) / BATCH_SIZE, batch.size()); + + ExecutorService executor = Executors.newFixedThreadPool(Math.min(12, batch.size())); // 增加线程池大小 + List> futures = new ArrayList<>(); + + for (String url : batch) { + CompletableFuture future = CompletableFuture.runAsync(() -> { + byte[] imageBytes = downloadImageWithRetry(url, MAX_RETRIES); + if (imageBytes != null && imageBytes.length > 0) { + synchronized (imageCache) { + imageCache.put(url, imageBytes); + } + } + }, executor); + futures.add(future); + } + + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(30, TimeUnit.SECONDS); // 减少超时时间 + } catch (Exception e) { + log.warn("批量下载图片超时或失败: {}", e.getMessage()); + // 取消未完成的任务 + futures.forEach(future -> future.cancel(true)); + } + + executor.shutdown(); + + if (i + BATCH_SIZE < urlList.size()) { + try { + Thread.sleep(500); // 减少批次间延迟 + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } + + log.info("图片下载完成,成功缓存 {} 个图片", imageCache.size()); + } + + /** + * 带重试的图片下载 - 优化版本 + */ + private static byte[] downloadImageWithRetry(String imageUrl, int maxRetries) { + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + URL url = new URL(imageUrl); + // 设置连接超时和读取超时 + java.net.URLConnection connection = url.openConnection(); + connection.setConnectTimeout(5000); // 5秒连接超时 + connection.setReadTimeout(10000); // 10秒读取超时 + connection.setRequestProperty("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (InputStream inputStream = connection.getInputStream()) { + byte[] data = IOUtils.toByteArray(inputStream); + if (data.length > 0) { + return data; + } + } + } catch (Exception e) { + if (attempt == maxRetries) { + log.warn("下载图片失败 (已重试{}次): {}, 错误: {}", maxRetries, imageUrl, e.getMessage()); + } else { + try { + Thread.sleep(500 * attempt); // 减少重试间隔 + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + return null; + } + + /** + * 安全获取字符串值 + */ + private static String getStringValue(Map map, String key) { + Object value = map.get(key); + return value != null ? value.toString() : ""; + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelParseUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelParseUtil.java new file mode 100644 index 0000000..bc701d8 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ExcelParseUtil.java @@ -0,0 +1,104 @@ +package com.tashow.erp.utils; + +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Excel 解析工具类 + * 统一处理各种平台的 Excel 文件解析需求 + * + * @author Claude Code + */ +@Slf4j +public class ExcelParseUtil { + + /** + * 解析 Excel 文件第一列数据 + * 通用方法,适用于店铺名、ASIN、订单号等标识符解析 + * + * @param file Excel 文件 + * @return 解析出的字符串列表,跳过表头,过滤空值 + */ + public static List parseFirstColumn(MultipartFile file) { + List result = new ArrayList<>(); + if (file == null || file.isEmpty()) { + log.warn("Excel 文件为空"); + return result; + } + + try (InputStream in = file.getInputStream()) { + ExcelReader reader = ExcelUtil.getReader(in, 0); + List> rows = reader.read(); + + for (int i = 1; i < rows.size(); i++) { // 从第2行开始,跳过表头 + List row = rows.get(i); + if (row != null && !row.isEmpty()) { + Object cell = row.get(0); // 获取第一列 + if (cell != null) { + String value = cell.toString().trim(); + if (!value.isEmpty()) { + result.add(value); + } + } + } + } + + log.info("成功解析 Excel 文件: {}, 共解析出 {} 条数据", + file.getOriginalFilename(), result.size()); + + } catch (Exception e) { + log.error("解析 Excel 文件失败: {}, 文件名: {}", e.getMessage(), + file.getOriginalFilename(), e); + } + + return result; + } + + /** + * 解析指定列的数据 + * + * @param file Excel 文件 + * @param columnIndex 列索引(从0开始) + * @return 解析出的字符串列表 + */ + public static List parseColumn(MultipartFile file, int columnIndex) { + List result = new ArrayList<>(); + if (file == null || file.isEmpty()) { + log.warn("Excel 文件为空"); + return result; + } + + try (InputStream in = file.getInputStream()) { + ExcelReader reader = ExcelUtil.getReader(in, 0); + List> rows = reader.read(); + + for (int i = 1; i < rows.size(); i++) { // 从第2行开始,跳过表头 + List row = rows.get(i); + if (row != null && row.size() > columnIndex) { + Object cell = row.get(columnIndex); + if (cell != null) { + String value = cell.toString().trim(); + if (!value.isEmpty()) { + result.add(value); + } + } + } + } + + log.info("成功解析 Excel 文件第{}列: {}, 共解析出 {} 条数据", + columnIndex + 1, file.getOriginalFilename(), result.size()); + + } catch (Exception e) { + log.error("解析 Excel 文件第{}列失败: {}, 文件名: {}", + columnIndex + 1, e.getMessage(), file.getOriginalFilename(), e); + } + + return result; + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/JsonData.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/JsonData.java new file mode 100644 index 0000000..09ba4a2 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/JsonData.java @@ -0,0 +1,96 @@ +package com.tashow.erp.utils; + +public class JsonData { + + /** + * 状态码 0表示成功过,1表示处理中,-1 表示失败 + */ + private Integer code; + + /** + * 业务数据 + */ + private Object data; + + /** + * 信息表示 + */ + private String msg; + + public JsonData() { + } + + public JsonData(Integer code, Object data, String msg) { + this.code = code; + this.data = data; + this.msg = msg; + } + + + /** + * 成功,不用返回数据 + * + * @return + */ + public static JsonData buildSuccess() { + return new JsonData(0, null, null); + } + + /** + * 成功,返回数据 + * + * @param data + * @return + */ + public static JsonData buildSuccess(Object data) { + return new JsonData(0, data, null); + } + + + /** + * 失败,固定状态码 + * + * @param msg + * @return + */ + public static JsonData buildError(String msg) { + return new JsonData(-1, null, msg); + } + + + /** + * 失败,自定义错误码和信息 + * + * @param code + * @param msg + * @return + */ + public static JsonData buildError(Integer code, String msg) { + return new JsonData(code, null, msg); + } + + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/LoggerUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/LoggerUtil.java new file mode 100644 index 0000000..e76b572 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/LoggerUtil.java @@ -0,0 +1,73 @@ +package com.tashow.erp.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 日志工具类 + * + * @author ruoyi + */ +public class LoggerUtil { + + /** + * 获取Logger实例 + */ + public static Logger getLogger(Class clazz) { + return LoggerFactory.getLogger(clazz); + } + + /** + * 记录API调用信息 + */ + public static void logApiCall(Logger logger, String apiName, String url, Object params) { + if (logger.isDebugEnabled()) { + logger.debug("调用API: {} - URL: {} - 参数: {}", apiName, url, params); + } + } + + /** + * 记录API响应信息 + */ + public static void logApiResponse(Logger logger, String apiName, Object response) { + if (logger.isDebugEnabled()) { + logger.debug("API响应: {} - 结果: {}", apiName, response); + } + } + + /** + * 记录错误信息 + */ + public static void logError(Logger logger, String operation, Throwable throwable) { + logger.error("操作失败: {} - 错误: {}", operation, throwable.getMessage(), throwable); + } + + /** + * 记录错误信息(不包含堆栈跟踪) + */ + public static void logErrorSimple(Logger logger, String operation, String errorMessage) { + logger.error("操作失败: {} - 错误: {}", operation, errorMessage); + } + + /** + * 记录警告信息 + */ + public static void logWarning(Logger logger, String operation, String message) { + logger.warn("操作警告: {} - 消息: {}", operation, message); + } + + /** + * 记录信息 + */ + public static void logInfo(Logger logger, String operation, String message) { + logger.info("操作信息: {} - 消息: {}", operation, message); + } + + /** + * 记录性能信息 + */ + public static void logPerformance(Logger logger, String operation, long startTime) { + long duration = System.currentTimeMillis() - startTime; + logger.info("性能统计: {} - 耗时: {}ms", operation, duration); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/PersistentDriverManager.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/PersistentDriverManager.java new file mode 100644 index 0000000..5295458 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/PersistentDriverManager.java @@ -0,0 +1,63 @@ +package com.tashow.erp.utils; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.chrome.ChromeDriver; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class PersistentDriverManager { + + private ChromeDriver headlessDriver; + private ChromeDriver headfulDriver; + private boolean usingHeadless = true; + + @PostConstruct + public void init() { + headlessDriver = StealthSelenium.createDriver(true); + } + + public ChromeDriver getCurrentDriver() { + return usingHeadless ? headlessDriver : headfulDriver; + } + + public ChromeDriver switchToHeadful() { + if (usingHeadless) { + if (headfulDriver == null) { + headfulDriver = StealthSelenium.createDriver(false); + } + transferState(headlessDriver, headfulDriver); + usingHeadless = false; + } + return headfulDriver; + } + + + private void transferState(ChromeDriver from, ChromeDriver to) { + try { + String currentUrl = from.getCurrentUrl(); + Set cookies = from.manage().getCookies(); + + to.get(currentUrl); + cookies.forEach(cookie -> { + try { + to.manage().addCookie(cookie); + } catch (Exception ignored) {} + }); + to.navigate().refresh(); + } catch (Exception ignored) {} + } + + @PreDestroy + public void cleanup() { + if (headlessDriver != null) { + headlessDriver.quit(); + } + if (headfulDriver != null) { + headfulDriver.quit(); + } + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/QiniuUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/QiniuUtil.java new file mode 100644 index 0000000..ac61ab1 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/QiniuUtil.java @@ -0,0 +1,44 @@ +package com.tashow.erp.utils; + +import com.qiniu.common.QiniuException; +import com.qiniu.http.Response; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.Region; +import com.qiniu.storage.UploadManager; +import com.qiniu.util.Auth; +import org.springframework.web.client.RestTemplate; + +import java.io.ByteArrayInputStream; +public class QiniuUtil { + // 七牛配置 + private static final String ACCESS_KEY = "M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF"; + private static final String SECRET_KEY = "Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl"; + private static final String BUCKET = "pxdj-prod"; + private static final String DOMAIN = "https://qiniu.pxdj.tashowz.com/"; // 开启HTTPS的外链域名 + + public static String uploadFromUrl(String imageUrl, String key) throws Exception { + // 1. 下载远程图片 + RestTemplate restTemplate = new RestTemplate(); + byte[] imageBytes = restTemplate.getForObject(imageUrl, byte[].class); + // 2. 配置七牛上传管理器 + Configuration cfg = new Configuration(Region.autoRegion()); + UploadManager uploadManager = new UploadManager(cfg); + // 3. 生成上传凭证 + Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY); + String upToken = auth.uploadToken(BUCKET); + + // 4. 上传图片 + try { + Response response = uploadManager.put(new ByteArrayInputStream(imageBytes), key, upToken, null, null); + if (response.isOK()) { + // 返回HTTPS访问URL + return DOMAIN + key; + } else { + throw new RuntimeException("上传失败: " + response.bodyString()); + } + } catch (QiniuException ex) { + throw new RuntimeException("上传异常: " + ex.response.toString(), ex); + } + } + +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/RakutenProxyUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/RakutenProxyUtil.java new file mode 100644 index 0000000..d6a734c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/RakutenProxyUtil.java @@ -0,0 +1,123 @@ +package com.tashow.erp.utils; +import us.codecraft.webmagic.downloader.HttpClientDownloader; +import us.codecraft.webmagic.proxy.SimpleProxyProvider; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.Proxy; +/** + * 乐天爬取代理工具类 + */ +public class RakutenProxyUtil { + /** + * 动态检测系统代理配置 + * @param targetUrl 目标URL + * @return 代理信息,如果没有代理返回null + */ + public Proxy detectSystemProxy(String targetUrl) { + try { + System.setProperty("java.net.useSystemProxies", "true"); + Proxy windowsProxy = getWindowsProxy(); + if (windowsProxy != null) { + return windowsProxy; + } + System.out.println("未找到可用的HTTP或SOCKS代理"); + } catch (Exception e) { + System.out.println("检测系统代理失败:"+e ); + } + return null; + } + /** + * 从Windows注册表获取代理设置 + */ + private Proxy getWindowsProxy() { + try { + // 先检查ProxyEnable是否启用 + String enableCommand = "reg query \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable"; + Process enableProcess = Runtime.getRuntime().exec(enableCommand); + try (BufferedReader enableReader = new BufferedReader(new InputStreamReader(enableProcess.getInputStream(), "GBK"))) { + String line; + while ((line = enableReader.readLine()) != null) { + if (line.contains("ProxyEnable") && line.contains("REG_DWORD")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length >= 3) { + String enableValue = parts[parts.length - 1]; + if ("0x0".equals(enableValue) || "0".equals(enableValue)) { + System.out.println("系统代理未启用,不使用代理"); + return null; + } + } + } + } + } + enableProcess.waitFor(); + + // ProxyEnable为1,继续检查ProxyServer配置 + String command = "reg query \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyServer"; + Process process = Runtime.getRuntime().exec(command); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("ProxyServer") && line.contains("REG_SZ")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length >= 3) { + String proxyServer = parts[parts.length - 1]; + if (proxyServer.contains(":")) { + String[] hostPort = proxyServer.split(":"); + if (hostPort.length == 2) { + try { + String host = hostPort[0]; + int port = Integer.parseInt(hostPort[1]); + InetSocketAddress address = new InetSocketAddress(host, port); + System.out.println("检测到系统代理: " + host + ":" + port); + return new Proxy(Proxy.Type.HTTP, address); + } catch (NumberFormatException e) { + System.out.println("Windows代理端口格式错误: "+hostPort[1]); + } + } + } + } + } + } + } + process.waitFor(); + } catch (Exception e) { + System.out.println("读取Windows代理设置失败: " + e.getMessage()); + } + return null; + } + + /** + * 创建带代理的HttpClientDownloader - 禁用默认行为 + * @param proxy 代理对象 + * @return HttpClientDownloader实例 + */ + public HttpClientDownloader createProxyDownloader(Proxy proxy) { + HttpClientDownloader httpClientDownloader = new HttpClientDownloader(); + if (proxy != null && proxy.address() instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) proxy.address(); + String host = address.getHostName(); + int port = address.getPort(); + clearSystemProxy(); + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", String.valueOf(port)); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", String.valueOf(port)); + httpClientDownloader.setProxyProvider(SimpleProxyProvider.from( + new us.codecraft.webmagic.proxy.Proxy(host, port) + )); + System.out.println("已配置代理:"+ host+":"+port); + } + return httpClientDownloader; + } + + public static void clearSystemProxy() { + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + System.clearProperty("socksProxyHost"); + System.clearProperty("socksProxyPort"); + } + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ResourcePreloader.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ResourcePreloader.java new file mode 100644 index 0000000..072d434 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ResourcePreloader.java @@ -0,0 +1,23 @@ +package com.tashow.erp.utils; + +import lombok.extern.slf4j.Slf4j; + +/** + * 资源预加载器(纯 Spring Boot 占位实现) + * 去除 JavaFX 依赖,保留方法签名以兼容调用方。 + */ +@Slf4j +public class ResourcePreloader { + + public static synchronized void init() { + // no-op in Spring Boot mode + } + + public static java.util.concurrent.CompletableFuture preloadErpDashboard() { + return java.util.concurrent.CompletableFuture.completedFuture(null); + } + + public static void executePreloading() { + // no-op in Spring Boot mode + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/SagawaExpressSdk.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/SagawaExpressSdk.java new file mode 100644 index 0000000..8f759df --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/SagawaExpressSdk.java @@ -0,0 +1,99 @@ +package com.tashow.erp.utils; + +import lombok.Data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import us.codecraft.webmagic.Page; +import us.codecraft.webmagic.Site; +import us.codecraft.webmagic.Spider; +import us.codecraft.webmagic.processor.PageProcessor; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +@Data +public class SagawaExpressSdk { + private static final Logger logger = LoggerFactory.getLogger(SagawaExpressSdk.class); + private final ExecutorService executorService = Executors.newFixedThreadPool(10); + private int timeout = 5000; // 默认超时时间5秒 + /** + * 关闭线程池 + */ +// @PreDestroy + public void destroy() { + executorService.shutdownNow(); + } + /** + * 查询佐川急便物流信息 - API入口 + */ + + public Map getTrackingInfo(String trackingNumber) { + try { + String url = "https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=" + trackingNumber.trim(); + Map result = new HashMap<>(); + Spider.create(new PageProcessor() { + @Override + public void process(Page page) { + String pageContent = page.getHtml().toString(); + if (pageContent.contains("お荷物データが登録されておりません")) { + return; + } + + // 提取物流表格 + String trackingTableRegex = "]*class=\"table_basic table_okurijo_detail2\"[^>]*>\\s*\\s*(?:.*?]*>荷物状況.*?.*?.*?.*?)+\\s*\\s*"; + Pattern pattern = Pattern.compile(trackingTableRegex, Pattern.DOTALL); + Matcher matcher = pattern.matcher(pageContent); + + if (matcher.find()) { + String trackingTable = matcher.group(0); + + // 从表格中提取所有行并取最后一行数据 + String rowRegex = "\\s*\\s*([^<]*?)\\s*\\s*\\s*([^<]*?)\\s*\\s*\\s*([^<]*?)\\s*\\s*"; + Pattern rowPattern = Pattern.compile(rowRegex, Pattern.DOTALL); + Matcher rowMatcher = rowPattern.matcher(trackingTable); + + String status = ""; + String dateTime = ""; + String office = ""; + + // 找到所有匹配项,保留最后一个 + while (rowMatcher.find()) { + status = rowMatcher.group(1).trim(); + dateTime = rowMatcher.group(2).trim(); + office = rowMatcher.group(3).trim(); + } + + if (!status.isEmpty()) { + Map trackInfo = new HashMap<>(); + trackInfo.put("status", status); + trackInfo.put("dateTime", dateTime); + trackInfo.put("office", office); + System.out.println("11111111111"+trackInfo); + result.put("trackInfo", trackInfo); + } else { + + } + } + + } + + @Override + public Site getSite() { + return Site.me() + .setTimeOut(timeout) // 使用设置的超时时间 + .setRetryTimes(3) + .setSleepTime(500); + } + }).addUrl(url).thread(executorService, 5).start(); + return result; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/StealthSelenium.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/StealthSelenium.java new file mode 100644 index 0000000..e13eede --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/StealthSelenium.java @@ -0,0 +1,143 @@ +package com.tashow.erp.utils; + +import io.github.bonigarcia.wdm.WebDriverManager; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Selenium反检测工具类 + * 使用方法:ChromeDriver driver = StealthSelenium.createDriver(); + */ +public class StealthSelenium { + private static final List USER_AGENTS = Arrays.asList( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + ); + + private static final Random random = new Random(); + /** + * 创建反检测ChromeDriver + * @return 配置好的ChromeDriver实例 + */ + public static ChromeDriver createDriver() { + return createDriver(false); + } + + /** + * 创建反检测ChromeDriver + * @param headless 是否无头模式 + * @return 配置好的ChromeDriver实例 + */ + public static ChromeDriver createDriver(boolean headless) { + WebDriverManager.chromedriver().setup(); + ChromeOptions options = createStealthOptions(headless); + ChromeDriver driver = new ChromeDriver(options); + applyAntiDetection(driver); + setRandomWindowProperties(driver); + driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + return driver; + } + private static ChromeOptions createStealthOptions(boolean headless) { + ChromeOptions options = new ChromeOptions(); + + options.addArguments( + "--disable-blink-features=AutomationControlled", + "--disable-extensions", + "--disable-gpu", + "--disable-dev-shm-usage", + "--no-sandbox", + "--disable-background-timer-throttling", + "--disable-backgrounding-occluded-windows", + "--disable-renderer-backgrounding", + "--disable-features=TranslateUI", + "--disable-ipc-flooding-protection", + "--exclude-switches=enable-automation", + "--disable-component-extensions-with-background-pages", + "--disable-web-security", + "--disable-client-side-phishing-detection", + "--lang=zh-CN" + ); + + if (headless) { + options.addArguments("--headless"); + } + + String userAgent = USER_AGENTS.get(random.nextInt(USER_AGENTS.size())); + options.addArguments("--user-agent=" + userAgent); + + Map prefs = new HashMap<>(); + prefs.put("profile.default_content_setting_values.notifications", 2); + prefs.put("profile.default_content_settings.popups", 0); + options.setExperimentalOption("prefs", prefs); + options.setExperimentalOption("excludeSwitches", Arrays.asList("enable-automation")); + options.setExperimentalOption("useAutomationExtension", false); + + return options; + } + + private static void applyAntiDetection(ChromeDriver driver) { + String stealthScript = """ + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + delete Object.getPrototypeOf(navigator).webdriver; + + window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} }; + + Object.defineProperty(navigator, 'permissions', { + get: () => ({ query: () => Promise.resolve({state: 'granted'}) }) + }); + + Object.defineProperty(navigator, 'plugins', { + get: () => Array.from({length: Math.floor(Math.random() * 5) + 2}, (_, i) => ({ + name: `Plugin ${i}`, description: `Plugin Description ${i}`, + filename: `plugin${i}.dll`, version: `${Math.floor(Math.random() * 10)}.0.0` + })) + }); + + Object.defineProperty(navigator, 'languages', { + get: () => ['zh-CN', 'zh', 'en-US', 'en'] + }); + + const screenWidth = 1920 + Math.floor(Math.random() * 100); + const screenHeight = 1080 + Math.floor(Math.random() * 100); + Object.defineProperty(screen, 'width', { get: () => screenWidth }); + Object.defineProperty(screen, 'height', { get: () => screenHeight }); + Object.defineProperty(screen, 'availWidth', { get: () => screenWidth }); + Object.defineProperty(screen, 'availHeight', { get: () => screenHeight - 40 }); + + if (window.document) { + ['$cdc_asdjflasutopfhvcZLmcfl_', '$chrome_asyncScriptInfo'].forEach(prop => { + Object.defineProperty(window.document, prop, { get: () => undefined, set: () => {} }); + }); + } + """; + + Map params = new HashMap<>(); + params.put("source", stealthScript); + driver.executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", params); + + String userAgent = USER_AGENTS.get(random.nextInt(USER_AGENTS.size())); + Map networkParams = new HashMap<>(); + networkParams.put("userAgent", userAgent); + networkParams.put("acceptLanguage", "zh-CN,zh;q=0.9,en;q=0.8"); + networkParams.put("platform", "Win32"); + driver.executeCdpCommand("Network.setUserAgentOverride", networkParams); + } + + private static void setRandomWindowProperties(ChromeDriver driver) { + int width = 1200 + random.nextInt(400); + int height = 800 + random.nextInt(300); + driver.manage().window().setSize(new Dimension(width, height)); + driver.manage().window().setPosition(new Point(random.nextInt(100), random.nextInt(100))); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/StrFormatter.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/StrFormatter.java new file mode 100644 index 0000000..92c9a4a --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/StrFormatter.java @@ -0,0 +1,93 @@ +package com.tashow.erp.utils; + + +import com.tashow.erp.common.Convert; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java new file mode 100644 index 0000000..f20c36d --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java @@ -0,0 +1,718 @@ +package com.tashow.erp.utils; + + +import com.tashow.erp.common.Constants; +import org.springframework.util.AntPathMatcher; + +import java.util.*; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** 星号 */ + private static final char ASTERISK = '*'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) + { + if (isEmpty(str)) + { + return NULLSTR; + } + final int strLength = str.length(); + if (startInclude > strLength) + { + return NULLSTR; + } + if (endExclude > strLength) + { + endExclude = strLength; + } + if (startInclude > endExclude) + { + // 如果起始位置大于结束位置,不替换 + return NULLSTR; + } + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) + { + if (i >= startInclude && i < endExclude) + { + chars[i] = ASTERISK; + } + else + { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串 + * + * @param str 要截取的字符串 + * @param open 起始字符串 + * @param close 结束字符串 + * @return 截取结果 + */ + public static String substringBetweenLast(final String str, final String open, final String close) + { + if (isEmpty(str) || isEmpty(open) || isEmpty(close)) + { + return NULLSTR; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) + { + final int end = str.lastIndexOf(close); + if (end != INDEX_NOT_FOUND) + { + return str.substring(start + open.length(), end); + } + } + return NULLSTR; + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) + { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/application.yml b/erp_client_sb/src/main/resources/application.yml new file mode 100644 index 0000000..f28bdd2 --- /dev/null +++ b/erp_client_sb/src/main/resources/application.yml @@ -0,0 +1,71 @@ +javafx: + title: ERP系统 + appicons: /icon/icon.png + stage: + width: 1280 + height: 800 +# style: DECORATED # javafx.stage.StageStyle [DECORATED, UNDECORATED, TRANSPARENT, UTILITY, UNIFIED] +# resizable: false + +spring: + datasource: + url: jdbc:sqlite:./data/erp-cache.db?journal_mode=WAL&synchronous=NORMAL&cache_size=10000&temp_store=memory&busy_timeout=30000 + driver-class-name: org.sqlite.JDBC + username: + password: + hikari: + maximum-pool-size: 1 + connection-timeout: 60000 + idle-timeout: 300000 + max-lifetime: 900000 + auto-commit: false + jpa: + database-platform: org.hibernate.community.dialect.SQLiteDialect + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate: + format_sql: true + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + connection: + provider_disables_autocommit: true + open-in-view: false + web: + resources: + static-locations: classpath:/static/ +server: + port: 8081 + address: 127.0.0.1 + +# 外部API服务配置 +api: + server: + # 主服务器API配置 + # base-url: "http://8.138.23.49:8080" + base-url: "http://192.168.1.89:8080" + paths: + monitor: "/monitor/client/api" + login: "/monitor/account/login" + heartbeat: "/monitor/client/api/heartbeat" + error: "/monitor/client/api/error" + data: "/monitor/client/api/data" + alibaba1688: "/monitor/client/api/alibaba1688" + version: "/system/version/check" + getGenmaijlToken: "/getToken" + updateGenmaijlToken: "/saveToken" +# 项目信息配置 +project: + version: @project.version@ + build: + time: @maven.build.timestamp@ + +logging: + level: + com.tashow.erp: INFO + org.hibernate.SQL: WARN + org.hibernate.type.descriptor.sql.BasicBinder: WARN + diff --git a/erp_client_sb/src/main/resources/logback.xml b/erp_client_sb/src/main/resources/logback.xml new file mode 100644 index 0000000..9386091 --- /dev/null +++ b/erp_client_sb/src/main/resources/logback.xml @@ -0,0 +1,19 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + + + + + + + + diff --git a/erp_client_sb/src/main/resources/static/css/Main.css b/erp_client_sb/src/main/resources/static/css/Main.css new file mode 100644 index 0000000..9511ef5 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/css/Main.css @@ -0,0 +1,3 @@ +.root { + /*-fx-background-color: #f0f0f0;*/ +} diff --git a/erp_client_sb/src/main/resources/static/css/erp-dashboard.css b/erp_client_sb/src/main/resources/static/css/erp-dashboard.css new file mode 100644 index 0000000..373f908 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/css/erp-dashboard.css @@ -0,0 +1,709 @@ +/* 全局样式重置 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f5f5f5; +} + +/* 主容器 */ +.erp-container { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* 左侧导航栏 */ +.sidebar { + width: 200px; + background-color: #ffffff; + border-right: 1px solid #e6e6e6; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.user-avatar { + padding: 20px; + text-align: center; + border-bottom: 1px solid #f0f0f0; +} + +.menu-group-title { + padding: 15px 20px 8px; + font-size: 12px; + color: #999; + font-weight: 500; +} + +.sidebar-menu { + border: none; + flex: 1; +} + +.menu-item-custom { + height: 45px; + line-height: 45px; + margin: 0 10px; + border-radius: 6px; +} + +.menu-item-custom:hover { + background-color: #f5f7fa !important; +} + +.menu-item-custom.is-active { + background-color: #ecf5ff !important; + color: #409EFF !important; +} + +.platform-item { + display: flex; + align-items: center; +} + +.platform-logo { + width: 20px; + height: 20px; + margin-right: 8px; + border-radius: 4px; +} + +.vip-section { + padding: 20px; + border-top: 1px solid #f0f0f0; +} + +.vip-button { + width: 100%; + height: 60px; + background: linear-gradient(135deg, #FFD700, #FFA500); + border: none; + border-radius: 8px; + color: white; + font-weight: bold; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.vip-button:hover { + background: linear-gradient(135deg, #FFA500, #FF8C00); +} + +.vip-subtitle { + font-size: 10px; + margin-top: 2px; + opacity: 0.9; +} + +/* 主内容区 */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 顶部导航条 */ +.top-navbar { + height: 60px; + background-color: #ffffff; + border-bottom: 1px solid #e6e6e6; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; +} + +.navbar-left .el-button-group { + margin-right: 20px; +} + +.navbar-center { + flex: 1; + text-align: center; +} + +.navbar-center .el-breadcrumb { + display: inline-block; +} + +.navbar-right { + display: flex; + align-items: center; + gap: 10px; +} + +/* 内容主体 */ +.content-body { + flex: 1; + padding: 20px; + overflow-y: auto; + background-color: #f5f5f5; + height: auto; + min-height: auto; +} + +/* 功能卡片 */ +.feature-cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; +} + +/* 欢迎区域 */ +.welcome-section { + padding: 20px 0; +} + +.welcome-card { + background: white; + border-radius: 12px; + padding: 40px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + text-align: center; + max-width: 600px; + margin: 0 auto; +} + +.welcome-card h2 { + color: #333; + font-size: 28px; + margin-bottom: 16px; + font-weight: 600; +} + +.welcome-card p { + color: #666; + font-size: 16px; + margin-bottom: 24px; + line-height: 1.6; +} + +.welcome-card ul { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + margin-top: 20px; +} + +.welcome-card li { + background: #f8f9fa; + padding: 12px 16px; + border-radius: 8px; + color: #555; + font-size: 14px; + border-left: 3px solid #409EFF; +} + +.card-item { + background: white; + border-radius: 8px; + padding: 20px; + display: flex; + align-items: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: transform 0.2s; +} + +.card-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +.card-icon { + width: 50px; + height: 50px; + border-radius: 50%; + background: #f0f9ff; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; +} + +.card-icon i { + font-size: 24px; + color: #409EFF; +} + +.card-content h3 { + font-size: 16px; + font-weight: 600; + margin-bottom: 5px; + color: #333; +} + +.card-content p { + font-size: 12px; + color: #666; + line-height: 1.4; +} + +/* 数据统计区域 */ +.stats-section { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 0; + margin-bottom: 15px; + padding: 0; + background: white; + border-radius: 0; + box-shadow: none; + border-bottom: 1px solid #e6e6e6; + width: 100%; +} + +.stats-item { + text-align: left; + padding: 8px 12px; + border-right: 1px solid #f0f0f0; + display: flex; + flex-direction: column; + justify-content: flex-start; + min-height: 60px; +} + +.stats-item:last-child { + border-right: none; +} + +.stats-label { + font-size: 11px; + color: #333; + margin-bottom: 4px; + line-height: 1.2; + font-weight: 500; +} + +.stats-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2px; +} + +.stats-row:last-child { + margin-bottom: 0; +} + +.stats-type { + font-size: 10px; + color: #666; + min-width: 30px; +} + +.stats-value { + font-size: 12px; + font-weight: bold; + color: #333; + line-height: 1.1; +} + +.stats-subtitle { + font-size: 10px; + color: #999; + margin-top: 2px; + line-height: 1.1; +} + +.stats-unit { + font-size: 12px; + color: #999; + margin-top: 2px; +} + +/* 筛选区域 */ +.filter-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding: 12px 20px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* 表格区域 */ +.table-section { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + overflow: hidden; + width: 100%; +} + +.table-section .el-table { + width: 100% !important; +} + +.table-section .el-table__body-wrapper { + width: 100% !important; + height:calc(100%) +} + +/* 内容主体区域优化 */ +.content-body { + flex: 1; + padding: 15px; + overflow-y: auto; + background-color: #f5f5f5; + width: 100%; + height: auto; + min-height: auto; +} + +/* 确保所有容器都占满宽度 */ +.filter-section, +.table-section, +.stats-section { + width: 100% !important; + box-sizing: border-box; +} + +/* Element UI表格强制全宽 */ +.el-table, +.el-table__header-wrapper, +.el-table__body-wrapper, +.el-table__footer-wrapper { + width: 100% !important; +} + +.el-table .el-table__header, +.el-table .el-table__body { + width: 100% !important; + table-layout: fixed; +} + +.product-info { + display: flex; + align-items: center; +} + +.product-image-placeholder { + width: 50px; + height: 50px; + border-radius: 4px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 18px; +} + +.product-details { + flex: 1; +} + +.product-title { + font-size: 14px; + color: #333; + margin-bottom: 5px; + line-height: 1.4; +} + +.product-tags { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.budget-info, .roi-info, .period-info, .create-time { + font-size: 12px; + line-height: 1.4; +} + +.budget-info .current-budget { + font-weight: bold; + color: #333; +} + +.budget-info .target-budget { + color: #666; +} + +.roi-info .roi-current { + color: #333; +} + +.roi-info .roi-actual { + color: #666; +} + +.ad-spend { + font-weight: bold; + color: #333; + margin-bottom: 4px; +} + +.spend-link a { + color: #409EFF; + text-decoration: none; + font-size: 12px; +} + +.spend-link a:hover { + text-decoration: underline; +} + +.status-info { + display: flex; + align-items: center; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 6px; +} + +.status-dot.running { + background-color: #67C23A; +} + +.status-dot.paused { + background-color: #E6A23C; +} + +.status-dot.stopped { + background-color: #909399; +} + +/* 分页 */ +.pagination-section { + padding: 20px; + text-align: right; + border-top: 1px solid #f0f0f0; +} + +/* 悬浮账号管理窗口 */ +.account-float-window { + position: fixed; + top: 80px; + right: 20px; + width: 280px; + background: white; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,0.15); + z-index: 1000; + overflow: hidden; +} + +.float-window-header { + height: 40px; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 15px; + border-bottom: 1px solid #e6e6e6; +} + +.window-title { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.window-controls { + display: flex; + gap: 5px; +} + +.float-window-content { + padding: 15px; +} + +.account-list { + margin-bottom: 15px; +} + +.account-item { + display: flex; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #f0f0f0; +} + +.account-item:last-child { + border-bottom: none; +} + +.account-status { + margin-right: 10px; +} + +.account-status .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.account-status .status-dot.online { + background-color: #67C23A; +} + +.account-status .status-dot.offline { + background-color: #F56C6C; +} + +.account-name { + margin-left: 10px; + font-size: 14px; + color: #333; +} + +.account-actions { + display: flex; + gap: 10px; +} + +.account-actions .el-button { + flex: 1; +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .feature-cards { + grid-template-columns: repeat(2, 1fr); + } + + .stats-section { + flex-wrap: wrap; + gap: 15px; + } +} + +@media (max-width: 768px) { + .sidebar { + width: 60px; + } + + .sidebar .menu-item-custom span { + display: none; + } + + .feature-cards { + grid-template-columns: 1fr; + } + + .filter-section { + flex-direction: column; + gap: 15px; + } + + .account-float-window { + width: 250px; + right: 10px; + } +} + +/* 自定义滚动条 */ +.sidebar::-webkit-scrollbar, +.content-body::-webkit-scrollbar { + width: 6px; +} + +.sidebar::-webkit-scrollbar-track, +.content-body::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.sidebar::-webkit-scrollbar-thumb, +.content-body::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.sidebar::-webkit-scrollbar-thumb:hover, +.content-body::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* 动画效果 */ +.card-item, +.account-float-window, +.menu-item-custom { + transition: all 0.3s ease; +} + +/* Element UI 组件样式覆盖 */ +.el-table { + font-size: 12px; +} + +.el-table th { + background-color: #fafafa; + font-weight: 600; + color: #333; +} + +.el-table td { + padding: 12px 0; +} + +.el-tag--mini { + height: 20px; + line-height: 18px; + font-size: 10px; +} + +.el-breadcrumb__inner { + color: #666; + font-weight: normal; +} + +.el-breadcrumb__inner:hover { + color: #409EFF; +} + +/* 状态标签颜色 */ +.el-tag--success { + background-color: #f0f9ff; + border-color: #b3d8ff; + color: #409EFF; +} + +.el-tag--warning { + background-color: #fdf6ec; + border-color: #f5dab1; + color: #e6a23c; +} + +.el-tag--danger { + background-color: #fef0f0; + border-color: #fbc4c4; + color: #f56c6c; +} +.el-tag--info { + background-color: #f4f4f5; + border-color: #d3d4d6; + color: #909399; +} + +/* 价格/费用标签与卖家样式 */ +.price-tag { + color: #F56C6C; + font-weight: 600; +} +.fee-tag { + color: #E6A23C; + font-weight: 600; +} +.primary-seller { + color: #303133; + font-weight: 600; +} diff --git a/erp_client_sb/src/main/resources/static/css/update-dialog.css b/erp_client_sb/src/main/resources/static/css/update-dialog.css new file mode 100644 index 0000000..8d69ee9 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/css/update-dialog.css @@ -0,0 +1,227 @@ +/* 更新对话框样式 */ +.update-dialog .el-dialog { + border-radius: 16px; + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15); +} + +.update-dialog .el-dialog__header { + display: block; + padding: 12px 20px 0 20px; + text-align: right; +} + +.update-dialog .el-dialog__headerbtn { + top: 12px; + right: 20px; + width: 32px; + height: 32px; + font-size: 18px; + border-radius: 50%; + background-color: transparent; +} + +.update-dialog .el-dialog__headerbtn:hover { + background-color: #f5f5f5; +} + +.update-dialog .el-dialog__close { + color: #909399; + font-weight: 400; +} + +.update-dialog .el-dialog__close:hover { + color: #409EFF; +} + +.update-dialog .el-dialog__body { + padding: 0; +} + +.update-content { + padding: 32px 24px 24px; +} + +.update-header { + display: flex; + align-items: flex-start; + margin-bottom: 24px; +} + +.update-header.text-center { + text-align: center; + flex-direction: column; + align-items: center; +} + +.app-icon { + width: 64px; + height: 64px; + border-radius: 12px; + margin-right: 16px; + flex-shrink: 0; +} + +.update-header.text-center .app-icon { + margin-right: 0; + margin-bottom: 16px; +} + +.update-header-content { + flex: 1; + min-width: 0; +} + +.update-header h3 { + font-size: 20px; + font-weight: 600; + margin: 16px 0 8px 0; + color: #1f2937; +} + +.update-header p { + font-size: 14px; + color: #6b7280; + margin: 0; + line-height: 1.5; +} + +.update-details { + background-color: #f9fafb; + border-radius: 8px; + padding: 16px; + margin: 24px 0; + max-height: 200px; + overflow-y: auto; +} + +.update-details h4 { + font-size: 14px; + font-weight: 600; + color: #374151; + margin: 0 0 8px 0; +} + +.update-details p { + font-size: 13px; + color: #6b7280; + line-height: 1.6; + margin: 0 0 16px 0; +} + +.update-details p:last-child { + margin-bottom: 0; +} + +.auto-update-section { + margin: 16px 0; +} + +.auto-update-section .el-checkbox { + color: #374151; + font-size: 14px; +} + +.update-actions { + margin-top: 24px; +} + +.update-buttons { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.update-buttons.text-center { + justify-content: center; +} + +.update-buttons .el-button { + flex: 1; + height: 40px; + font-size: 14px; + border-radius: 8px; +} + +.update-buttons.text-center .el-button { + flex: none; + min-width: 140px; +} + +/* 下载进度区域 */ +.download-progress { + margin: 24px 0; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + color: #6b7280; +} + +.download-note { + background-color: #fef3cd; + border: 1px solid #fcd34d; + border-radius: 8px; + padding: 12px; + margin: 16px 0; +} + +.download-note p { + font-size: 12px; + color: #92400e; + margin: 0; + line-height: 1.5; +} + +/* 进度条自定义样式 */ +.el-progress-bar__outer { + border-radius: 4px; + background-color: #e5e7eb; +} + +.el-progress-bar__inner { + border-radius: 4px; + transition: width 0.3s ease; +} + +/* 按钮样式调整 */ +.update-buttons .el-button--primary { + background-color: #2563eb; + border-color: #2563eb; + font-weight: 500; +} + +.update-buttons .el-button--primary:hover { + background-color: #1d4ed8; + border-color: #1d4ed8; +} + +.update-buttons .el-button:not(.el-button--primary) { + background-color: #f3f4f6; + border-color: #d1d5db; + color: #374151; + font-weight: 500; +} + +.update-buttons .el-button:not(.el-button--primary):hover { + background-color: #e5e7eb; + border-color: #9ca3af; +} + +/* 响应式调整 */ +@media (max-width: 480px) { + .update-content { + padding: 24px 16px; + } + + .update-buttons { + flex-direction: column; + } + + .update-buttons .el-button { + flex: none; + } +} \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/fxml/Main.fxml b/erp_client_sb/src/main/resources/static/fxml/Main.fxml new file mode 100644 index 0000000..470f8fb --- /dev/null +++ b/erp_client_sb/src/main/resources/static/fxml/Main.fxml @@ -0,0 +1,10 @@ + + + + + + +
+ +
+
diff --git a/erp_client_sb/src/main/resources/static/html/components/platform-amazon.html b/erp_client_sb/src/main/resources/static/html/components/platform-amazon.html new file mode 100644 index 0000000..69535e0 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/platform-amazon.html @@ -0,0 +1,718 @@ + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/platform-rakuten.html b/erp_client_sb/src/main/resources/static/html/components/platform-rakuten.html new file mode 100644 index 0000000..8d0d600 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/platform-rakuten.html @@ -0,0 +1,1183 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/platform-shopee.html b/erp_client_sb/src/main/resources/static/html/components/platform-shopee.html new file mode 100644 index 0000000..2154988 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/platform-shopee.html @@ -0,0 +1,699 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/platform-zebra.html b/erp_client_sb/src/main/resources/static/html/components/platform-zebra.html new file mode 100644 index 0000000..2031a28 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/platform-zebra.html @@ -0,0 +1,935 @@ + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/shopee/ad-hosting.html b/erp_client_sb/src/main/resources/static/html/components/shopee/ad-hosting.html new file mode 100644 index 0000000..1a0ff45 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/shopee/ad-hosting.html @@ -0,0 +1,431 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/shopee/auto-review.html b/erp_client_sb/src/main/resources/static/html/components/shopee/auto-review.html new file mode 100644 index 0000000..521e261 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/shopee/auto-review.html @@ -0,0 +1,264 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/shopee/flash-sale-auto.html b/erp_client_sb/src/main/resources/static/html/components/shopee/flash-sale-auto.html new file mode 100644 index 0000000..3c117fa --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/shopee/flash-sale-auto.html @@ -0,0 +1,1158 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/components/shopee/long-product-manage.html b/erp_client_sb/src/main/resources/static/html/components/shopee/long-product-manage.html new file mode 100644 index 0000000..344197b --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/components/shopee/long-product-manage.html @@ -0,0 +1,780 @@ + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/erp-dashboard.html b/erp_client_sb/src/main/resources/static/html/erp-dashboard.html new file mode 100644 index 0000000..d7f84e4 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/erp-dashboard.html @@ -0,0 +1,1093 @@ + + + + + + ERP管理系统 + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ + + + +
+ +
+ + v{{ updateInfo.currentVersion }} + + + + + +
+ +
+ + + + +
+ + +
+ +
+
+ ERP Logo +
+
+ +
+
+
+ +
+
+

{{ card.title }}

+

{{ card.description }}

+
+
+
+ + +
+
+
{{ stat.label }}
+
+ {{ row.type }}: + {{ row.value }} +
+
+
+ + +
+
+ + + {{ status.label }} + + +
+ +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ ERP Logo +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备
+ + + + + + + + + + + + + + + + 关闭 + +
+ + + +
+
+ +

请登录以使用系统功能

+
+ + + + + + + +
+ + 登录 + + + 取消 + +
+ +
+ + 还没有账号?点击注册 + +
+
+
+ + + +
+
+ +

创建新账号

+
+ + + + +
+ + 用户名可用 + + + 用户名已存在 + +
+ + + + + + + +
+ + 注册 + + + 取消 + +
+ +
+ + 已有账号?返回登录 + +
+
+
+ + + + + +
+
+ App Icon +
+

软件更新

+

新版本的"{{ updateInfo.appName || 'ERP客户端' }}"已发布。您现在的版本是 ERP{{ updateInfo.currentVersion }},要更新到 ERP{{ updateInfo.latestVersion }} 吗?

+
+
+ +
+

{{ updateInfo.updateTitle || '新版本功能' }}

+

{{ updateInfo.updateNotes || '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 增加了新的功能模块\n• 优化了数据处理性能' }}

+
+ +
+
+ 以后自动下载并安装更新 +
+
+ 跳过这个版本 + 安装更新 +
+
+
+ + +
+
+ App Icon +

正在更新 {{ updateInfo.appName || 'erp' }}

+

正在下载新版...

+
+ +
+
+ {{ downloadProgress.current }} / {{ downloadProgress.total }} +
+ + +
+ {{ downloadProgress.percentage }}% + {{ downloadProgress.speed }} +
+
+ + +
+ + +
+
+ App Icon +

正在更新 {{ updateInfo.appName || '我了个电商' }}

+

可以开始安装了

+
+ +
+
+ {{ downloadProgress.current }} /{{ downloadProgress.total }} +
+ + +
+ +
+ 稍后更新 + 安装并重新启动 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/html/index.html b/erp_client_sb/src/main/resources/static/html/index.html new file mode 100644 index 0000000..8b1ae9b --- /dev/null +++ b/erp_client_sb/src/main/resources/static/html/index.html @@ -0,0 +1,493 @@ + + + + + + ERP系统 + + + +
+ +
+
+
Shopee 工具箱
+
+ + +
+
+ +
+
+
Ad
+
+

广告投放情况

+

5个广告自动化投放中

+
+
+
+
A
+
+

自动回评

+

按照自动回复设置回复

+
+
+
+
T
+
+

同时特卖自动化

+

按照特卖时间自动上下架

+
+
+
+
P
+
+

批量修改商品

+

按模板批量修改商品信息

+
+
+
+ +
+
列表查看
+
广告设置
+
运行中
+
已结束
+
统计
+
任务中
+
已完成
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
商品信息每日预算目标ROIRO设置周期RO自动配置剩余时间/预计时间操作
+
商品超级高品质超级商品品牌
+
商品ID: 554677
+
NT$ 10057天307-09 17:00 + 任务中 +
+
商品超级高品质超级商品品牌
+
商品ID: 554677
+
NT$ 10057天307-09 17:00 + 任务中 +
+
商品超级高品质超级商品品牌
+
商品ID: 554677
+
NT$ 10057天307-09 17:00 + 已完成 +
+
商品超级高品质超级商品品牌
+
商品ID: 554677
+
NT$ 10057天307-09 17:00 + 已完成 +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/icon/icon.ico b/erp_client_sb/src/main/resources/static/icon/icon.ico new file mode 100644 index 0000000..6fcfdc3 Binary files /dev/null and b/erp_client_sb/src/main/resources/static/icon/icon.ico differ diff --git a/erp_client_sb/src/main/resources/static/icon/icon.png b/erp_client_sb/src/main/resources/static/icon/icon.png new file mode 100644 index 0000000..d826be6 Binary files /dev/null and b/erp_client_sb/src/main/resources/static/icon/icon.png differ diff --git a/erp_client_sb/src/main/resources/static/image/111.png b/erp_client_sb/src/main/resources/static/image/111.png new file mode 100644 index 0000000..aabd8cd Binary files /dev/null and b/erp_client_sb/src/main/resources/static/image/111.png differ diff --git a/erp_client_sb/src/main/resources/static/image/splash_screen.png b/erp_client_sb/src/main/resources/static/image/splash_screen.png new file mode 100644 index 0000000..165407b Binary files /dev/null and b/erp_client_sb/src/main/resources/static/image/splash_screen.png differ diff --git a/erp_client_sb/src/main/resources/static/js/api-utils.js b/erp_client_sb/src/main/resources/static/js/api-utils.js new file mode 100644 index 0000000..5d60776 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api-utils.js @@ -0,0 +1,157 @@ +/** + * 统一的API请求工具和错误处理 + * 极简化版本,专注于核心功能 + */ +class ApiUtils { + constructor() { + this.defaultTimeout = 30000; // 30秒超时 + } + + // 统一的错误消息映射 + getErrorMessage(error) { + const errorMap = { + // 网络相关错误 + 'network error': '网络连接失败', + 'timeout': '请求超时', + 'connection refused': '服务连接失败', + 'cors': '跨域请求失败', + + // 文件相关错误 + 'file format error': '文件格式错误', + 'file too large': '文件过大', + 'invalid file type': '文件类型不正确', + 'no data found': '未找到有效数据', + + // 服务器相关错误 + 'server error': '服务器繁忙', + 'internal server error': '系统内部错误', + 'forbidden': '操作被拒绝', + 'not found': '资源不存在', + 'unauthorized': '权限不足', + + // 业务相关错误 + 'asin': 'ASIN格式错误', + 'proxy': '代理连接异常', + 'amazon': 'Amazon服务异常', + 'rakuten': '乐天服务异常', + 'shopee': 'Shopee服务异常', + 'version check': '版本检查失败', + 'update': '更新失败', + + // JSON相关错误 + 'jsonnode': '数据解析失败', + 'cannot invoke': '数据处理异常', + 'null': '数据为空', + 'parsing': '数据格式错误' + }; + + const errorMsg = error?.message || error || ''; + const lowerMsg = errorMsg.toLowerCase(); + + // 查找匹配的错误类型 + for (const [key, value] of Object.entries(errorMap)) { + if (lowerMsg.includes(key)) { + return value; + } + } + + // 如果是纯英文,返回通用提示 + if (/^[a-zA-Z\s\d_.\-"():]+$/.test(errorMsg)) { + return '操作失败,请稍后重试'; + } + + return errorMsg || '未知错误'; + } + + + async request(url, options = {}) { + const config = { + timeout: options.timeout || this.defaultTimeout, + headers: { 'Content-Type': 'application/json', ...options.headers }, + ...options + }; + + const response = await httpClient(url, config).catch(error => { + throw new Error(this.getErrorMessage(error)); + }); + + if (response.data?.code !== undefined && !(response.data?.code === 0 || response.data?.code === 200)) { + throw new Error(this.getErrorMessage(response.data?.msg || '请求失败')); + } + + return response.data?.data !== undefined ? response.data.data : response.data; + } + + // 性能优化:直接返回数据,减少包装层级 + async get(url, params = {}) { + return this.request(url, { method: 'GET', params }); + } + + async post(url, data = {}) { + return this.request(url, { method: 'POST', data }); + } + + async upload(url, formData) { + return this.request(url, { + method: 'POST', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }); + } + + // 显示成功消息 + showSuccess(message) { + if (window.Vue && window.Vue.prototype.$message) { + window.Vue.prototype.$message.success(message); + } else { + console.log('✅ ' + message); + } + } + + // 显示错误消息 + showError(error) { + const message = this.getErrorMessage(error); + if (window.Vue && window.Vue.prototype.$message) { + window.Vue.prototype.$message.error(message); + } else { + console.error('❌ ' + message); + } + } + + // 显示警告消息 + showWarning(message) { + if (window.Vue && window.Vue.prototype.$message) { + window.Vue.prototype.$message.warning(message); + } else { + console.warn('⚠️ ' + message); + } + } + + // 显示信息消息 + showInfo(message) { + if (window.Vue && window.Vue.prototype.$message) { + window.Vue.prototype.$message.info(message); + } else { + console.info('ℹ️ ' + message); + } + } + + // 性能优化:内联验证逻辑,减少函数调用开销 + validateFile(file, options = {}) { + const { maxSize = 10, allowedTypes = ['.xlsx', '.xls'] } = options; + const fileName = file.name.toLowerCase(); + + if (!allowedTypes.some(type => fileName.endsWith(type))) { + throw new Error(`只支持 ${allowedTypes.join('、')} 格式文件`); + } + + if (file.size > maxSize * 1024 * 1024) { + throw new Error(`文件大小不能超过 ${maxSize}MB`); + } + + return true; + } +} + +// 创建全局实例 +window.apiUtils = new ApiUtils(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/api/amazon-api.js b/erp_client_sb/src/main/resources/static/js/api/amazon-api.js new file mode 100644 index 0000000..c461f44 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/amazon-api.js @@ -0,0 +1,86 @@ +/** + * 亚马逊平台API封装 + * 专门处理亚马逊相关的所有接口调用 + */ +class AmazonAPI { + + /** + * 批量获取产品信息 + * @param {Array} asinList - ASIN列表 + * @param {String} batchId - 批次ID + */ + async getProductsBatch(asinList, batchId) { + return await apiUtils.post('/api/amazon/products/batch', { + asinList, + batchId + }); + } + + /** + * 获取最新产品数据 + */ + async getLatestProducts() { + return await apiUtils.get('/api/amazon/products/latest'); + } + + /** + * 根据批次ID获取产品数据 + * @param {String} batchId - 批次ID + */ + async getProductsByBatch(batchId) { + return await apiUtils.get(`/api/amazon/products/batch/${batchId}`); + } + + /** + * 更新产品信息 + * @param {Object} productData - 产品数据 + */ + async updateProduct(productData) { + return await apiUtils.post('/api/amazon/products/update', productData); + } + + /** + * 删除产品 + * @param {String} productId - 产品ID + */ + async deleteProduct(productId) { + return await apiUtils.post('/api/amazon/products/delete', { id: productId }); + } + + /** + * 导出Excel + * @param {Array} products - 产品列表 + * @param {Object} options - 导出选项 + */ + async exportToExcel(products, options = {}) { + return await apiUtils.post('/api/amazon/export', { + products, + ...options + }); + } + + /** + * 获取产品统计信息 + */ + async getProductStats() { + return await apiUtils.get('/api/amazon/stats'); + } + + /** + * 搜索产品 + * @param {Object} searchParams - 搜索参数 + */ + async searchProducts(searchParams) { + return await apiUtils.get('/api/amazon/products/search', searchParams); + } + + /** + * 打开跟卖精灵 + */ + async openGenmaiSpirit() { + return await apiUtils.post('/api/genmai/open'); + } +} + +// 创建全局实例 +window.amazonAPI = new AmazonAPI(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/api/common-api.js b/erp_client_sb/src/main/resources/static/js/api/common-api.js new file mode 100644 index 0000000..a3bea8e --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/common-api.js @@ -0,0 +1,152 @@ +/** + * 通用API封装 + * 处理版本更新、系统监控等通用功能 + */ +class CommonAPI { + + /** + * 检查版本更新 + */ + async checkVersion() { + return await apiUtils.get('/api/update/check'); + } + + /** + * 获取当前版本信息 + */ + async getVersion() { + return await apiUtils.get('/api/update/version'); + } + + /** + * 自动更新 + * @param {Object} updateData - 更新数据 + */ + async autoUpdate(updateData) { + return await apiUtils.post('/api/update/auto-update', updateData); + } + + /** + * 获取下载进度 + */ + async getDownloadProgress() { + return await apiUtils.get('/api/update/progress'); + } + + /** + * 重置下载状态 + */ + async resetDownload() { + return await apiUtils.post('/api/update/reset'); + } + + /** + * 取消下载 + */ + async cancelDownload() { + return await apiUtils.post('/api/update/cancel'); + } + + /** + * 安装更新 + */ + async installUpdate() { + return await apiUtils.post('/api/update/install'); + } + + /** + * 跳过版本 + * @param {String} version - 版本号 + */ + async skipVersion(version) { + return await apiUtils.post('/api/update/skip-version', { version }); + } + + /** + * 客户端状态上报 + * @param {Object} statusData - 状态数据 + */ + async reportClientStatus(statusData) { + return await apiUtils.post('/api/monitor/client/status', statusData); + } + + /** + * 获取系统信息 + */ + async getSystemInfo() { + return await apiUtils.get('/api/system/info'); + } + + /** + * 获取服务器状态 + */ + async getServerStatus() { + return await apiUtils.get('/api/monitor/server'); + } + + /** + * 上传错误日志 + * @param {Object} errorData - 错误数据 + */ + async uploadErrorLog(errorData) { + return await apiUtils.post('/api/logs/error', errorData); + } + + /** + * 获取公告信息 + */ + async getNotifications() { + return await apiUtils.get('/api/notifications'); + } + + /** + * 标记公告已读 + * @param {String} notificationId - 公告ID + */ + async markNotificationRead(notificationId) { + return await apiUtils.post('/api/notifications/read', { + id: notificationId + }); + } + + /** + * 获取用户配置 + */ + async getUserConfig() { + return await apiUtils.get('/api/config/user'); + } + + /** + * 保存用户配置 + * @param {Object} config - 配置数据 + */ + async saveUserConfig(config) { + return await apiUtils.post('/api/config/user', config); + } + + /** + * 重置配置 + */ + async resetConfig() { + return await apiUtils.post('/api/config/reset'); + } + + /** + * 获取帮助文档 + * @param {String} section - 文档章节 + */ + async getHelpDoc(section) { + return await apiUtils.get(`/api/help/${section}`); + } + + /** + * 提交反馈 + * @param {Object} feedback - 反馈数据 + */ + async submitFeedback(feedback) { + return await apiUtils.post('/api/feedback', feedback); + } +} + +// 创建全局实例 +window.commonAPI = new CommonAPI(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/api/device-api.js b/erp_client_sb/src/main/resources/static/js/api/device-api.js new file mode 100644 index 0000000..ac9067a --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/device-api.js @@ -0,0 +1,30 @@ +// 设备管理 API - 通过本地代理转发到 ruoyi-admin +(function () { + const api = window.apiUtils; + const base = '/api/device'; // 由 DeviceProxyController 转发到 /monitor/device + + async function getQuota(username) { + const res = await api.get(`${base}/quota?username=${username}`); + return res && res.limit !== undefined ? res : (res?.data || res || {}); + } + + async function list(username) { + const res = await api.get(`${base}/list?username=${username}`); + return Array.isArray(res) ? res : (res?.data || []); + } + + async function register(payload) { + return api.post(`${base}/register`, payload); + } + + async function remove(payload) { + return api.post(`${base}/remove`, payload); + } + + async function heartbeat(payload) { + return api.post(`${base}/heartbeat`, payload); + } + + window.deviceAPI = { getQuota, list, register, remove, heartbeat }; +})(); + diff --git a/erp_client_sb/src/main/resources/static/js/api/rakuten-api.js b/erp_client_sb/src/main/resources/static/js/api/rakuten-api.js new file mode 100644 index 0000000..cc55514 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/rakuten-api.js @@ -0,0 +1,63 @@ +/** + * 乐天平台API封装 + * 专门处理乐天相关的所有接口调用 + */ +class RakutenAPI { + + /** + * 获取乐天商品数据(支持文件上传和单个店铺查询) + * @param {Object} params - 参数对象 + * @param {File} params.file - Excel文件(可选) + * @param {String} params.shopName - 店铺名(可选) + * @param {String} params.batchId - 批次ID(可选) + */ + async getProducts(params = {}) { + const formData = new FormData(); + + if (params.file) { + formData.append('file', params.file); + } + if (params.shopName) { + formData.append('shopName', params.shopName); + } + if (params.batchId) { + formData.append('batchId', params.batchId); + } + + return await apiUtils.upload('/api/rakuten/products', formData); + } + + /** + * 1688识图搜索 + * @param {String} imageUrl - 图片URL + * @param {String} sessionId - 会话ID (可选) + */ + async search1688(imageUrl, sessionId) { + const params = { imageUrl }; + if (sessionId) { + params.sessionId = sessionId; + } + return await apiUtils.post('/api/rakuten/search1688', params); + } + + + /** + * 获取最新的乐天商品数据 + */ + async getLatestProducts() { + return await apiUtils.get('/api/rakuten/products/latest'); + } + + /** + * 导出并保存Excel文件到桌面(JavaFX专用) + * @param {Object} exportData - 导出数据 + */ + async exportAndSave(exportData) { + return await apiUtils.post('/api/rakuten/export-and-save', exportData); + } + + +} + +// 创建全局实例 +window.rakutenAPI = new RakutenAPI(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/api/shopee-api.js b/erp_client_sb/src/main/resources/static/js/api/shopee-api.js new file mode 100644 index 0000000..783a66a --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/shopee-api.js @@ -0,0 +1,130 @@ +/** + * Shopee平台API封装 + * 专门处理Shopee相关的所有接口调用 + */ +class ShopeeAPI { + + /** + * 获取广告托管数据 + * @param {Object} params - 查询参数 + */ + async getAdHosting(params = {}) { + return await apiUtils.get('/api/shopee/ad-hosting', params); + } + + /** + * 编辑广告托管 + * @param {Object} adData - 广告数据 + */ + async editAdHosting(adData) { + return await apiUtils.post('/api/shopee/ad-hosting/edit', adData); + } + + /** + * 删除广告托管 + * @param {String} id - 广告ID + */ + async deleteAdHosting(id) { + return await apiUtils.post('/api/shopee/ad-hosting/delete', { id }); + } + + /** + * 获取评价列表 + * @param {Object} params - 查询参数 + */ + async getReviews(params = {}) { + return await apiUtils.get('/api/shopee/reviews', params); + } + + /** + * 回复评价 + * @param {Object} replyData - 回复数据 + */ + async replyToReview(replyData) { + return await apiUtils.post('/api/shopee/review/reply', replyData); + } + + /** + * 删除评价 + * @param {String} id - 评价ID + */ + async deleteReview(id) { + return await apiUtils.post('/api/shopee/review/delete', { id }); + } + + /** + * 保存回评模板 + * @param {String} template - 模板内容 + */ + async saveReplyTemplate(template) { + return await apiUtils.post('/api/shopee/reply-template', { template }); + } + + /** + * 获取回评模板 + */ + async getReplyTemplate() { + return await apiUtils.get('/api/shopee/reply-template'); + } + + /** + * 获取闪购活动列表 + * @param {Object} params - 查询参数 + */ + async getFlashSales(params = {}) { + return await apiUtils.get('/api/shopee/flash-sales', params); + } + + /** + * 创建闪购活动 + * @param {Object} flashSaleData - 闪购数据 + */ + async createFlashSale(flashSaleData) { + return await apiUtils.post('/api/shopee/flash-sales/create', flashSaleData); + } + + /** + * 更新闪购活动 + * @param {Object} flashSaleData - 闪购数据 + */ + async updateFlashSale(flashSaleData) { + return await apiUtils.post('/api/shopee/flash-sales/update', flashSaleData); + } + + /** + * 删除闪购活动 + * @param {String} id - 闪购ID + */ + async deleteFlashSale(id) { + return await apiUtils.post('/api/shopee/flash-sales/delete', { id }); + } + + /** + * 获取商品管理列表 + * @param {Object} params - 查询参数 + */ + async getProductManagement(params = {}) { + return await apiUtils.get('/api/shopee/products', params); + } + + /** + * 批量更新商品 + * @param {Array} products - 商品列表 + */ + async batchUpdateProducts(products) { + return await apiUtils.post('/api/shopee/products/batch-update', { + products + }); + } + + /** + * 导出Shopee数据 + * @param {Object} exportParams - 导出参数 + */ + async exportData(exportParams) { + return await apiUtils.post('/api/shopee/export', exportParams); + } +} + +// 创建全局实例 +window.shopeeAPI = new ShopeeAPI(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/api/zebra-api.js b/erp_client_sb/src/main/resources/static/js/api/zebra-api.js new file mode 100644 index 0000000..9e4c59c --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/api/zebra-api.js @@ -0,0 +1,69 @@ +/** + * 斑马平台API封装 + * 专门处理斑马相关的所有接口调用 + */ +class ZebraAPI { + + /** + * 获取订单数据(分页) + * @param {Object} params - 查询参数 + */ + async getOrders(params = {}) { + return await apiUtils.get('/api/banma/orders', params); + } + + /** + * 根据批次ID获取订单数据 + * @param {String} batchId - 批次ID + */ + async getOrdersByBatch(batchId) { + return await apiUtils.get(`/api/banma/orders/batch/${batchId}`); + } + + /** + * 获取最新的订单数据 + */ + async getLatestOrders() { + return await apiUtils.get('/api/banma/orders/latest'); + } + + /** + * 获取店铺列表 + */ + async getShops() { + return await apiUtils.get('/api/banma/shops'); + } + + /** + * 刷新认证Token + */ + async refreshToken() { + return await apiUtils.post('/api/banma/refresh-token'); + } + + /** + * 导出订单数据到Excel + * @param {Object} exportData - 导出数据 + */ + async exportAndSaveOrders(exportData) { + return await apiUtils.post('/api/banma/export-and-save', exportData); + } + + /** + * 获取订单统计信息 + */ + async getOrderStats() { + return await apiUtils.get('/api/banma/orders/stats'); + } + + /** + * 搜索订单 + * @param {Object} searchParams - 搜索参数 + */ + async searchOrders(searchParams) { + return await apiUtils.get('/api/banma/orders/search', searchParams); + } +} + +// 创建全局实例 +window.zebraAPI = new ZebraAPI(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/erp-dashboard.js b/erp_client_sb/src/main/resources/static/js/erp-dashboard.js new file mode 100644 index 0000000..0be0f19 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/erp-dashboard.js @@ -0,0 +1,1938 @@ +// 预先定义数据,避免渲染时计算 +const featureCards = [ + { + id: 1, + title: '广告投放托管', + description: '广告广告自动化托管', + icon: 'el-icon-s-promotion' + }, + { + id: 2, + title: '自动回评', + description: '自动回复客户评价', + icon: 'el-icon-chat-dot-round' + }, + { + id: 3, + title: '限时特卖自动化', + description: '限时特卖自动化设置上下架', + icon: 'el-icon-time' + }, + { + id: 4, + title: '较长铺货商品', + description: '较长铺货商品管理', + icon: 'el-icon-goods' + } +]; + +const statsData = [ + { + label: '广告营销周期', + rows: [ + { type: '新品', value: '3天' }, + { type: '老品', value: '7天' } + ] + }, + { + label: '新品/老品养成进度', + rows: [ + { type: '养定期', value: '7天' }, + { type: '老品', value: '7天' } + ] + }, + { + label: '预算/营销/营销时间', + rows: [ + { type: '每日', value: '119时30分' } + ] + }, + { + label: 'ROI变更周期', + rows: [ + { type: '新品', value: '3天' }, + { type: '老品', value: '7天' } + ] + }, + { + label: 'ROI自动优化', + rows: [ + { type: '新品', value: '3' }, + { type: '老品', value: '5' } + ] + }, + { + label: '新品/营销防控预算', + rows: [ + { type: '新品', value: '3天' }, + { type: '老品', value: '7天' } + ] + } +]; + +const statusFilters = [ + { key: '全部商品', label: '全部商品' }, + { key: '进行中', label: '进行中' }, + { key: '已排序', label: '已排序' }, + { key: '暂停中', label: '暂停中' }, + { key: '已结束', label: '已结束' }, + { key: '托管中', label: '托管中' }, + { key: '已归档', label: '已归档' } +]; +const tableData = [ + { + id: 1, + title: '商品标题商品标题商品标题商品标题商品标题商品标题', + tags: ['广告进行中', '新品/老品'], + budget: { + current: 'NT$ 100', + target: 'NT$ 180' + }, + roi: { + target: '自由ROI', + current: '8' + }, + adSpend: 'NT$ 27.86', + period: { + duration: '2', + updateTime: '07-09 17:00' + }, + status: '进行中', + createTime: { + date: '月日分分', + time: '数据自动时间', + creator: '月日分分' + } + }, + { + id: 2, + title: '商品标题商品标题商品标题商品标题商品标题商品标题', + tags: ['广告进行中', '老品/老品'], + budget: { + current: 'NT$ 100', + target: 'NT$ 180' + }, + roi: { + target: '未有自动优化托', + current: '3.22 - 5.37' + }, + adSpend: 'NT$ 27.86', + period: { + duration: '2', + updateTime: '07-09 17:00' + }, + status: '暂停中', + createTime: { + date: '时间', + time: '活动结束时间', + creator: '月日分分' + } + }, + { + id: 3, + title: '商品标题商品标题商品标题商品标题商品标题商品标题', + tags: ['广告自己进', '新品/老品'], + budget: { + current: 'NT$ 100', + target: 'NT$ 180' + }, + roi: { + target: '自由ROI', + current: '5' + }, + adSpend: 'NT$ 27.86', + period: { + duration: '2', + updateTime: '07-09 17:00' + }, + status: '暂停中', + createTime: { + date: '月日分分', + time: '月日分分', + creator: '删除号' + } + }, + { + id: 4, + title: '商品标题商品标题商品标题商品标题商品标题商品标题', + tags: ['广告自己进', '新品/老品'], + budget: { + current: 'NT$ 100', + target: 'NT$ 180' + }, + roi: { + target: '自由ROI', + current: '5' + }, + adSpend: 'NT$ 27.86', + period: { + duration: '2', + updateTime: '07-09 17:00' + }, + status: '已归档', + createTime: { + date: '月日分分', + time: '月日分分', + creator: '月日分分' + } + } +]; + +const accounts = [ + { + id: 1, + name: 'iamsam165888', + online: true + }, + { + id: 2, + name: 'iamsam165888', + online: true + }, + { + id: 3, + name: 'iamsam165888', + online: false + }, + { + id: 4, + name: 'iamsam165888', + online: true + }, + { + id: 5, + name: 'iamsam165888', + online: false + } +]; + +// 预定义颜色数组 +const productColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']; + +// 菜单配置对象 - 统一管理所有平台信息 +const menuConfig = [ + { + key: 'rakuten', + name: 'Rakuten', + index: 'rakuten', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iI0JGMDAwMCIvPgo8dGV4dCB4PSIxMiIgeT0iMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5SPC90ZXh0Pgo8L3N2Zz4K' + }, + { + key: 'shopee', + name: '虾皮购物', + index: 'shopee', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iI0VFNEQyRCIvPgo8dGV4dCB4PSIxMiIgeT0iMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSI4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlM8L3RleHQ+Cjwvc3ZnPgo=' + }, + { + key: 'amazon', + name: 'amazon', + index: 'amazon', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iI0ZGOTkwMCIvPgo8dGV4dCB4PSIxMiIgeT0iMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSI4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkE8L3RleHQ+Cjwvc3ZnPgo=' + }, + { + key: 'zebra', + name: '斑马', + index: 'zebra', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iIzAwNTFBNSIvPgo8dGV4dCB4PSIxMiIgeT0iMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSI4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlo8L3RleHQ+Cjwvc3ZnPgo=' + } +]; + +// 立即创建Vue实例,不使用setTimeout +document.addEventListener('DOMContentLoaded', function() { + // 等待平台组件注册完成(若存在加载器) + if (window.loadSfcComponents && !window.platformComponentsReady) { + var start = Date.now(); + var check = function() { + if (window.platformComponentsReady || Date.now() - start > 5000) { + initVue(); + } else { + setTimeout(check, 30); + } + }; + return check(); + } + initVue(); + + function initVue() { + // Vue实例 + new Vue({ + el: '#app', + data() { + return { + // 认证相关 + isAuthenticated: false, + showAuthDialog: false, + authForm: { + username: '', + password: '' + }, + authLoading: false, + + // 注册相关 + showRegDialog: false, + registerForm: { + username: '', + password: '', + confirmPassword: '' + }, + registerLoading: false, + usernameCheckResult: null, + + // 用户权限配置 + userPermissions: null, + + // 当前激活的菜单项 + activeMenu: 'rakuten', + + // 面包屑导航 + breadcrumbs: { + main: 'ERP 管理系统', + sub: '', + page: '' + }, + + // 当前激活的状态筛选 + activeStatus: '进行中', + + // 搜索文本 + searchText: '', + + // 是否显示账号管理窗口 + showAccountWindow: true, + + // 分页相关 + currentPage: 1, + pageSize: 10, + total: 100, + + // 使用预先定义的数据 + featureCards, + statsData, + statusFilters, + tableData, + accounts, + menuConfig, + // 平台数据占位 + products: [], + productData: [], + orderData: [], + // 更新相关 + updateInfo: { + hasUpdate: false, + currentVersion: '', // 动态获取 + latestVersion: '', + downloadUrl: '', + updateNotes: '• 新增跨境电商数据分析功能\n• 优化订单管理流程\n• 修复已知Bug,提升系统稳定性\n• 增强用户权限管理\n• 改进界面交互体验', + updateTitle: '重要功能更新', + fileSize: '0.0 MB', + releaseDate: '', + appName: 'ERP客户端' + }, + showUpdateDialog: false, + checkingUpdate: false, + updateStage: 'check', // 'check', 'downloading', 'completed' + autoUpdateSelected: false, + postponedUpdate: null, // 存储稍后更新的信息 + downloadProgress: { + current: '0.0 MB', + total: '68.5 MB', + percentage: 0 + }, + + // 性能优化相关 + dataSaveTimeout: null, + tableLoading: false, + scrollTimeout: null, + + // 导航历史记录 + navigationHistory: ['rakuten'], + currentHistoryIndex: 0, + navigatingHistory: false + , + // 设备管理 + showDeviceDialog: false, + devices: [], + deviceQuota: { limit: 3, used: 0 }, + deviceLoading: false + } + }, + + computed: { + canRegister() { + return this.registerForm.username && + this.registerForm.password && + this.registerForm.confirmPassword && + this.registerForm.password.length >= 6 && + this.registerForm.password === this.registerForm.confirmPassword && + (this.usernameCheckResult === true || this.usernameCheckResult === null); + }, + // 分页处理的表格数据 + paginatedTableData() { + const start = (this.currentPage - 1) * this.pageSize; + const end = start + this.pageSize; + return this.tableData.slice(start, end); + } + }, + + mounted() { + // 建立SSE连接 + try { + const vm = this; + if (!this._sse) { + (async function connect() { + try { + const token = await vm.safeApiGet('token'); + if (!token) return; + + // 从JWT token中解析clientId + const clientId = await vm.getClientIdFromToken(); + if (!clientId) return; + + // 获取服务器配置 + const configResp = await fetch('/api/config/server'); + const config = await configResp.json(); + const sseUrl = config.sseUrl || 'http://192.168.1.89:8080/monitor/account/events'; + + const src = new EventSource(`${sseUrl}?clientId=${encodeURIComponent(clientId)}&token=${encodeURIComponent(token)}`); + vm._sse = src; + src.addEventListener('ready', () => console.log('SSE ready')); + src.addEventListener('ping', () => {}); + src.addEventListener('event', (e) => { + try { const payload = JSON.parse(e.data || '{}'); + if (payload.type === 'DEVICE_REMOVED' || payload.type === 'FORCE_LOGOUT') { + vm.logout(); + vm.$message && vm.$message.warning('会话已失效,请重新登录'); + } else if (payload.type === 'PERMISSIONS_UPDATED') { + vm.forceRefreshPermissions(); + } + } catch(err) {} + }); + src.onerror = () => { try { src.close(); } catch(e){} vm._sse=null; setTimeout(connect, 3000); }; + } catch(e) { + console.error('SSE连接失败:', e); + } + })(); + } + } catch(e) {} + // 全局Axios响应拦截:401 或 DEVICE_REMOVED 时强制登出并弹出登录 + try { + const vm = this; + if (!axios.__fxInterceptorInstalled) { + axios.interceptors.response.use(function (response) { + const data = response && response.data; + if (data && data.bizCode === 'DEVICE_REMOVED') { + vm.isAuthenticated = false; + vm.showAuthDialog = true; + vm.activeMenu = 'rakuten'; + try { vm.safeApiRemove && vm.safeApiRemove('token'); } catch (e) {} + vm.$message && vm.$message.warning('设备已被移除,请重新登录'); + } + return response; + }, function (error) { + const r = error && error.response; + const data = r && r.data ? r.data : {}; + if (r && r.status === 401) { + vm.isAuthenticated = false; + vm.showAuthDialog = true; + vm.activeMenu = 'rakuten'; + try { vm.safeApiRemove && vm.safeApiRemove('token'); } catch (e) {} + vm.$message && vm.$message.warning(data.msg || '登录已失效,请重新登录'); + } + return Promise.reject(error); + }); + axios.__fxInterceptorInstalled = true; + } + } catch (e) {} + // 页面加载时从localStorage恢复数据 + this.loadDataFromStorage(); + // 检查认证状态和获取最新权限 + this.checkAuth(); + // 初始化设备并开始心跳 + this.ensureCurrentDeviceId().then(() => { + this.startHeartbeatTimer(); + }); + // 检查URL参数并设置激活菜单 + this.handleUrlParameters(); + // 初始化导航历史 + this.initializeNavigationHistory(); + // 初始化面包屑 + this.updateBreadcrumbs(this.activeMenu); + + this.initializeVersion(); + // 然后检查更新 + this.checkForUpdates(); + }, + methods: { + // ============= 设备管理 ============= + async ensureCurrentDeviceId() { + try { + let deviceId = await this.safeApiGet('deviceId'); + if (!deviceId) { + if (window.javaConnector && window.javaConnector.getDeviceId) { + try { deviceId = window.javaConnector.getDeviceId(); } catch (e) {} + } + } + if (!deviceId) { + deviceId = 'fx-' + Math.random().toString(36).slice(2) + Date.now().toString(36); + } + await this.safeApiSet('deviceId', deviceId); + + const os = navigator.platform || 'unknown'; + // 若已登录,直接注册设备(后端会生成设备名称) + if (this.isAuthenticated) { + const regUsername = (this.authForm.username || (await this.safeApiGet('lastLoginUsername')) || '').trim(); + if (regUsername) { + await deviceAPI.register({ username: regUsername, deviceId, os, version: this.updateInfo.currentVersion || '' }); + } + } + } catch (e) { + console.warn('初始化设备失败', e); + } + }, + async openDeviceManager() { + this.showDeviceDialog = true; + await this.fetchDeviceData(); + }, + async fetchDeviceData() { + try { + this.deviceLoading = true; + let quota = { limit: 3, used: 0 }; + let list = []; + [quota, list] = await Promise.all([ + deviceAPI.getQuota(this.authForm.username), + deviceAPI.list(this.authForm.username) + ]); + this.deviceQuota = quota || { limit: 3, used: 0 }; + this.devices = Array.isArray(list) ? list : []; + // 标记本机 + const clientId = await this.getClientIdFromToken(); + this.devices = this.devices.map(d => ({ ...d, isCurrent: d.deviceId === clientId })); + } catch (e) { + this.$message.error('获取设备列表失败'); + } finally { + this.deviceLoading = false; + } + }, + + async confirmRemoveDevice(row) { + try { + await this.$confirm('确定要移除该设备吗?', '你确定要移除设备吗?', { + confirmButtonText: '确定移除', + cancelButtonText: '取消', + type: 'warning' + }); + await deviceAPI.remove({ deviceId: row.deviceId }); + this.devices = this.devices.filter(d => d.deviceId !== row.deviceId); + this.deviceQuota.used = Math.max(0, (this.deviceQuota.used || 0) - 1); + // 如果是本机,清理登录态 + const clientId = await this.getClientIdFromToken(); + if (row.deviceId === clientId) { + await this.logout(); + } + this.$message.success('已移除设备'); + } catch (e) { + if (e !== 'cancel') this.$message.error(apiUtils.getErrorMessage(e)); + } + }, + startHeartbeatTimer() { + if (this._hb) clearInterval(this._hb); + this._hb = setInterval(async () => { + try { + const clientId = await this.getClientIdFromToken(); + if (!clientId) return; + if (this.isAuthenticated) { + await deviceAPI.heartbeat({ username: this.authForm.username, deviceId: clientId, version: this.updateInfo.currentVersion || '' }); + } + } catch (e) {} + }, 60000); + }, + // =========== 设备管理结束 ========== + + // 简化权限检查方法 + hasPermission(module) { + // 未认证时显示基础平台菜单,隐藏高级功能 + if (!this.isAuthenticated) { + return ['rakuten', 'amazon', 'zebra'].includes(module); + } + + // 已认证时检查具体权限 + if (!this.userPermissions) return false; + try { + const permissions = JSON.parse(this.userPermissions); + return permissions[module] === true; + } catch (e) { + console.error('权限配置解析失败:', e); + return false; + } + }, + + // 检查功能访问权限 + checkFeatureAccess(feature) { + const hasAccess = this.hasPermission(feature); + if (!hasAccess) { + console.log('权限检查失败:', { + feature: feature, + userPermissions: this.userPermissions, + parsedPermissions: this.parsedPermissions, + hasAccess: hasAccess + }); + this.$message.warning('您没有访问此功能的权限'); + return false; + } + return true; + }, + + // 简化调试权限配置 + debugPermissions() {}, + + async initializeVersion() { + try { + const response = await axios.get('/api/update/version'); + if (response.data && response.data.success) { + this.updateInfo.currentVersion = response.data.currentVersion || '2.3.6'; + } else { + this.updateInfo.currentVersion = '2.3.6'; + } + } catch (error) { + console.warn('获取版本号失败,使用默认版本:', error); + this.updateInfo.currentVersion = '2.3.6'; + } + }, + + // 本地认证密钥存储API + async safeApiGet(key) { + try { + const response = await axios.get(`/api/cache/get?key=${encodeURIComponent(key)}`); + return response.data?.code === 0 ? response.data.data : null; + } catch (e) { + return null; + } + }, + async safeApiSet(key, value) { + try { + const response = await axios.post('/api/cache/save', { key, value }); + return response.data?.code === 0; + } catch (e) { + return false; + } + }, + + // 从JWT token中获取clientId + async getClientIdFromToken() { + try { + const token = await this.safeApiGet('token'); + if (!token) return null; + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.clientId || null; + } catch (e) { + return null; + } + }, + + // 清理所有缓存数据,重新登录 + async clearAllCacheAndRelogin() { + try { + // 清理所有缓存 + await this.safeApiRemove('token'); + await this.safeApiRemove('deviceId'); + await this.safeApiRemove('lastLoginUsername'); + // 重置状态 + this.isAuthenticated = false; + this.userPermissions = null; + this.showAuthDialog = true; + // 关闭SSE连接 + if (this._sse) { + try { this._sse.close(); } catch(e) {} + this._sse = null; + } + } catch (e) {} + }, + + // 检查认证状态和获取最新权限 + async checkAuth() { + // 从本地获取保存的token + const token = await this.safeApiGet('token'); + const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'selection', 'price-compare', 'data-collection']; + + if (token) { + // 有token,验证token有效性并获取权限 + await this.verifyToken(token); + } + + // 检查是否需要显示登录弹框:没有认证且在需要认证的菜单 + if (!this.isAuthenticated && authRequiredMenus.includes(this.activeMenu)) { + this.showAuthDialog = true; + } + }, + + // 验证token + async verifyToken(token) { + try { + const response = await axios.post('/api/verify', { + token: token + }); + + if (response.data.success === true) { + this.userPermissions = response.data.permissions; + this.isAuthenticated = true; + if (response.data.username) { + this.authForm.username = response.data.username; + try { await this.safeApiSet('lastLoginUsername', response.data.username); } catch (e) {} + } + } else { + this.isAuthenticated = false; + this.userPermissions = null; + // 清除本地保存的token + await this.safeApiRemove('token'); + } + } catch (error) { + console.error('Token验证失败:', error); + this.isAuthenticated = false; + this.userPermissions = null; + } + }, + + // 强制刷新权限配置(带用户反馈) + async forceRefreshPermissions() { + // 临时清空权限,触发重新渲染 + this.userPermissions = null; + + // 强制Vue重新渲染 + await this.$nextTick(); + + // 重新验证token + const token = await this.safeApiGet('token'); + if (token) { + await this.verifyToken(token); + } + + if (!this.userPermissions) { + this.$message.warning('权限配置刷新失败,请重新登录'); + } + }, + + // 登录对话框确认 + async handleAuth() { + if (!this.authForm.username || !this.authForm.password) { + this.$message.error('请输入用户名和密码'); + return; + } + + this.authLoading = true; + try { + const response = await axios.post('/api/login', { + username: this.authForm.username, + password: this.authForm.password + }); + + if (response.data.success === true) { + // 保存token到本地 + await this.safeApiSet('token', response.data.token); + this.userPermissions = response.data.permissions; + this.isAuthenticated = true; + // 持久化用户名 + try { await this.safeApiSet('lastLoginUsername', this.authForm.username); } catch (e) {} + // 登录成功后补登记当前设备 + try { + const clientId = await this.getClientIdFromToken(); + if (clientId) { + const os = navigator.platform || 'unknown'; + await deviceAPI.register({ username: this.authForm.username, deviceId: clientId, os, version: this.updateInfo.currentVersion || '' }); + } + } catch (e) {} + this.showAuthDialog = false; + + this.$message.success(response.data.message || '登录成功'); + } else { + this.$message.error(response.data.message || '登录失败'); + } + } catch (error) { + this.$message.error(error.message || '登录失败'); + } finally { + this.authLoading = false; + } + }, + // 简化取消登录 + cancelAuth() { + this.showAuthDialog = false; + this.authForm.username = ''; + this.authForm.password = ''; + }, + + // 注册相关方法 + showRegisterDialog() { + this.showAuthDialog = false; + this.showRegDialog = true; + this.resetRegisterForm(); + }, + + backToLogin() { + this.showRegDialog = false; + this.showAuthDialog = true; + this.resetRegisterForm(); + }, + + resetRegisterForm() { + this.registerForm = { + username: '', + password: '', + confirmPassword: '' + }; + this.usernameCheckResult = null; + }, + + async checkUsernameAvailability() { + if (!this.registerForm.username) { + this.usernameCheckResult = null; + return; + } + + try { + const response = await axios.get('/api/check-username', { + params: { username: this.registerForm.username } + }); + + if (response.data.code === 200) { + this.usernameCheckResult = response.data.data; + } + } catch (error) { + console.error('检查用户名失败:', error); + this.usernameCheckResult = false; + } + }, + + async handleRegister() { + // 验证表单 + if (!this.registerForm.username || + !this.registerForm.password || !this.registerForm.confirmPassword) { + this.$message.error('请填写所有必填字段'); + return; + } + + if (this.registerForm.password.length < 6) { + this.$message.error('密码长度不能少于6位'); + return; + } + + if (this.registerForm.password !== this.registerForm.confirmPassword) { + this.$message.error('两次输入密码不一致'); + return; + } + + if (this.usernameCheckResult === false) { + this.$message.error('用户名已存在,请更换'); + return; + } + + this.registerLoading = true; + + try { + const response = await axios.post('/api/register', { + username: this.registerForm.username, + password: this.registerForm.password + }); + + if (response.data.success) { + const loginResp = await axios.post('/api/login', { + username: this.registerForm.username, + password: this.registerForm.password + }); + if (loginResp.data.success === true) { + await this.safeApiSet('token', loginResp.data.token); + this.userPermissions = loginResp.data.permissions; + this.isAuthenticated = true; + // 登记设备 + try { + const clientId = await this.getClientIdFromToken(); + if (clientId) { + const os = navigator.platform || 'unknown'; + await deviceAPI.register({ username: this.registerForm.username, deviceId: clientId, os, version: this.updateInfo.currentVersion || '' }); + } + } catch (e) {} + this.showRegDialog = false; + this.showAuthDialog = false; + this.resetRegisterForm(); + } else { + this.$message.success('注册成功,请登录'); + this.showRegDialog = false; + this.showAuthDialog = true; + } + } else { + this.$message.error(response.data.message || '注册失败'); + } + } catch (error) { + console.error('注册失败:', error); + this.$message.error('注册失败:' + (error.response?.data?.message || error.message)); + } finally { + this.registerLoading = false; + } + }, + + cancelRegister() { + this.showRegDialog = false; + this.resetRegisterForm(); + }, + handleMenuSelect(key) { + // 检查是否需要认证的功能 + const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee', 'selection', 'price-compare', 'data-collection', 'toolbox']; + if (authRequiredMenus.includes(key) && !this.isAuthenticated) { + this.$message.warning('该功能需要认证后才能使用,请先认证'); + this.showAuthDialog = true; + return; + } + + // 检查权限 + if (authRequiredMenus.includes(key) && this.isAuthenticated) { + if (!this.checkFeatureAccess(key)) { + return; // 权限不足,阻止菜单切换 + } + } + + if (!this.navigatingHistory) { + // 移除当前位置之后的所有历史记录 + this.navigationHistory = this.navigationHistory.slice(0, this.currentHistoryIndex + 1); + // 添加新的菜单项(如果不是当前菜单) + if (key !== this.activeMenu) { + this.navigationHistory.push(key); + this.currentHistoryIndex = this.navigationHistory.length - 1; + } + } + this.activeMenu = key; + // 更新面包屑 + this.updateBreadcrumbs(key); + }, + + mainTableCellStyle({ row, column, rowIndex, columnIndex }) { + return { + padding: '8px 10px', + height: '42px', + 'line-height': '26px' + }; + }, + // 主表格表头样式 + mainTableHeaderCellStyle({ row, column, rowIndex, columnIndex }) { + return { + padding: '10px', + height: '40px', + 'background-color': '#f5f7fa', + 'font-weight': '500' + }; + }, + + // 更新面包屑导航 + updateBreadcrumbs(menuKey) { + const breadcrumbMap = { + 'dashboard': { + main: 'ERP 管理系统', + sub: '系统首页', + page: '欢迎页面' + }, + 'toolbox': { + main: 'ERP 管理系统', + sub: '工具箱', + page: '跨境电商工具' + }, + 'rakuten': { + main: 'ERP 管理系统', + sub: '电商平台', + page: 'Rakuten 管理' + }, + 'shopee': { + main: 'ERP 管理系统', + sub: '电商平台', + page: '虾皮购物管理' + }, + 'amazon': { + main: 'ERP 管理系统', + sub: '电商平台', + page: 'Amazon 管理' + }, + 'zebra': { + main: 'ERP 管理系统', + sub: '电商平台', + page: '斑马平台管理' + }, + 'selection': { + main: 'ERP 管理系统', + sub: '电商工具', + page: '跨境选品' + }, + 'price-compare': { + main: 'ERP 管理系统', + sub: '电商工具', + page: '1688比价' + }, + 'data-collection': { + main: 'ERP 管理系统', + sub: '电商工具', + page: '数据采集' + }, + 'tools': { + main: 'ERP 管理系统', + sub: '电商工具', + page: '全部工具' + } + }; + + this.breadcrumbs = breadcrumbMap[menuKey] || { + main: 'ERP 管理系统', + sub: '', + page: '' + }; + }, + + openShopeePage() { + console.log('切换到虾皮平台组件'); + this.activeMenu = 'shopee'; + }, + // 图片加载失败 + handleImageError(row) { + if (!row) return; + row.productImage = ''; + row.imageUrl = ''; + }, + + // 复制到剪贴板 + async copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text || ''); + this.$message.success('已复制'); + } catch (e) { + this.$message.error('复制失败'); + } + }, + + // 打开链接 + openLink(url) { + if (!url) return; + window.open(url, '_blank'); + }, + + // 识图搜索(占位) + search1688ByImage(row) { + if (!row) return; + this.$set(row, 'searching1688', true); + setTimeout(() => { + this.$set(row, 'searching1688', false); + this.$set(row, 'image1688Url', row.productUrl || ''); + }, 1000); + }, + + // 价格格式化 + formatJpy(val) { + const n = Number(val) || 0; + return `¥${n}`; + }, + formatCny(val) { + const n = Number(val) || 0; + return `¥${n}`; + }, + + // 设置激活状态 + setActiveStatus(status) { + this.activeStatus = status; + }, + + // 获取标签类型 + getTagType(tag) { + if (tag.includes('进行中')) return 'success'; + if (tag.includes('新品')) return 'warning'; + if (tag.includes('老品')) return 'info'; + return ''; + }, + + // 获取状态样式类 + getStatusClass(status) { + switch(status) { + case '进行中': return 'running'; + case '暂停中': return 'paused'; + case '已归档': return 'stopped'; + default: return 'stopped'; + } + }, + + // 获取商品图片背景颜色 + getProductColor(id) { + return productColors[(id - 1) % productColors.length]; + }, + + // 分页大小改变 + handleSizeChange(val) { + this.tableLoading = true; + this.pageSize = val; + this.currentPage = 1; + // 使用 nextTick 等待 DOM 更新完成 + this.$nextTick(() => { + setTimeout(() => { + this.tableLoading = false; + }, 100); + }); + }, + + // 当前页改变 + handleCurrentChange(val) { + this.tableLoading = true; + this.currentPage = val; + this.$nextTick(() => { + setTimeout(() => { + this.tableLoading = false; + }, 50); + }); + }, + + // 表格滚动事件处理 + handleTableScroll(e) { + // 防抖处理滚动事件 + if (this.scrollTimeout) { + clearTimeout(this.scrollTimeout); + } + this.scrollTimeout = setTimeout(() => { + // 在这里可以处理虚拟滚动逻辑 + }, 16); // 60FPS + }, + + // 导航按钮 + goBack() { + if (this.currentHistoryIndex > 0) { + this.navigatingHistory = true; + this.currentHistoryIndex--; + const targetMenu = this.navigationHistory[this.currentHistoryIndex]; + this.handleMenuSelect(targetMenu); + this.$nextTick(() => { + this.navigatingHistory = false; + }); + } + }, + goForward() { + if (this.currentHistoryIndex < this.navigationHistory.length - 1) { + this.navigatingHistory = true; + this.currentHistoryIndex++; + const targetMenu = this.navigationHistory[this.currentHistoryIndex]; + this.handleMenuSelect(targetMenu); + this.$nextTick(() => { + this.navigatingHistory = false; + }); + } + }, + + // 刷新 + async refresh() { + // 刷新权限配置和页面数据 + await this.forceRefreshPermissions(); + }, + + // 打开设置 + openSettings() { + if (!this.isAuthenticated) { + this.showAuthDialog = true; + return; + } + this.$confirm('请选择操作', '系统设置', { + confirmButtonText: '检查更新', + cancelButtonText: '退出登录', + type: 'info', + distinguishCancelAndClose: true + }).then(() => { + // 检查更新 + this.manualCheckUpdate(); + }).catch((action) => { + if (action === 'cancel') { + // 退出登录 + this.$confirm('确定要退出登录吗?', '退出确认', { + confirmButtonText: '退出', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.logout(); + }); + } + }); + }, + + // 退出登录 + async logout() { + try { + await axios.post('/api/cache/delete?key=token'); + } catch (e) { + console.warn('删除后端token缓存失败:', e); + } + this.isAuthenticated = false; + this.authForm.username = ''; + this.authForm.password = ''; + this.userPermissions = null; + this.showAuthDialog = true; + this.$message.success('已退出登录'); + }, + + // VIP按钮点击 + handleVipClick() { + this.$message.warning('VIP功能开发中'); + }, + + // 账号管理窗口操作 + closeAccountWindow() { + this.showAccountWindow = false; + }, + + minimizeWindow() { + this.$message.info('窗口已最小化'); + }, + + addAccount() { + this.$message.success('添加账号功能开发中'); + }, + + manageAccounts() { + this.$message.info('账号管理功能开发中'); + }, + + // 数据持久化方法(已由各组件直接调用后端API,这里保留空实现) + saveDataToStorage() { + // 数据已经由各平台组件自动保存到后端,这里不需要操作 + }, + + loadDataFromStorage() { + // 数据已经由各平台组件自动从后端加载,这里不需要操作 + }, + + clearStorageData() { + // 数据已经在后端管理,这里不需要操作 + }, + + // 组件数据更新事件处理(防抖优化) + handleProductsUpdated(products) { + this.products = products; + this.debounceDataSave(); + }, + + handleProductDataUpdated(productData) { + this.productData = productData; + this.debounceDataSave(); + }, + + handleOrderDataUpdated(orderData) { + this.orderData = orderData; + this.debounceDataSave(); + }, + + // 防抖保存数据 + debounceDataSave() { + if (this.dataSaveTimeout) { + clearTimeout(this.dataSaveTimeout); + } + this.dataSaveTimeout = setTimeout(() => { + this.saveDataToStorage(); + }, 500); // 500ms 防抖 + }, + + // 初始化导航历史 + initializeNavigationHistory() { + // 将当前激活的菜单设置为历史记录的起点 + this.navigationHistory = [this.activeMenu]; + this.currentHistoryIndex = 0; + }, + + // 处理URL参数,用于从其他页面返回时设置激活菜单 + handleUrlParameters() { + try { + const urlParams = new URLSearchParams(window.location.search); + const activeMenu = urlParams.get('activeMenu'); + + if (activeMenu && ['toolbox', 'rakuten', 'amazon', 'zebra', 'selection', 'price-compare', 'data-collection', 'tools'].includes(activeMenu)) { + this.activeMenu = activeMenu; + + // 清除URL参数,避免刷新时重复处理 + const url = new URL(window.location); + url.searchParams.delete('activeMenu'); + window.history.replaceState({}, document.title, url.pathname); + } + } catch (error) { + console.warn('处理URL参数失败:', error); + } + }, + + // 检查更新 + async performUpdateCheck() { + this.checkingUpdate = true; + try { + const response = await axios.get('/api/update/check'); + if (response.data.success) { + const needUpdate = response.data.needUpdate || false; + const skippedVersion = response.data.skippedVersion; + const latestVersion = response.data.latestVersion || ''; + + // 如果需要更新但版本被跳过,则不显示更新提示 + const shouldShowUpdate = needUpdate && (!skippedVersion || skippedVersion !== latestVersion); + this.updateInfo = { + hasUpdate: shouldShowUpdate, + currentVersion: response.data.currentVersion, + latestVersion: latestVersion, + downloadUrl: response.data.downloadUrl || '', + updateNotes: response.data.updateNotes || '', + fileSize: response.data.fileSize || '', + releaseDate: response.data.releaseDate || '' + }; + + return { + success: true, + hasUpdate: this.updateInfo.hasUpdate + }; + } else { + return { + success: false, + message: response.data.message || '检查更新失败' + }; + } + } catch (error) { + console.error('检查更新失败:', error); + return { + success: false, + message: '检查更新失败:' + error.message + }; + } finally { + this.checkingUpdate = false; + } + }, + + // 检查稍后更新的内容 + async checkPostponedUpdate() { + try { + const postponedUpdateStr = await this.safeApiGet('postponedUpdate'); + if (postponedUpdateStr) { + const postponedInfo = JSON.parse(postponedUpdateStr); + this.postponedUpdate = postponedInfo; + + // 恢复更新信息到updateInfo + this.updateInfo.hasUpdate = true; + this.updateInfo.latestVersion = postponedInfo.version; + this.updateInfo.downloadUrl = postponedInfo.downloadUrl; + this.updateInfo.fileSize = postponedInfo.fileSize; + this.updateInfo.updateNotes = postponedInfo.updateNotes; + this.updateInfo.updateTitle = postponedInfo.updateTitle; + this.updateInfo.appName = postponedInfo.appName; + + // 恢复下载状态 + if (postponedInfo.downloadCompleted) { + // 如果标记为下载完成,先验证文件是否真的存在 + this.verifyAndRestoreDownloadStatus(postponedInfo); + } else if (postponedInfo.downloadProgress) { + this.updateStage = 'downloading'; + this.downloadProgress = postponedInfo.downloadProgress; + } + + const postponedDate = new Date(postponedInfo.postponedAt); + const today = new Date(); + const daysDiff = Math.floor((today - postponedDate) / (1000 * 60 * 60 * 24)); + + if (daysDiff >= 1) { + setTimeout(() => { + // 根据下载状态决定显示阶段 + if (!postponedInfo.downloadCompleted) { + this.updateStage = 'check'; // 如果下载未完成,回到检查阶段让用户重新开始 + } // 如果已下载完成,保持completed状态 + + this.showUpdateDialog = true; + const message = postponedInfo.downloadCompleted + ? '您有一个已下载的更新,建议立即安装' + : '您有一个稍后更新的项目,建议立即更新'; + this.$message.info(message); + }, 2000); // 延迟2秒显示,避免启动时过于突兀 + } + } + } catch (error) { + console.error('检查稍后更新失败:', error); + // 清除无效的数据 + this.safeLocalStorageRemove('postponedUpdate'); + } + }, + + // 验证并恢复下载状态 + async verifyAndRestoreDownloadStatus(postponedInfo) { + try { + const response = await fetch('/api/update/verify-file', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success && data.fileExists) { + // 文件存在,恢复完成状态 + this.updateStage = 'completed'; + this.downloadProgress = { + current: postponedInfo.fileSize || '68.5 MB', + total: postponedInfo.fileSize || '68.5 MB', + percentage: 100, + speed: '下载完成' + }; + } else { + // 文件不存在,重置为检查状态 + this.updateStage = 'check'; + this.postponedUpdate.downloadCompleted = false; + this.safeLocalStorageSet('postponedUpdate', JSON.stringify(this.postponedUpdate)); + console.log('稍后更新文件不存在,已重置下载状态'); + } + } catch (error) { + console.error('验证稍后更新文件失败:', error); + // 验证失败,重置为检查状态 + this.updateStage = 'check'; + if (this.postponedUpdate) { + this.postponedUpdate.downloadCompleted = false; + this.safeLocalStorageSet('postponedUpdate', JSON.stringify(this.postponedUpdate)); + } + } + }, + + // 自动检查更新(页面加载时) + async checkForUpdates() { + // 首先检查是否有稍后更新的内容 + await this.checkPostponedUpdate(); + + // 检查是否在1秒内已经检查过更新 + const lastCheckTime = this.safeLocalStorageGet('updateCheckTime'); + const now = Date.now(); + + if (lastCheckTime && (now - parseInt(lastCheckTime)) < 1000) { + console.log('1秒内已检查过更新,跳过自动检查'); + return; + } + + const result = await this.performUpdateCheck(); + + if (result.success) { + // 记录检查时间 + this.safeLocalStorageSet('updateCheckTime', now.toString()); + // 如果有更新,检查是否已跳过该版本 + if (result.hasUpdate) { + // 页面加载时自动弹框显示更新提示 + this.updateStage = 'check'; // 重置更新阶段 + this.showUpdateDialog = true; + } else { + console.log('当前已是最新版本'); + } + } else { + console.warn('自动更新检查失败:', result.message); + } + }, + + // 手动检查更新 + async manualCheckUpdate() { + this.checkingUpdate = true; + try { + const response = await commonAPI.checkVersion(); + const needUpdate = response.needUpdate || false; + const latestVersion = response.latestVersion || ''; + + // 手动检查时更新完整信息(忽略跳过状态) + this.updateInfo = { + hasUpdate: needUpdate, + currentVersion: response.currentVersion, + latestVersion: latestVersion, + downloadUrl: response.downloadUrl || '', + updateNotes: response.updateNotes || '', + fileSize: response.fileSize || '', + releaseDate: response.releaseDate || '' + }; + + if (needUpdate) { + this.updateStage = 'check'; + this.showUpdateDialog = true; + this.$message.success('发现新版本!'); + } else { + this.$message.success('当前已是最新版本'); + } + } catch (error) { + apiUtils.showError(error); + } finally { + this.checkingUpdate = false; + } + }, + + // 立即更新 + async handleUpdate() { + const downloadUrl = this.updateInfo.downloadUrl || 'https://qiniu.pxdj.tashowz.com/2025/08/d93db3c01cae4cb196cca5e91df1127c.exe'; + + try { + const result = await this.$confirm(` +
+

新版本:${this.updateInfo.latestVersion}

+

当前版本:${this.updateInfo.currentVersion}

+ ${this.updateInfo.fileSize ? `

文件大小:${this.updateInfo.fileSize}

` : ''} + ${this.updateInfo.releaseDate ? `

发布时间:${this.updateInfo.releaseDate}

` : ''} + ${this.updateInfo.updateNotes ? `

更新内容:
${this.updateInfo.updateNotes}

` : ''} +

注意:系统将自动下载并重启更新

+
+ `, '确认自动更新', { + confirmButtonText: '立即更新', + cancelButtonText: '稍后更新', + type: 'warning', + dangerouslyUseHTMLString: true, + customClass: 'update-confirm-dialog' + }); + + // 用户确认更新 + this.showUpdateDialog = false; + + // 显示更新进度 + const loading = this.$loading({ + lock: true, + text: '正在下载更新文件,请稍候...', + spinner: 'el-icon-loading', + background: 'rgba(0, 0, 0, 0.7)' + }); + + try { + // 调用自动更新接口 + const response = await fetch('/api/update/auto-update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + downloadUrl: downloadUrl + }) + }); + const data = await response.json(); + + if (data.success) { + // 更新加载文本 + loading.text = '下载完成!正在准备更新...'; + + // 2秒后显示即将重启的消息 + setTimeout(() => { + loading.close(); + this.$message({ + message: '更新完成!程序将自动重启,请稍候...', + type: 'success', + duration: 5000, + showClose: false + }); + }, 2000); + } else { + loading.close(); + this.$message.error('更新失败:' + data.message); + } + + } catch (updateError) { + loading.close(); + console.error('调用更新接口失败:', updateError); + this.$message.error('更新失败:' + updateError.message); + } + + } catch (error) { + // 用户取消了更新 + if (error !== 'cancel') { + console.error('处理更新失败:', error); + this.$message.error('处理更新失败'); + } + } + }, + + // 稍后提醒 + remindLater() { + this.showUpdateDialog = false; + // 1秒后重新检查 + this.safeLocalStorageRemove('updateCheckTime'); + }, + // 跳过这个版本 + skipUpdate() { + const skippedVersion = this.updateInfo.latestVersion; + if (skippedVersion) { + httpClient.post('/api/update/skip-version', { version: skippedVersion }); + } + this.showUpdateDialog = false; + }, + handleDialogClose() { + if (this.updateStage === 'check') { + this.showUpdateDialog = false; + } else if (this.updateStage === 'downloading') { + this.cancelDownloadAndClose(); + } else if (this.updateStage === 'completed') { + // 在完成阶段,直接关闭对话框不弹确认 + this.showUpdateDialog = false; + } else { + // 其他情况直接关闭 + this.showUpdateDialog = false; + } + }, + async startUpdate() { + this.safeLocalStorageRemove('skippedUpdateVersion'); + + // 检查是否有稍后更新且已下载完成 + if (this.postponedUpdate && this.postponedUpdate.downloadCompleted) { + // 验证后端文件是否真的存在 + try { + const response = await fetch('/api/update/verify-file'); + const data = await response.json(); + + if (data.success && data.fileExists) { + // 文件确实存在,直接跳到安装阶段 + this.updateStage = 'completed'; + this.downloadProgress = { + current: this.updateInfo.fileSize || '68.5 MB', + total: this.updateInfo.fileSize || '68.5 MB', + percentage: 100, + speed: '下载完成' + }; + this.$message.success('检测到已下载的更新文件,可直接安装'); + return; + } else { + // 文件不存在,只清除前端记录,不删除后端文件(因为本来就不存在) + console.log('更新文件不存在,清除稍后更新记录'); + this.safeLocalStorageRemove('postponedUpdate'); + this.postponedUpdate = null; + this.$message.warning('更新文件已丢失,将重新下载'); + } + } catch (error) { + console.error('验证更新文件失败:', error); + // 验证失败只清除前端记录 + this.safeLocalStorageRemove('postponedUpdate'); + this.postponedUpdate = null; + this.$message.warning('无法验证更新文件,将重新下载'); + } + } + + // 开始新的下载 + this.updateStage = 'downloading'; + this.downloadProgress = { + current: '0.0 MB', + total: this.updateInfo.fileSize || '68.5 MB', + percentage: 0, + speed: '0 KB/s' + }; + this.startActualDownload(); + }, + + // 取消下载并关闭对话框 + async cancelDownloadAndClose() { + try { + // 清理进度定时器 + if (this.progressTimer) { + clearInterval(this.progressTimer); + this.progressTimer = null; + } + + await commonAPI.cancelDownload(); + this.showUpdateDialog = false; + this.updateStage = 'check'; // 重置到检查阶段 + + } catch (error) { + this.showUpdateDialog = false; + this.updateStage = 'check'; // 重置到检查阶段 + apiUtils.showError(error); + } + }, + + // 稍后更新(下载完成后) + async cancelUpdate() { + try { + console.log('点击稍后更新,当前阶段:', this.updateStage); + + // 如果当前是已完成状态,验证文件是否真的存在 + let actuallyCompleted = this.updateStage === 'completed'; + if (actuallyCompleted) { + const fileExists = await this.verifyDownloadedFile(); + actuallyCompleted = fileExists; + console.log('稍后更新时验证文件存在性:', fileExists); + + if (!fileExists) { + this.$message.warning('检测到下载文件已丢失,状态已重置'); + } + } + + // 保存稍后更新的信息,包含当前下载状态 + const postponedInfo = { + version: this.updateInfo.latestVersion, + downloadUrl: this.updateInfo.downloadUrl, + fileSize: this.updateInfo.fileSize, + updateNotes: this.updateInfo.updateNotes, + updateTitle: this.updateInfo.updateTitle, + postponedAt: new Date().toISOString(), + appName: this.updateInfo.appName, + downloadCompleted: actuallyCompleted, // 使用验证后的状态 + downloadProgress: this.updateStage === 'downloading' ? this.downloadProgress : null + }; + + console.log('保存的稍后更新信息:', postponedInfo); + this.safeLocalStorageSet('postponedUpdate', JSON.stringify(postponedInfo)); + this.postponedUpdate = postponedInfo; + + const data = await commonAPI.cancelDownload(); + this.showUpdateDialog = false; + + // 更新版本显示状态 + this.updateInfo.hasUpdate = true; + + } catch (error) { + console.error('稍后更新设置失败:', error); + this.showUpdateDialog = false; + } + }, + + // 安装更新 + installUpdate() { + this.$confirm('安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?', '确认安装', { + confirmButtonText: '立即安装', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + // 清除稍后更新记录 + this.safeLocalStorageRemove('postponedUpdate'); + this.postponedUpdate = null; + this.performActualUpdate(); + }).catch(() => { + // 用户取消了安装 + }); + }, + + // 清除稍后更新记录 + async clearPostponedUpdate(showMessage = true, clearBackendFile = true) { + this.safeLocalStorageRemove('postponedUpdate'); + this.postponedUpdate = null; + this.updateInfo.hasUpdate = false; + + // 只有在明确需要时才清除后端文件 + if (clearBackendFile) { + try { + await fetch('/api/update/clear', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('清除后端更新文件失败:', error); + } + } + + if (showMessage) { + this.$message.success('稍后更新记录已清除'); + } + }, + + // 开始实际下载 + async startActualDownload() { + const downloadUrl = this.updateInfo.downloadUrl || 'https://update.code.visualstudio.com/latest/win32-x64-user/stable'; + + try { + await commonAPI.autoUpdate({ downloadUrl }); + console.log('下载已启动'); + this.trackDownloadProgress(); + } catch (error) { + console.error('启动下载失败:', error); + apiUtils.showError(error); + this.showUpdateDialog = false; + } + }, + + // 跟踪下载进度 + trackDownloadProgress() { + let progressFailCount = 0; + const maxProgressFails = 10; // 最多允许10次获取进度失败 + + const progressInterval = setInterval(() => { + fetch('/api/update/progress') + .then(response => response.json()) + .then(data => { + progressFailCount = 0; // 重置失败计数 + + if (data.success) { + // 使用服务器返回的真实数据 + this.downloadProgress.percentage = data.progress || 0; + this.downloadProgress.current = data.downloadedMB ? `${data.downloadedMB} MB` : '0.0 MB'; + this.downloadProgress.total = data.totalMB ? `${data.totalMB} MB` : '68.5 MB'; + this.downloadProgress.speed = data.downloadSpeed || '0 KB/s'; + + console.log(`下载进度: ${data.progress}%, 已下载: ${data.downloadedMB}MB, 总大小: ${data.totalMB}MB, 速度: ${data.downloadSpeed}`); + + // 实时保存下载进度到稍后更新信息 + this.saveDownloadProgressToPostponed(); + + if (data.status === 'completed') { + this.downloadProgress.percentage = 100; + this.downloadProgress.speed = '完成'; + this.updateStage = 'completed'; + + console.log('下载完成,文件路径:', data.filePath); + console.log('下载完成,文件大小:', data.fileSize); + + // 立即验证下载的文件是否存在 + this.verifyDownloadedFile().then(fileExists => { + if (fileExists) { + console.log('下载完成后验证:文件存在'); + // 标记下载完成 + if (this.postponedUpdate) { + this.postponedUpdate.downloadCompleted = true; + this.postponedUpdate.filePath = data.filePath; // 保存文件路径 + this.safeLocalStorageSet('postponedUpdate', JSON.stringify(this.postponedUpdate)); + } + } else { + console.error('下载完成后验证:文件不存在!这可能是一个问题'); + this.$message.error('下载完成但文件验证失败,请重试'); + this.updateStage = 'check'; + } + }); + + clearInterval(progressInterval); + } else if (data.status === 'failed') { + this.$message.error('下载失败,请稍后重试'); + this.updateStage = 'check'; // 回到检查阶段允许重试 + clearInterval(progressInterval); + } else if (data.status === 'cancelled') { + this.$message.warning('下载已取消'); + this.updateStage = 'check'; // 回到检查阶段允许重试 + clearInterval(progressInterval); + } + } + }) + .catch(error => { + progressFailCount++; + console.error('获取进度失败:', error, '失败次数:', progressFailCount); + + if (progressFailCount >= maxProgressFails) { + this.$message.error('下载连接中断,请检查网络后重试'); + this.updateStage = 'check'; // 回到检查阶段允许重试 + clearInterval(progressInterval); + } + }); + }, 1000); // 改为每1秒查询一次,减少服务器压力 + + // 保存定时器ID以便取消 + this.progressTimer = progressInterval; + }, + + // 验证刚下载的文件是否存在 + async verifyDownloadedFile() { + try { + const response = await fetch('/api/update/verify-file', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + console.log('文件验证结果:', data); + + return data.success && data.fileExists; + } catch (error) { + console.error('验证下载文件失败:', error); + return false; + } + }, + + // 实时保存下载进度到稍后更新信息 + saveDownloadProgressToPostponed() { + if (this.postponedUpdate) { + this.postponedUpdate.downloadProgress = { ...this.downloadProgress }; + this.postponedUpdate.downloadCompleted = this.updateStage === 'completed'; + this.safeLocalStorageSet('postponedUpdate', JSON.stringify(this.postponedUpdate)); + } + }, + + // 模拟下载进度(备用方案) + simulateDownloadProgress() { + const totalSize = parseFloat(this.downloadProgress.total.replace(/[^\d.]/g, '')); + let currentSize = 0; + const step = totalSize / 200; // 减少步长,让进度更稳定 + + const progressInterval = setInterval(() => { + // 确保进度只增不减 + const increment = step * (0.5 + Math.random() * 1.5); // 0.5-2倍的随机增长 + currentSize = Math.min(currentSize + increment, totalSize); + + this.downloadProgress.current = `${currentSize.toFixed(1)} MB`; + this.downloadProgress.percentage = Math.round((currentSize / totalSize) * 100); + + if (currentSize >= totalSize) { + this.downloadProgress.current = `${totalSize.toFixed(1)} MB`; + this.downloadProgress.percentage = 100; + this.updateStage = 'completed'; + clearInterval(progressInterval); + } + }, 150); + }, + + // 执行实际更新 + async performActualUpdate() { + // 显示安装进度 + const loading = this.$loading({ + lock: true, + text: '正在安装更新,请稍候...', + spinner: 'el-icon-loading', + background: 'rgba(0, 0, 0, 0.7)' + }); + + try { + // 调用后端安装接口 + const response = await fetch('/api/update/install', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + loading.text = '更新完成!程序将自动重启...'; + + setTimeout(() => { + loading.close(); + this.$message({ + message: '更新完成!程序将自动重启,请稍候...', + type: 'success', + duration: 5000, + showClose: false + }); + + // 关闭更新对话框 + this.showUpdateDialog = false; + }, 2000); + } else { + loading.close(); + this.$message.error('安装失败:' + (data.message || '未知错误')); + } + + } catch (error) { + loading.close(); + apiUtils.showError(error); + console.error('安装更新失败:', error); + } + }, + + // 安全的localStorage操作函数 + safeLocalStorageGet(key) { + try { + return localStorage.getItem(key); + } catch (error) { + console.warn('LocalStorage读取失败:', key, error); + return null; + } + }, + + safeLocalStorageSet(key, value) { + try { + localStorage.setItem(key, value); + return true; + } catch (error) { + console.warn('LocalStorage写入失败:', key, error); + return false; + } + }, + + safeLocalStorageRemove(key) { + try { + localStorage.removeItem(key); + return true; + } catch (error) { + console.warn('LocalStorage删除失败:', key, error); + return false; + } + } + } + }); + } +}); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/http-client.js b/erp_client_sb/src/main/resources/static/js/http-client.js new file mode 100644 index 0000000..c2ca111 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/http-client.js @@ -0,0 +1,61 @@ +/** + * HTTP Client Utility - Axios wrapper with error handling and logging + */ +(function() { + // Create axios instance with default config + const httpClient = axios.create({ + baseURL: 'http://localhost:8081', // Set the base URL for all requests + timeout: 0, // 无限时间 - 永不超时 + // 针对JavaFX WebView的特殊配置 + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + }, + // 对于文件上传,禁用自动设置Content-Type + transformRequest: [function (data, headers) { + // 如果是FormData,让浏览器自动设置Content-Type + if (data instanceof FormData) { + delete headers['Content-Type']; + return data; + } + return axios.defaults.transformRequest[0](data, headers); + }] + }); + + // Request interceptor for logging + httpClient.interceptors.request.use( + config => { + + console.log(`🚀 REQUEST: ${config.method.toUpperCase()} ${config.baseURL}${config.url}`, config); + return config; + }, + error => { + console.error('❌ Request Error:', error); + return Promise.reject(error); + } + ); + + // Response interceptor for logging + httpClient.interceptors.response.use( + response => { + console.log(`✅ RESPONSE: ${response.config.method.toUpperCase()} ${response.config.url}`, response.data); + return response; + }, + error => { + if (error.response) { + // Server responded with a status code outside of 2xx range + console.error(`❌ Response Error: ${error.response.status}`, error.response.data); + } else if (error.request) { + // Request was made but no response was received + console.error('❌ Network Error: No response received', error.request); + } else { + // Something happened in setting up the request + console.error('❌ Request Config Error:', error.message); + } + return Promise.reject(error); + } + ); + + // Export the httpClient to global scope + window.httpClient = httpClient; +})(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/services/data-service.js b/erp_client_sb/src/main/resources/static/js/services/data-service.js new file mode 100644 index 0000000..f6ae3d0 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/services/data-service.js @@ -0,0 +1,282 @@ +/** + * 数据处理服务 + * 统一处理数据格式化、验证、转换等功能 + */ +class DataService { + + /** + * 格式化产品数据 + * @param {Array} products - 原始产品数据 + * @param {String} platform - 平台类型 + * @returns {Array} 格式化后的产品数据 + */ + // 性能优化:直接返回结果,减少中间变量 + formatProductData(products, platform = 'amazon') { + if (!Array.isArray(products)) return []; + + const formatters = { + 'amazon': this.formatAmazonProduct.bind(this), + 'rakuten': this.formatRakutenProduct.bind(this), + 'shopee': this.formatShopeeProduct.bind(this) + }; + + const formatter = formatters[platform]; + return formatter ? products.map(formatter) : products; + } + + /** + * 格式化亚马逊产品数据 + * @param {Object} product - 产品数据 + */ + formatAmazonProduct(product) { + return { + ...product, + asin: product.asin || '无数据', + seller: product.seller || '无货', + shipper: product.shipper || '', + price: product.price || '无货', + title: product.title || '无标题', + createTime: this.formatTime(product.createTime), + updateTime: this.formatTime(product.updateTime) + }; + } + + /** + * 格式化乐天产品数据 + * @param {Object} product - 产品数据 + */ + formatRakutenProduct(product) { + return { + ...product, + productUrl: product.productUrl || '', + imgUrl: product.imgUrl || '', + productTitle: product.productTitle || '无标题', + price: product.price || '无价格', + ranking: product.ranking || '无排名', + shopName: product.shopName || product.originalShopName || '无店铺', + image1688Url: product.image1688Url || '', + detailUrl1688: product.detailUrl1688 || '', + }; + } + + /** + * 格式化Shopee产品数据 + * @param {Object} product - 产品数据 + */ + formatShopeeProduct(product) { + return { + ...product, + title: product.title || '无标题', + price: product.price || '无价格', + sales: product.sales || 0, + rating: product.rating || 0, + shopName: product.shopName || '无店铺' + }; + } + + /** + * 验证产品数据 + * @param {Object} product - 产品数据 + * @param {String} platform - 平台类型 + * @returns {Object} 验证结果 {valid: boolean, errors: Array} + */ + validateProductData(product, platform = 'amazon') { + const errors = []; + + if (!product || typeof product !== 'object') { + errors.push('产品数据无效'); + return { valid: false, errors }; + } + + switch (platform) { + case 'amazon': + if (!product.asin || product.asin.trim() === '') { + errors.push('ASIN不能为空'); + } + break; + case 'rakuten': + if (!product.productUrl) { + errors.push('产品URL不能为空'); + } + if (!product.productTitle) { + errors.push('产品标题不能为空'); + } + break; + case 'shopee': + if (!product.title) { + errors.push('商品标题不能为空'); + } + break; + } + + return { + valid: errors.length === 0, + errors + }; + } + + /** + * 去重产品数据 + * @param {Array} products - 产品数据数组 + * @param {String} keyField - 去重的关键字段 + * @returns {Array} 去重后的产品数据 + */ + // 性能优化:简化去重逻辑,提高可读性 + removeDuplicateProducts(products, keyField = 'id') { + if (!Array.isArray(products)) return []; + + const seen = new Set(); + return products.filter(product => { + const key = product[keyField]; + return key && !seen.has(key) && seen.add(key); + }); + } + + /** + * 分页数据 + * @param {Array} data - 数据数组 + * @param {Number} page - 页码(从1开始) + * @param {Number} pageSize - 页面大小 + * @returns {Object} 分页结果 {data: Array, total: Number, hasMore: Boolean} + */ + paginate(data, page = 1, pageSize = 10) { + if (!Array.isArray(data)) { + return { data: [], total: 0, hasMore: false }; + } + + const start = (page - 1) * pageSize; + const end = start + pageSize; + const paginatedData = data.slice(start, end); + + return { + data: paginatedData, + total: data.length, + hasMore: end < data.length, + currentPage: page, + totalPages: Math.ceil(data.length / pageSize) + }; + } + + /** + * 搜索过滤数据 + * @param {Array} data - 数据数组 + * @param {String} searchText - 搜索文本 + * @param {Array} searchFields - 搜索字段列表 + * @returns {Array} 过滤后的数据 + */ + // 性能优化:内联逻辑,减少函数调用层级 + searchFilter(data, searchText, searchFields = ['title', 'name']) { + if (!Array.isArray(data) || !searchText?.trim()) return data; + + const lowerSearchText = searchText.toLowerCase().trim(); + + return data.filter(item => + searchFields.some(field => { + const value = item[field]; + return value && value.toString().toLowerCase().includes(lowerSearchText); + }) + ); + } + + /** + * 状态过滤数据 + * @param {Array} data - 数据数组 + * @param {String} status - 状态值 + * @param {String} statusField - 状态字段名 + * @returns {Array} 过滤后的数据 + */ + statusFilter(data, status, statusField = 'status') { + if (!Array.isArray(data) || !status || status === '全部' || status === '全部商品') { + return data; + } + + return data.filter(item => item[statusField] === status); + } + + /** + * 格式化时间 + * @param {String|Date|Object} time - 时间对象 + * @returns {String} 格式化后的时间字符串 + */ + formatTime(time) { + if (!time) return '无数据'; + + if (typeof time === 'object' && time.date) { + return time.date; + } + + if (typeof time === 'string') { + return time.replace(/T/, ' ').slice(0, 16); + } + + if (time instanceof Date) { + return time.toISOString().replace(/T/, ' ').slice(0, 16); + } + + return '无数据'; + } + + /** + * 格式化价格 + * @param {String|Number} price - 价格 + * @param {String} currency - 货币符号 + * @returns {String} 格式化后的价格 + */ + formatPrice(price, currency = '¥') { + if (!price || price === '无价格' || price === '无货') { + return '无货'; + } + + const numPrice = parseFloat(price.toString().replace(/[^0-9.]/g, '')); + if (isNaN(numPrice)) { + return '无货'; + } + + return `${currency} ${numPrice.toFixed(2)}`; + } + + /** + * 计算统计信息 + * @param {Array} data - 数据数组 + * @param {String} field - 统计字段 + * @returns {Object} 统计结果 + */ + calculateStats(data, field) { + if (!Array.isArray(data) || data.length === 0) { + return { + count: 0, + min: 0, + max: 0, + avg: 0, + sum: 0 + }; + } + + const values = data + .map(item => parseFloat(item[field])) + .filter(val => !isNaN(val)); + + if (values.length === 0) { + return { + count: 0, + min: 0, + max: 0, + avg: 0, + sum: 0 + }; + } + + const sum = values.reduce((a, b) => a + b, 0); + + return { + count: values.length, + min: Math.min(...values), + max: Math.max(...values), + avg: sum / values.length, + sum: sum + }; + } +} + +// 创建全局实例 +window.dataService = new DataService(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/services/file-service.js b/erp_client_sb/src/main/resources/static/js/services/file-service.js new file mode 100644 index 0000000..2175632 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/services/file-service.js @@ -0,0 +1,206 @@ +/** + * 文件处理服务 + * 统一处理文件上传、验证、解析、导出等功能 + */ +class FileService { + + /** + * 验证文件 + * @param {File} file - 文件对象 + * @param {Object} options - 验证选项 + * @param {Number} options.maxSize - 最大文件大小(MB),默认10MB + * @param {Array} options.allowedTypes - 允许的文件类型,默认['.xlsx', '.xls'] + */ + // 性能优化:内联判断逻辑,减少变量声明 + validateFile(file, options = {}) { + const { maxSize = 10, allowedTypes = ['.xlsx', '.xls'] } = options; + + if (!file) throw new Error('请选择文件'); + + const fileName = file.name.toLowerCase(); + if (!allowedTypes.some(type => fileName.endsWith(type))) { + throw new Error(`只支持 ${allowedTypes.join('、')} 格式文件`); + } + + if (file.size > maxSize * 1048576) { // 1MB = 1048576 bytes + throw new Error(`文件大小不能超过 ${maxSize}MB`); + } + + return true; + } + + /** + * 解析Excel文件并提取ASIN列表 + * @param {File} file - Excel文件 + * @returns {Promise} ASIN列表 + */ + // 性能优化:直接处理错误,减少嵌套try-catch + async parseExcelForASIN(file) { + const data = await this.readFileAsArrayBuffer(file); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(data); + const worksheet = workbook.worksheets[0]; + + const asinList = []; + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > 1) { // 跳过标题行 + const asin = row.getCell(1).text.trim(); + if (asin) asinList.push(asin); + } + }); + + if (asinList.length === 0) { + throw new Error('Excel文件中未找到有效的ASIN'); + } + + return asinList; + } + + /** + * 解析Excel文件并提取店铺名列表 + * @param {File} file - Excel文件 + * @returns {Promise} 店铺名列表 + */ + // 性能优化:复用parseExcelForASIN逻辑,减少代码重复 + async parseExcelForShopNames(file) { + const data = await this.readFileAsArrayBuffer(file); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(data); + const worksheet = workbook.worksheets[0]; + + const shopNames = []; + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > 1) { + const shopName = row.getCell(1).text.trim(); + if (shopName) shopNames.push(shopName); + } + }); + + if (shopNames.length === 0) { + throw new Error('Excel文件中未找到有效的店铺名'); + } + + return shopNames; + } + + /** + * 创建Excel工作簿 + * @param {String} sheetName - 工作表名称 + * @param {Array} headers - 表头数组 + * @param {Array} data - 数据数组 + * @returns {Promise} Excel工作簿 + */ + async createExcelWorkbook(sheetName, headers, data) { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet(sheetName); + + // 设置表头 + worksheet.columns = headers.map((header, index) => ({ + header: header.label || header, + key: header.key || `col_${index}`, + width: header.width || 15 + })); + + // 添加数据 + data.forEach(row => { + worksheet.addRow(row); + }); + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).alignment = { + vertical: 'middle', + horizontal: 'center' + }; + + return workbook; + } + + /** + * 导出Excel文件(浏览器下载) + * @param {ExcelJS.Workbook} workbook - Excel工作簿 + * @param {String} fileName - 文件名 + */ + async exportExcelFile(workbook, fileName) { + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + /** + * 保存Excel文件到桌面(JavaFX环境) + * @param {ExcelJS.Workbook} workbook - Excel工作簿 + * @param {String} fileName - 文件名 + * @returns {String|null} 保存路径 + */ + async saveExcelToDesktop(workbook, fileName) { + try { + const buffer = await workbook.xlsx.writeBuffer(); + const base64Data = btoa(String.fromCharCode(...new Uint8Array(buffer))); + + if (window.javaConnector) { + return window.javaConnector.saveExcelFile(base64Data, fileName); + } else { + // 降级到浏览器下载 + await this.exportExcelFile(workbook, fileName); + return null; + } + } catch (error) { + throw new Error(`保存文件失败: ${error.message}`); + } + } + + /** + * 读取文件为ArrayBuffer + * @param {File} file - 文件对象 + * @returns {Promise} + */ + readFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => reject(new Error('读取文件失败')); + reader.readAsArrayBuffer(file); + }); + } + + /** + * 读取文件为Base64 + * @param {File} file - 文件对象 + * @returns {Promise} + */ + readFileAsBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result.split(',')[1]); + reader.onerror = e => reject(new Error('读取文件失败')); + reader.readAsDataURL(file); + }); + } + + /** + * 格式化文件大小 + * @param {Number} bytes - 字节数 + * @returns {String} 格式化后的大小 + */ + formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } +} + +// 创建全局实例 +window.fileService = new FileService(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/js/sfc-loader.js b/erp_client_sb/src/main/resources/static/js/sfc-loader.js new file mode 100644 index 0000000..100d12c --- /dev/null +++ b/erp_client_sb/src/main/resources/static/js/sfc-loader.js @@ -0,0 +1,55 @@ +(function(){ + function parseComponent(name, source){ + var container = document.createElement('div'); + container.innerHTML = source; + var tpl = container.querySelector('template'); + var style = container.querySelector('style'); + var script = container.querySelector('script'); + + if (style && style.textContent) { + var styleTag = document.createElement('style'); + styleTag.type = 'text/css'; + styleTag.textContent = style.textContent; + document.head.appendChild(styleTag); + } + + var options = {}; + if (script && script.textContent) { + var code = script.textContent.trim(); + // support export default ... + if (/^export\s+default/.test(code)) { + code = code.replace(/^export\s+default/, 'return'); + } else if (/module\.exports\s*=/.test(code)) { + code = code.replace(/module\.exports\s*=/, 'return'); + } else if (!/^return\s+/.test(code)) { + code = 'return (' + code + ')'; + } + try { + options = (new Function(code))() || {}; + } catch (e) { + console.error('SFC script eval failed for', name, e); + options = {}; + } + } + + options = options || {}; + options.template = tpl ? tpl.innerHTML : '
'; + Vue.component(name, options); + } + + function loadOne(def){ + // 使用axios替代fetch + return axios.get(def.url, { cache: false }) + .then(function(response){ return response.data; }) + .then(function(text){ parseComponent(def.name, text); }) + .catch(function(error) { + console.error('Failed to load component:', def.name, error); + return Promise.reject(error); + }); + } + + window.loadSfcComponents = function(defs){ + if (!defs || !defs.length) return Promise.resolve(); + return Promise.all(defs.map(loadOne)); + }; +})(); \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/libs/axios.min.js b/erp_client_sb/src/main/resources/static/libs/axios.min.js new file mode 100644 index 0000000..c9a7d02 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/libs/axios.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),p(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,N=function(e){return!h(e)&&e!==C};var x,P=(x="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return x&&e instanceof x}),k=l("HTMLFormElement"),U=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),_=l("RegExp"),F=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};T(n,(function(n,o){var i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},B="abcdefghijklmnopqrstuvwxyz",L="0123456789",D={DIGIT:L,ALPHA:B,ALPHA_DIGIT:B+B.toUpperCase()+L};var I=l("AsyncFunction"),q={isArray:p,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!h(e)&&null!==e.constructor&&!h(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||y(e.append)&&("formdata"===(t=f(e))||"object"===t&&y(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:h,isDate:E,isFile:O,isBlob:S,isRegExp:_,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:P,isFileList:R,forEach:T,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&j(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):p(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return T(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(p(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:k,hasOwnProperty:U,hasOwnProp:U,reduceDescriptors:F,freezeMethods:function(e){F(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return p(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:j,global:C,isContextDefined:N,ALPHABET:D,generateString:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:16,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:D.ALPHA_DIGIT,n="",r=t.length;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&y(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=p(n)?[]:{};return T(n,(function(t,n){var i=e(t,r+1);!h(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)},isAsyncFn:I,isThenable:function(e){return e&&(g(e)||y(e))&&y(e.then)&&y(e.catch)}};function M(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}q.inherits(M,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:q.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var z=M.prototype,H={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){H[e]={value:e}})),Object.defineProperties(M,H),Object.defineProperty(z,"isAxiosError",{value:!0}),M.from=function(e,t,n,r,o,i){var a=Object.create(z);return q.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),M.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};function J(e){return q.isPlainObject(e)||q.isArray(e)}function W(e){return q.endsWith(e,"[]")?e.slice(0,-2):e}function K(e,t,n){return e?e.concat(t).map((function(e,t){return e=W(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var V=q.toFlatObject(q,{},null,(function(e){return/^is[A-Z]/.test(e)}));function G(t,n,r){if(!q.isObject(t))throw new TypeError("target must be an object");n=n||new FormData;var o=(r=q.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!q.isUndefined(t[e])}))).metaTokens,i=r.visitor||f,a=r.dots,s=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&q.isSpecCompliantForm(n);if(!q.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(q.isDate(e))return e.toISOString();if(!u&&q.isBlob(e))throw new M("Blob is not supported. Use a Buffer instead.");return q.isArrayBuffer(e)||q.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function f(t,r,i){var u=t;if(t&&!i&&"object"===e(t))if(q.endsWith(r,"{}"))r=o?r:r.slice(0,-2),t=JSON.stringify(t);else if(q.isArray(t)&&function(e){return q.isArray(e)&&!e.some(J)}(t)||(q.isFileList(t)||q.endsWith(r,"[]"))&&(u=q.toArray(t)))return r=W(r),u.forEach((function(e,t){!q.isUndefined(e)&&null!==e&&n.append(!0===s?K([r],t,a):null===s?r:r+"[]",c(e))})),!1;return!!J(t)||(n.append(K(i,r,a),c(t)),!1)}var l=[],d=Object.assign(V,{defaultVisitor:f,convertValue:c,isVisitable:J});if(!q.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!q.isUndefined(t)){if(-1!==l.indexOf(t))throw Error("Circular reference detected in "+r.join("."));l.push(t),q.forEach(t,(function(t,o){!0===(!(q.isUndefined(t)||null===t)&&i.call(n,t,q.isString(o)?o.trim():o,r,d))&&e(t,r?r.concat(o):[o])})),l.pop()}}(t),n}function $(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function X(e,t){this._pairs=[],e&&G(e,this,t)}var Q=X.prototype;function Z(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Y(e,t,n){if(!t)return e;var r,o=n&&n.encode||Z,i=n&&n.serialize;if(r=i?i(t,n):q.isURLSearchParams(t)?t.toString():new X(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}Q.append=function(e,t){this._pairs.push([e,t])},Q.toString=function(e){var t=e?function(t){return e.call(this,t,$)}:$;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var ee,te=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){q.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),ne={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},re={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:X,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},isStandardBrowserEnv:("undefined"==typeof navigator||"ReactNative"!==(ee=navigator.product)&&"NativeScript"!==ee&&"NS"!==ee)&&"undefined"!=typeof window&&"undefined"!=typeof document,isStandardBrowserWebWorkerEnv:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,protocols:["http","https","file","blob","url","data"]};function oe(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&q.isArray(r)?r.length:i,s?(q.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&q.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&q.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=q.isObject(e);if(i&&q.isHTMLForm(e)&&(e=new FormData(e)),q.isFormData(e))return o&&o?JSON.stringify(oe(e)):e;if(q.isArrayBuffer(e)||q.isBuffer(e)||q.isStream(e)||q.isFile(e)||q.isBlob(e))return e;if(q.isArrayBufferView(e))return e.buffer;if(q.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return G(e,new re.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return re.isNode&&q.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=q.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return G(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(q.isString(e))try{return(t||JSON.parse)(e),q.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||ie.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&q.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw M.from(e,M.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:re.classes.FormData,Blob:re.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};q.forEach(["delete","get","head","post","put","patch"],(function(e){ie.headers[e]={}}));var ae=ie,se=q.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ue=Symbol("internals");function ce(e){return e&&String(e).trim().toLowerCase()}function fe(e){return!1===e||null==e?e:q.isArray(e)?e.map(fe):String(e)}function le(e,t,n,r,o){return q.isFunction(r)?r.call(this,t,n):(o&&(t=n),q.isString(t)?q.isString(r)?-1!==t.indexOf(r):q.isRegExp(r)?r.test(t):void 0:void 0)}var de=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=ce(t);if(!o)throw new Error("header name must be a non-empty string");var i=q.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=fe(e))}var i,a,s,u,c,f=function(e,t){return q.forEach(e,(function(e,n){return o(e,n,t)}))};return q.isPlainObject(e)||e instanceof this.constructor?f(e,t):q.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&se[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(q.isFunction(t))return t.call(this,r,n);if(q.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);return!(!n||void 0===this[n]||t&&!le(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=ce(e)){var o=q.findKey(n,e);!o||t&&!le(0,n[o],o,t)||(delete n[o],r=!0)}}return q.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var o=t[n];e&&!le(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}},{key:"normalize",value:function(e){var t=this,n={};return q.forEach(this,(function(r,o){var i=q.findKey(n,o);if(i)return t[i]=fe(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=fe(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o1?"since :\n"+u.map(Oe).join("\n"):" "+Oe(u[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n};function Ae(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ve(null,e)}function Te(e){return Ae(e),e.headers=pe.from(e.headers),e.data=he.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Re(e.adapter||ae.adapter)(e).then((function(t){return Ae(e),t.data=he.call(e,e.transformResponse,t),t.headers=pe.from(t.headers),t}),(function(t){return me(t)||(Ae(e),t&&t.response&&(t.response.data=he.call(e,e.transformResponse,t.response),t.response.headers=pe.from(t.response.headers))),Promise.reject(t)}))}var je=function(e){return e instanceof pe?e.toJSON():e};function Ce(e,t){t=t||{};var n={};function r(e,t,n){return q.isPlainObject(e)&&q.isPlainObject(t)?q.merge.call({caseless:n},e,t):q.isPlainObject(t)?q.merge({},t):q.isArray(t)?t.slice():t}function o(e,t,n){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!q.isUndefined(t))return r(void 0,t)}function a(e,t){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}var u={url:i,method:i,data:i,baseURL:a,transformRequest:a,transformResponse:a,paramsSerializer:a,timeout:a,timeoutMessage:a,withCredentials:a,adapter:a,responseType:a,xsrfCookieName:a,xsrfHeaderName:a,onUploadProgress:a,onDownloadProgress:a,decompress:a,maxContentLength:a,maxBodyLength:a,beforeRedirect:a,transport:a,httpAgent:a,httpsAgent:a,cancelToken:a,socketPath:a,responseEncoding:a,validateStatus:s,headers:function(e,t){return o(je(e),je(t),!0)}};return q.forEach(Object.keys(Object.assign({},e,t)),(function(r){var i=u[r]||o,a=i(e[r],t[r],r);q.isUndefined(a)&&i!==s||(n[r]=a)})),n}var Ne="1.6.0",xe={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){xe[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var Pe={};xe.transitional=function(e,t,n){function r(e,t){return"[Axios v1.6.0] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new M(r(o," has been removed"+(t?" in "+t:"")),M.ERR_DEPRECATED);return t&&!Pe[o]&&(Pe[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var ke={assertOptions:function(t,n,r){if("object"!==e(t))throw new M("options must be an object",M.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new M("option "+a+" must be "+c,M.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new M("Unknown option "+a,M.ERR_BAD_OPTION)}},validators:xe},Ue=ke.validators,_e=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new te,response:new te}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=Ce(this.defaults,t),r=n.transitional,o=n.paramsSerializer,i=n.headers;void 0!==r&&ke.assertOptions(r,{silentJSONParsing:Ue.transitional(Ue.boolean),forcedJSONParsing:Ue.transitional(Ue.boolean),clarifyTimeoutError:Ue.transitional(Ue.boolean)},!1),null!=o&&(q.isFunction(o)?t.paramsSerializer={serialize:o}:ke.assertOptions(o,{encode:Ue.function,serialize:Ue.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&q.merge(i.common,i[t.method]);i&&q.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=pe.concat(a,i);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var p=[Te.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,f),l=p.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new ve(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Le={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Le).forEach((function(e){var t=o(e,2),n=t[0],r=t[1];Le[r]=n}));var De=Le;var Ie=function e(t){var n=new Fe(t),r=a(Fe.prototype.request,n);return q.extend(r,Fe.prototype,n,{allOwnKeys:!0}),q.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Ce(t,n))},r}(ae);return Ie.Axios=Fe,Ie.CanceledError=ve,Ie.CancelToken=Be,Ie.isCancel=me,Ie.VERSION=Ne,Ie.toFormData=G,Ie.AxiosError=M,Ie.Cancel=Ie.CanceledError,Ie.all=function(e){return Promise.all(e)},Ie.spread=function(e){return function(t){return e.apply(null,t)}},Ie.isAxiosError=function(e){return q.isObject(e)&&!0===e.isAxiosError},Ie.mergeConfig=Ce,Ie.AxiosHeaders=pe,Ie.formToJSON=function(e){return oe(q.isHTMLForm(e)?new FormData(e):e)},Ie.getAdapter=Re,Ie.HttpStatusCode=De,Ie.default=Ie,Ie})); +//# sourceMappingURL=axios.min.js.map diff --git a/erp_client_sb/src/main/resources/static/libs/element-icons.ttf b/erp_client_sb/src/main/resources/static/libs/element-icons.ttf new file mode 100644 index 0000000..91b74de Binary files /dev/null and b/erp_client_sb/src/main/resources/static/libs/element-icons.ttf differ diff --git a/erp_client_sb/src/main/resources/static/libs/element-icons.woff b/erp_client_sb/src/main/resources/static/libs/element-icons.woff new file mode 100644 index 0000000..02b9a25 Binary files /dev/null and b/erp_client_sb/src/main/resources/static/libs/element-icons.woff differ diff --git a/erp_client_sb/src/main/resources/static/libs/element-ui.css b/erp_client_sb/src/main/resources/static/libs/element-ui.css new file mode 100644 index 0000000..2e1eab5 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/libs/element-ui.css @@ -0,0 +1 @@ +@charset "UTF-8";@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\e6a0"}.el-icon-ice-cream-square:before{content:"\e6a3"}.el-icon-lollipop:before{content:"\e6a4"}.el-icon-potato-strips:before{content:"\e6a5"}.el-icon-milk-tea:before{content:"\e6a6"}.el-icon-ice-drink:before{content:"\e6a7"}.el-icon-ice-tea:before{content:"\e6a9"}.el-icon-coffee:before{content:"\e6aa"}.el-icon-orange:before{content:"\e6ab"}.el-icon-pear:before{content:"\e6ac"}.el-icon-apple:before{content:"\e6ad"}.el-icon-cherry:before{content:"\e6ae"}.el-icon-watermelon:before{content:"\e6af"}.el-icon-grape:before{content:"\e6b0"}.el-icon-refrigerator:before{content:"\e6b1"}.el-icon-goblet-square-full:before{content:"\e6b2"}.el-icon-goblet-square:before{content:"\e6b3"}.el-icon-goblet-full:before{content:"\e6b4"}.el-icon-goblet:before{content:"\e6b5"}.el-icon-cold-drink:before{content:"\e6b6"}.el-icon-coffee-cup:before{content:"\e6b8"}.el-icon-water-cup:before{content:"\e6b9"}.el-icon-hot-water:before{content:"\e6ba"}.el-icon-ice-cream:before{content:"\e6bb"}.el-icon-dessert:before{content:"\e6bc"}.el-icon-sugar:before{content:"\e6bd"}.el-icon-tableware:before{content:"\e6be"}.el-icon-burger:before{content:"\e6bf"}.el-icon-knife-fork:before{content:"\e6c1"}.el-icon-fork-spoon:before{content:"\e6c2"}.el-icon-chicken:before{content:"\e6c3"}.el-icon-food:before{content:"\e6c4"}.el-icon-dish-1:before{content:"\e6c5"}.el-icon-dish:before{content:"\e6c6"}.el-icon-moon-night:before{content:"\e6ee"}.el-icon-moon:before{content:"\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\e6f1"}.el-icon-partly-cloudy:before{content:"\e6f2"}.el-icon-cloudy:before{content:"\e6f3"}.el-icon-sunny:before{content:"\e6f6"}.el-icon-sunset:before{content:"\e6f7"}.el-icon-sunrise-1:before{content:"\e6f8"}.el-icon-sunrise:before{content:"\e6f9"}.el-icon-heavy-rain:before{content:"\e6fa"}.el-icon-lightning:before{content:"\e6fb"}.el-icon-light-rain:before{content:"\e6fc"}.el-icon-wind-power:before{content:"\e6fd"}.el-icon-baseball:before{content:"\e712"}.el-icon-soccer:before{content:"\e713"}.el-icon-football:before{content:"\e715"}.el-icon-basketball:before{content:"\e716"}.el-icon-ship:before{content:"\e73f"}.el-icon-truck:before{content:"\e740"}.el-icon-bicycle:before{content:"\e741"}.el-icon-mobile-phone:before{content:"\e6d3"}.el-icon-service:before{content:"\e6d4"}.el-icon-key:before{content:"\e6e2"}.el-icon-unlock:before{content:"\e6e4"}.el-icon-lock:before{content:"\e6e5"}.el-icon-watch:before{content:"\e6fe"}.el-icon-watch-1:before{content:"\e6ff"}.el-icon-timer:before{content:"\e702"}.el-icon-alarm-clock:before{content:"\e703"}.el-icon-map-location:before{content:"\e704"}.el-icon-delete-location:before{content:"\e705"}.el-icon-add-location:before{content:"\e706"}.el-icon-location-information:before{content:"\e707"}.el-icon-location-outline:before{content:"\e708"}.el-icon-location:before{content:"\e79e"}.el-icon-place:before{content:"\e709"}.el-icon-discover:before{content:"\e70a"}.el-icon-first-aid-kit:before{content:"\e70b"}.el-icon-trophy-1:before{content:"\e70c"}.el-icon-trophy:before{content:"\e70d"}.el-icon-medal:before{content:"\e70e"}.el-icon-medal-1:before{content:"\e70f"}.el-icon-stopwatch:before{content:"\e710"}.el-icon-mic:before{content:"\e711"}.el-icon-copy-document:before{content:"\e718"}.el-icon-full-screen:before{content:"\e719"}.el-icon-switch-button:before{content:"\e71b"}.el-icon-aim:before{content:"\e71c"}.el-icon-crop:before{content:"\e71d"}.el-icon-odometer:before{content:"\e71e"}.el-icon-time:before{content:"\e71f"}.el-icon-bangzhu:before{content:"\e724"}.el-icon-close-notification:before{content:"\e726"}.el-icon-microphone:before{content:"\e727"}.el-icon-turn-off-microphone:before{content:"\e728"}.el-icon-position:before{content:"\e729"}.el-icon-postcard:before{content:"\e72a"}.el-icon-message:before{content:"\e72b"}.el-icon-chat-line-square:before{content:"\e72d"}.el-icon-chat-dot-square:before{content:"\e72e"}.el-icon-chat-dot-round:before{content:"\e72f"}.el-icon-chat-square:before{content:"\e730"}.el-icon-chat-line-round:before{content:"\e731"}.el-icon-chat-round:before{content:"\e732"}.el-icon-set-up:before{content:"\e733"}.el-icon-turn-off:before{content:"\e734"}.el-icon-open:before{content:"\e735"}.el-icon-connection:before{content:"\e736"}.el-icon-link:before{content:"\e737"}.el-icon-cpu:before{content:"\e738"}.el-icon-thumb:before{content:"\e739"}.el-icon-female:before{content:"\e73a"}.el-icon-male:before{content:"\e73b"}.el-icon-guide:before{content:"\e73c"}.el-icon-news:before{content:"\e73e"}.el-icon-price-tag:before{content:"\e744"}.el-icon-discount:before{content:"\e745"}.el-icon-wallet:before{content:"\e747"}.el-icon-coin:before{content:"\e748"}.el-icon-money:before{content:"\e749"}.el-icon-bank-card:before{content:"\e74a"}.el-icon-box:before{content:"\e74b"}.el-icon-present:before{content:"\e74c"}.el-icon-sell:before{content:"\e6d5"}.el-icon-sold-out:before{content:"\e6d6"}.el-icon-shopping-bag-2:before{content:"\e74d"}.el-icon-shopping-bag-1:before{content:"\e74e"}.el-icon-shopping-cart-2:before{content:"\e74f"}.el-icon-shopping-cart-1:before{content:"\e750"}.el-icon-shopping-cart-full:before{content:"\e751"}.el-icon-smoking:before{content:"\e752"}.el-icon-no-smoking:before{content:"\e753"}.el-icon-house:before{content:"\e754"}.el-icon-table-lamp:before{content:"\e755"}.el-icon-school:before{content:"\e756"}.el-icon-office-building:before{content:"\e757"}.el-icon-toilet-paper:before{content:"\e758"}.el-icon-notebook-2:before{content:"\e759"}.el-icon-notebook-1:before{content:"\e75a"}.el-icon-files:before{content:"\e75b"}.el-icon-collection:before{content:"\e75c"}.el-icon-receiving:before{content:"\e75d"}.el-icon-suitcase-1:before{content:"\e760"}.el-icon-suitcase:before{content:"\e761"}.el-icon-film:before{content:"\e763"}.el-icon-collection-tag:before{content:"\e765"}.el-icon-data-analysis:before{content:"\e766"}.el-icon-pie-chart:before{content:"\e767"}.el-icon-data-board:before{content:"\e768"}.el-icon-data-line:before{content:"\e76d"}.el-icon-reading:before{content:"\e769"}.el-icon-magic-stick:before{content:"\e76a"}.el-icon-coordinate:before{content:"\e76b"}.el-icon-mouse:before{content:"\e76c"}.el-icon-brush:before{content:"\e76e"}.el-icon-headset:before{content:"\e76f"}.el-icon-umbrella:before{content:"\e770"}.el-icon-scissors:before{content:"\e771"}.el-icon-mobile:before{content:"\e773"}.el-icon-attract:before{content:"\e774"}.el-icon-monitor:before{content:"\e775"}.el-icon-search:before{content:"\e778"}.el-icon-takeaway-box:before{content:"\e77a"}.el-icon-paperclip:before{content:"\e77d"}.el-icon-printer:before{content:"\e77e"}.el-icon-document-add:before{content:"\e782"}.el-icon-document:before{content:"\e785"}.el-icon-document-checked:before{content:"\e786"}.el-icon-document-copy:before{content:"\e787"}.el-icon-document-delete:before{content:"\e788"}.el-icon-document-remove:before{content:"\e789"}.el-icon-tickets:before{content:"\e78b"}.el-icon-folder-checked:before{content:"\e77f"}.el-icon-folder-delete:before{content:"\e780"}.el-icon-folder-remove:before{content:"\e781"}.el-icon-folder-add:before{content:"\e783"}.el-icon-folder-opened:before{content:"\e784"}.el-icon-folder:before{content:"\e78a"}.el-icon-edit-outline:before{content:"\e764"}.el-icon-edit:before{content:"\e78c"}.el-icon-date:before{content:"\e78e"}.el-icon-c-scale-to-original:before{content:"\e7c6"}.el-icon-view:before{content:"\e6ce"}.el-icon-loading:before{content:"\e6cf"}.el-icon-rank:before{content:"\e6d1"}.el-icon-sort-down:before{content:"\e7c4"}.el-icon-sort-up:before{content:"\e7c5"}.el-icon-sort:before{content:"\e6d2"}.el-icon-finished:before{content:"\e6cd"}.el-icon-refresh-left:before{content:"\e6c7"}.el-icon-refresh-right:before{content:"\e6c8"}.el-icon-refresh:before{content:"\e6d0"}.el-icon-video-play:before{content:"\e7c0"}.el-icon-video-pause:before{content:"\e7c1"}.el-icon-d-arrow-right:before{content:"\e6dc"}.el-icon-d-arrow-left:before{content:"\e6dd"}.el-icon-arrow-up:before{content:"\e6e1"}.el-icon-arrow-down:before{content:"\e6df"}.el-icon-arrow-right:before{content:"\e6e0"}.el-icon-arrow-left:before{content:"\e6de"}.el-icon-top-right:before{content:"\e6e7"}.el-icon-top-left:before{content:"\e6e8"}.el-icon-top:before{content:"\e6e6"}.el-icon-bottom:before{content:"\e6eb"}.el-icon-right:before{content:"\e6e9"}.el-icon-back:before{content:"\e6ea"}.el-icon-bottom-right:before{content:"\e6ec"}.el-icon-bottom-left:before{content:"\e6ed"}.el-icon-caret-top:before{content:"\e78f"}.el-icon-caret-bottom:before{content:"\e790"}.el-icon-caret-right:before{content:"\e791"}.el-icon-caret-left:before{content:"\e792"}.el-icon-d-caret:before{content:"\e79a"}.el-icon-share:before{content:"\e793"}.el-icon-menu:before{content:"\e798"}.el-icon-s-grid:before{content:"\e7a6"}.el-icon-s-check:before{content:"\e7a7"}.el-icon-s-data:before{content:"\e7a8"}.el-icon-s-opportunity:before{content:"\e7aa"}.el-icon-s-custom:before{content:"\e7ab"}.el-icon-s-claim:before{content:"\e7ad"}.el-icon-s-finance:before{content:"\e7ae"}.el-icon-s-comment:before{content:"\e7af"}.el-icon-s-flag:before{content:"\e7b0"}.el-icon-s-marketing:before{content:"\e7b1"}.el-icon-s-shop:before{content:"\e7b4"}.el-icon-s-open:before{content:"\e7b5"}.el-icon-s-management:before{content:"\e7b6"}.el-icon-s-ticket:before{content:"\e7b7"}.el-icon-s-release:before{content:"\e7b8"}.el-icon-s-home:before{content:"\e7b9"}.el-icon-s-promotion:before{content:"\e7ba"}.el-icon-s-operation:before{content:"\e7bb"}.el-icon-s-unfold:before{content:"\e7bc"}.el-icon-s-fold:before{content:"\e7a9"}.el-icon-s-platform:before{content:"\e7bd"}.el-icon-s-order:before{content:"\e7be"}.el-icon-s-cooperation:before{content:"\e7bf"}.el-icon-bell:before{content:"\e725"}.el-icon-message-solid:before{content:"\e799"}.el-icon-video-camera:before{content:"\e772"}.el-icon-video-camera-solid:before{content:"\e796"}.el-icon-camera:before{content:"\e779"}.el-icon-camera-solid:before{content:"\e79b"}.el-icon-download:before{content:"\e77c"}.el-icon-upload2:before{content:"\e77b"}.el-icon-upload:before{content:"\e7c3"}.el-icon-picture-outline-round:before{content:"\e75f"}.el-icon-picture-outline:before{content:"\e75e"}.el-icon-picture:before{content:"\e79f"}.el-icon-close:before{content:"\e6db"}.el-icon-check:before{content:"\e6da"}.el-icon-plus:before{content:"\e6d9"}.el-icon-minus:before{content:"\e6d8"}.el-icon-help:before{content:"\e73d"}.el-icon-s-help:before{content:"\e7b3"}.el-icon-circle-close:before{content:"\e78d"}.el-icon-circle-check:before{content:"\e720"}.el-icon-circle-plus-outline:before{content:"\e723"}.el-icon-remove-outline:before{content:"\e722"}.el-icon-zoom-out:before{content:"\e776"}.el-icon-zoom-in:before{content:"\e777"}.el-icon-error:before{content:"\e79d"}.el-icon-success:before{content:"\e79c"}.el-icon-circle-plus:before{content:"\e7a0"}.el-icon-remove:before{content:"\e7a2"}.el-icon-info:before{content:"\e7a1"}.el-icon-question:before{content:"\e7a4"}.el-icon-warning-outline:before{content:"\e6c9"}.el-icon-warning:before{content:"\e7a3"}.el-icon-goods:before{content:"\e7c2"}.el-icon-s-goods:before{content:"\e7b2"}.el-icon-star-off:before{content:"\e717"}.el-icon-star-on:before{content:"\e797"}.el-icon-more-outline:before{content:"\e6cc"}.el-icon-more:before{content:"\e794"}.el-icon-phone-outline:before{content:"\e6cb"}.el-icon-phone:before{content:"\e795"}.el-icon-user:before{content:"\e6e3"}.el-icon-user-solid:before{content:"\e7a5"}.el-icon-setting:before{content:"\e6ca"}.el-icon-s-tools:before{content:"\e7ac"}.el-icon-delete:before{content:"\e6d7"}.el-icon-delete-solid:before{content:"\e7c9"}.el-icon-eleme:before{content:"\e7c7"}.el-icon-platform-eleme:before{content:"\e7ca"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}.el-pagination{white-space:nowrap;padding:2px 5px;color:#303133;font-weight:700}.el-pagination::after,.el-pagination::before{display:table;content:""}.el-pagination::after{clear:both}.el-pagination button,.el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;-webkit-box-sizing:border-box;box-sizing:border-box}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield;line-height:normal}.el-pagination .el-input__suffix{right:0;-webkit-transform:scale(.8);transform:scale(.8)}.el-pagination .el-select .el-input{width:100px;margin:0 5px}.el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px}.el-pagination button{border:none;padding:0 6px;background:0 0}.el-pagination button:focus{outline:0}.el-pagination button:hover{color:#409EFF}.el-pagination button:disabled{color:#C0C4CC;background-color:#FFF;cursor:not-allowed}.el-pagination .btn-next,.el-pagination .btn-prev{background:center center no-repeat #FFF;background-size:16px;cursor:pointer;margin:0;color:#303133}.el-pagination .btn-next .el-icon,.el-pagination .btn-prev .el-icon{display:block;font-size:12px;font-weight:700}.el-pagination .btn-prev{padding-right:12px}.el-pagination .btn-next{padding-left:12px}.el-pagination .el-pager li.disabled{color:#C0C4CC;cursor:not-allowed}.el-pager li,.el-pager li.btn-quicknext:hover,.el-pager li.btn-quickprev:hover{cursor:pointer}.el-pagination--small .btn-next,.el-pagination--small .btn-prev,.el-pagination--small .el-pager li,.el-pagination--small .el-pager li.btn-quicknext,.el-pagination--small .el-pager li.btn-quickprev,.el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.el-pagination--small .arrow.disabled{visibility:hidden}.el-pagination--small .more::before,.el-pagination--small li.more::before{line-height:24px}.el-pagination--small button,.el-pagination--small span:not([class*=suffix]){height:22px;line-height:22px}.el-pagination--small .el-pagination__editor,.el-pagination--small .el-pagination__editor.el-input .el-input__inner{height:22px}.el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#606266}.el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.el-pagination__sizes .el-input .el-input__inner:hover{border-color:#409EFF}.el-pagination__total{margin-right:10px;font-weight:400;color:#606266}.el-pagination__jump{margin-left:24px;font-weight:400;color:#606266}.el-pagination__jump .el-input__inner{padding:0 3px}.el-pagination__rightwrapper{float:right}.el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:3px}.el-pager,.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev{padding:0}.el-dialog,.el-pager li{-webkit-box-sizing:border-box}.el-pagination__editor.el-input{width:50px}.el-pagination__editor.el-input .el-input__inner{height:28px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev,.el-pagination.is-background .el-pager li{margin:0 5px;background-color:#f4f4f5;color:#606266;min-width:30px;border-radius:2px}.el-pagination.is-background .btn-next.disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-prev.disabled,.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .el-pager li.disabled{color:#C0C4CC}.el-pagination.is-background .el-pager li:not(.disabled):hover{color:#409EFF}.el-pagination.is-background .el-pager li:not(.disabled).active{background-color:#409EFF;color:#FFF}.el-pagination.is-background.el-pagination--small .btn-next,.el-pagination.is-background.el-pagination--small .btn-prev,.el-pagination.is-background.el-pagination--small .el-pager li{margin:0 3px;min-width:22px}.el-pager,.el-pager li{vertical-align:top;margin:0;display:inline-block}.el-pager{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;list-style:none;font-size:0}.el-pager .more::before{line-height:30px}.el-pager li{padding:0 4px;background:#FFF;font-size:13px;min-width:35.5px;height:28px;line-height:28px;box-sizing:border-box;text-align:center}.el-pager li.btn-quicknext,.el-pager li.btn-quickprev{line-height:28px;color:#303133}.el-pager li.btn-quicknext.disabled,.el-pager li.btn-quickprev.disabled{color:#C0C4CC}.el-pager li.active+li{border-left:0}.el-pager li:hover{color:#409EFF}.el-pager li.active{color:#409EFF;cursor:default}@-webkit-keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}.el-dialog{position:relative;margin:0 auto 50px;background:#FFF;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#409EFF}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px;word-break:break-all}.el-dialog__footer{padding:10px 20px 20px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-autocomplete{position:relative;display:inline-block}.el-autocomplete-suggestion{margin:5px 0;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;border:1px solid #E4E7ED;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#FFF}.el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#606266;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-autocomplete-suggestion li.highlighted,.el-autocomplete-suggestion li:hover{background-color:#F5F7FA}.el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.el-autocomplete-suggestion.is-loading li::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-autocomplete-suggestion.is-loading li:hover{background-color:#FFF}.el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.el-dropdown{display:inline-block;position:relative;color:#606266;font-size:14px}.el-dropdown .el-button-group{display:block}.el-dropdown .el-button-group .el-button{float:none}.el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.el-dropdown .el-dropdown__caret-button::before{content:'';position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:rgba(255,255,255,.5)}.el-dropdown .el-dropdown__caret-button.el-button--default::before{background:rgba(220,223,230,.5)}.el-dropdown .el-dropdown__caret-button:hover:not(.is-disabled)::before{top:0;bottom:0}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.el-dropdown__icon{font-size:12px;margin:0 3px}.el-dropdown .el-dropdown-selfdefine:focus:active,.el-dropdown .el-dropdown-selfdefine:focus:not(.focusing){outline-width:0}.el-dropdown [disabled]{cursor:not-allowed;color:#bbb}.el-dropdown-menu{position:absolute;top:0;left:0;z-index:10;padding:10px 0;margin:5px 0;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-dropdown-menu__item,.el-menu-item{font-size:14px;padding:0 20px;cursor:pointer}.el-dropdown-menu__item{list-style:none;line-height:36px;margin:0;color:#606266;outline:0}.el-dropdown-menu__item:focus,.el-dropdown-menu__item:not(.is-disabled):hover{background-color:#ecf5ff;color:#66b1ff}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #EBEEF5}.el-dropdown-menu__item--divided:before{content:'';height:6px;display:block;margin:0 -20px;background-color:#FFF}.el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.el-dropdown-menu--medium{padding:6px 0}.el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.el-dropdown-menu--small{padding:6px 0}.el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.el-dropdown-menu--mini{padding:3px 0}.el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#FFF}.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu::after,.el-menu::before{display:table;content:""}.el-breadcrumb__item:last-child .el-breadcrumb__separator,.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu::after{clear:both}.el-menu.el-menu--horizontal{border-bottom:solid 1px #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409EFF;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--collapse .el-submenu,.el-menu-item{position:relative}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#FFF;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409EFF;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;list-style:none}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-menu .el-submenu{min-width:200px}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #E4E7ED;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:none;transform:none}.el-menu--popup{z-index:100;min-width:200px;border:none;padding:5px 0;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{color:#303133;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.el-radio-button__inner,.el-submenu__title{-webkit-box-sizing:border-box;position:relative;white-space:nowrap}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409EFF}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409EFF}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.el-radio-button__inner,.el-radio-group{display:inline-block;line-height:1;vertical-align:middle}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{-webkit-transition:.2s;transition:.2s;opacity:0}.el-radio-group{font-size:0}.el-radio-button{position:relative;display:inline-block;outline:0}.el-radio-button__inner{background:#FFF;border:1px solid #DCDFE6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#409EFF}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #409EFF;box-shadow:-1px 0 0 0 #409EFF}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#F2F6FC}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-picker-panel,.el-popover,.el-select-dropdown,.el-table-filter,.el-time-panel{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-switch{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{-webkit-transition:.2s;transition:.2s;height:20px;font-size:14px;font-weight:500;vertical-align:middle;color:#303133}.el-switch__label.is-active{color:#409EFF}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #DCDFE6;outline:0;border-radius:10px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#DCDFE6;-webkit-transition:border-color .3s,background-color .3s;transition:border-color .3s,background-color .3s;vertical-align:middle}.el-input__prefix,.el-input__suffix{-webkit-transition:all .3s;color:#C0C4CC}.el-switch__core:after{content:"";position:absolute;top:1px;left:1px;border-radius:100%;-webkit-transition:all .3s;transition:all .3s;width:16px;height:16px;background-color:#FFF}.el-switch.is-checked .el-switch__core{border-color:#409EFF;background-color:#409EFF}.el-switch.is-checked .el-switch__core::after{left:100%;margin-left:-17px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter,.el-switch .label-fade-leave-active{opacity:0}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #E4E7ED;border-radius:4px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item{padding-right:40px}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#409EFF;background-color:#FFF}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#F5F7FA}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\e6da";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#FFF}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#F5F7FA}.el-select-dropdown__item.selected{color:#409EFF;font-weight:700}.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#E4E7ED}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{display:inline-block;position:relative}.el-select .el-select__tags>span{display:contents}.el-select:hover .el-input__inner{border-color:#C0C4CC}.el-select .el-input__inner{cursor:pointer;padding-right:35px}.el-select .el-input__inner:focus{border-color:#409EFF}.el-select .el-input .el-select__caret{color:#C0C4CC;font-size:14px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);cursor:pointer}.el-select .el-input .el-select__caret.is-reverse{-webkit-transform:rotateZ(0);transform:rotateZ(0)}.el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);border-radius:100%;color:#C0C4CC;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-select .el-input .el-select__caret.is-show-close:hover{color:#909399}.el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.el-select .el-input.is-disabled .el-input__inner:hover{border-color:#E4E7ED}.el-range-editor.is-active,.el-range-editor.is-active:hover,.el-select .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-select>.el-input{display:block}.el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.el-select__input.is-mini{height:14px}.el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#C0C4CC;line-height:18px;font-size:14px}.el-select__close:hover{color:#909399}.el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-select__tags-text{overflow:hidden;text-overflow:ellipsis}.el-select .el-tag{-webkit-box-sizing:border-box;box-sizing:border-box;border-color:transparent;margin:2px 0 2px 6px;background-color:#f0f2f5;display:-webkit-box;display:-ms-flexbox;display:flex;max-width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-select .el-tag__close.el-icon-close{background-color:#C0C4CC;top:0;color:#FFF;-ms-flex-negative:0;flex-shrink:0}.el-select .el-tag__close.el-icon-close:hover{background-color:#909399}.el-table,.el-table__expanded-cell{background-color:#FFF}.el-select .el-tag__close.el-icon-close::before{display:block;-webkit-transform:translate(0,.5px);transform:translate(0,.5px)}.el-table{position:relative;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-flex:1;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;font-size:12px;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit .el-table__cell.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th.el-table__cell{background:#F5F7FA}.el-table .el-table__cell{padding:12px 0;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table .el-table__cell.is-center{text-align:center}.el-table .el-table__cell.is-right{text-align:right}.el-table .el-table__cell.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table .el-table__cell.is-hidden>*{visibility:hidden}.el-table--medium .el-table__cell{padding:10px 0}.el-table--small{font-size:12px}.el-table--small .el-table__cell{padding:8px 0}.el-table--mini{font-size:12px}.el-table--mini .el-table__cell{padding:6px 0}.el-table tr{background-color:#FFF}.el-table tr input[type=checkbox]{margin:0}.el-table td.el-table__cell,.el-table th.el-table__cell.is-leaf{border-bottom:1px solid #EBEEF5}.el-table th.el-table__cell.is-sortable{cursor:pointer}.el-table th.el-table__cell{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#FFF}.el-table th.el-table__cell>.cell{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th.el-table__cell>.cell.highlight{color:#409EFF}.el-table th.el-table__cell.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td.el-table__cell div{-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td,.el-table .cell,.el-table-filter{-webkit-box-sizing:border-box}.el-table td.el-table__cell.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-left:10px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #EBEEF5}.el-table--border::after,.el-table--group::after,.el-table::before{content:'';position:absolute;background-color:#EBEEF5;z-index:1}.el-table--border::after,.el-table--group::after{top:0;right:0;width:1px;height:100%}.el-table::before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border .el-table__cell,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #EBEEF5}.el-table--border .el-table__cell:first-child .cell{padding-left:10px}.el-table--border th.el-table__cell.gutter:last-of-type{border-bottom:1px solid #EBEEF5;border-bottom-width:1px}.el-table--border th.el-table__cell,.el-table__fixed-right-patch{border-bottom:1px solid #EBEEF5}.el-table--hidden{visibility:hidden}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;-webkit-box-shadow:0 0 10px rgba(0,0,0,.12);box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right::before,.el-table__fixed::before{content:'';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#EBEEF5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#FFF}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td.el-table__cell{border-top:1px solid #EBEEF5;background-color:#F5F7FA;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td.el-table__cell{border-top:1px solid #EBEEF5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td.el-table__cell,.el-table__header-wrapper tbody td.el-table__cell{background-color:#F5F7FA;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #EBEEF5}.el-table .caret-wrapper{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#C0C4CC;top:5px}.el-table .sort-caret.descending{border-top-color:#C0C4CC;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409EFF}.el-table .descending .sort-caret.descending{border-top-color:#409EFF}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{background:#FAFAFA}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td.el-table__cell,.el-table--striped .el-table__body tr.el-table__row--striped.selection-row td.el-table__cell{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.selection-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped>td.el-table__cell,.el-table__body tr.hover-row.selection-row>td.el-table__cell,.el-table__body tr.hover-row>td.el-table__cell{background-color:#F5F7FA}.el-table__body tr.current-row>td.el-table__cell,.el-table__body tr.selection-row>td.el-table__cell{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #EBEEF5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;-webkit-transform:scale(.75);transform:scale(.75)}.el-table--enable-row-transition .el-table__body td.el-table__cell{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell{background-color:#F5F7FA}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #EBEEF5;border-radius:2px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409EFF;color:#FFF}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #EBEEF5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-date-table td.in-range div,.el-date-table td.in-range div:hover,.el-date-table.is-week-mode .el-date-table__row.current div,.el-date-table.is-week-mode .el-date-table__row:hover div{background-color:#F2F6FC}.el-table-filter__bottom button:hover{color:#409EFF}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-date-table{font-size:12px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#606266}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td{width:32px;height:30px;padding:4px 0;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.el-date-table td div{height:30px;padding:3px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);border-radius:50%}.el-date-table td.next-month,.el-date-table td.prev-month{color:#C0C4CC}.el-date-table td.today{position:relative}.el-date-table td.today span{color:#409EFF;font-weight:700}.el-date-table td.today.end-date span,.el-date-table td.today.start-date span{color:#FFF}.el-date-table td.available:hover{color:#409EFF}.el-date-table td.current:not(.disabled) span{color:#FFF;background-color:#409EFF}.el-date-table td.end-date div,.el-date-table td.start-date div{color:#FFF}.el-date-table td.end-date span,.el-date-table td.start-date span{background-color:#409EFF}.el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td.disabled div{background-color:#F5F7FA;opacity:1;cursor:not-allowed;color:#C0C4CC}.el-date-table td.selected div{margin-left:5px;margin-right:5px;background-color:#F2F6FC;border-radius:15px}.el-date-table td.selected div:hover{background-color:#F2F6FC}.el-date-table td.selected span{background-color:#409EFF;color:#FFF;border-radius:15px}.el-date-table td.week{font-size:80%;color:#606266}.el-month-table,.el-year-table{font-size:12px;border-collapse:collapse}.el-date-table th{padding:5px;color:#606266;font-weight:400;border-bottom:solid 1px #EBEEF5}.el-month-table{margin:-1px}.el-month-table td{text-align:center;padding:8px 0;cursor:pointer}.el-month-table td div{height:48px;padding:6px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-month-table td.today .cell{color:#409EFF;font-weight:700}.el-month-table td.today.end-date .cell,.el-month-table td.today.start-date .cell{color:#FFF}.el-month-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-month-table td.disabled .cell:hover{color:#C0C4CC}.el-month-table td .cell{width:60px;height:36px;display:block;line-height:36px;color:#606266;margin:0 auto;border-radius:18px}.el-month-table td .cell:hover{color:#409EFF}.el-month-table td.in-range div,.el-month-table td.in-range div:hover{background-color:#F2F6FC}.el-month-table td.end-date div,.el-month-table td.start-date div{color:#FFF}.el-month-table td.end-date .cell,.el-month-table td.start-date .cell{color:#FFF;background-color:#409EFF}.el-month-table td.start-date div{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-month-table td.end-date div{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-month-table td.current:not(.disabled) .cell{color:#409EFF}.el-year-table{margin:-1px}.el-year-table .el-icon{color:#303133}.el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.el-year-table td.today .cell{color:#409EFF;font-weight:700}.el-year-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-year-table td.disabled .cell:hover{color:#C0C4CC}.el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#606266;margin:0 auto}.el-year-table td .cell:hover,.el-year-table td.current:not(.disabled) .cell{color:#409EFF}.el-date-range-picker{width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker__header{position:relative;text-align:center;height:28px}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{font-size:16px;font-weight:500;margin-right:50px}.el-date-range-picker__content{float:left;width:50%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:16px}.el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{-webkit-box-sizing:border-box;box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#303133}.el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#FFF}.el-date-picker{width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-picker__header{margin:12px;text-align:center}.el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #EBEEF5}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#606266}.el-date-picker__header-label.active,.el-date-picker__header-label:hover{color:#409EFF}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{padding:10px;text-align:center}.el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.time-select{margin:5px 0;min-width:0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.selected:not(.disabled){color:#409EFF;font-weight:700}.time-select-item.disabled{color:#E4E7ED;cursor:not-allowed}.time-select-item:hover{background-color:#F5F7FA;font-weight:700;cursor:pointer}.el-date-editor{position:relative;display:inline-block;text-align:left}.el-date-editor.el-input,.el-date-editor.el-input__inner{width:220px}.el-date-editor--monthrange.el-input,.el-date-editor--monthrange.el-input__inner{width:300px}.el-date-editor--daterange.el-input,.el-date-editor--daterange.el-input__inner,.el-date-editor--timerange.el-input,.el-date-editor--timerange.el-input__inner{width:350px}.el-date-editor--datetimerange.el-input,.el-date-editor--datetimerange.el-input__inner{width:400px}.el-date-editor--dates .el-input__inner{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .el-icon-circle-close{cursor:pointer}.el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#C0C4CC;float:left;line-height:32px}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#606266}.el-date-editor .el-range-input::-webkit-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input:-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::placeholder{color:#C0C4CC}.el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#303133}.el-date-editor .el-range__close-icon{font-size:14px;color:#C0C4CC;width:25px;display:inline-block;float:right;line-height:32px}.el-range-editor.el-input__inner{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:3px 10px}.el-range-editor .el-range-input{line-height:1}.el-range-editor--medium.el-input__inner{height:36px}.el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.el-range-editor--medium .el-range-input{font-size:14px}.el-range-editor--medium .el-range__close-icon,.el-range-editor--medium .el-range__icon{line-height:28px}.el-range-editor--small.el-input__inner{height:32px}.el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.el-range-editor--small .el-range-input{font-size:13px}.el-range-editor--small .el-range__close-icon,.el-range-editor--small .el-range__icon{line-height:24px}.el-range-editor--mini.el-input__inner{height:28px}.el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.el-range-editor--mini .el-range-input{font-size:12px}.el-range-editor--mini .el-range__close-icon,.el-range-editor--mini .el-range__icon{line-height:20px}.el-range-editor.is-disabled{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled:focus,.el-range-editor.is-disabled:hover{border-color:#E4E7ED}.el-range-editor.is-disabled input{background-color:#F5F7FA;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled input::-webkit-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input:-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::placeholder{color:#C0C4CC}.el-range-editor.is-disabled .el-range-separator{color:#C0C4CC}.el-picker-panel{color:#606266;border:1px solid #E4E7ED;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#FFF;border-radius:4px;line-height:30px;margin:5px 0}.el-picker-panel__body-wrapper::after,.el-picker-panel__body::after{content:"";display:table;clear:both}.el-picker-panel__content{position:relative;margin:15px}.el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#FFF;position:relative;font-size:0}.el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#606266;padding-left:12px;text-align:left;outline:0;cursor:pointer}.el-picker-panel__shortcut:hover{color:#409EFF}.el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#409EFF}.el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.el-picker-panel__icon-btn{font-size:12px;color:#303133;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.el-picker-panel__icon-btn:hover{color:#409EFF}.el-picker-panel__icon-btn.is-disabled{color:#bbb}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;background-color:#FFF;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__input.el-input .el-input__inner,.el-time-spinner__list{padding:0;text-align:center}.el-time-spinner__wrapper.is-arrow{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{-webkit-transform:translateY(-32px);transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#FFF;cursor:default}.el-time-spinner__arrow{font-size:12px;color:#909399;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.el-time-spinner__arrow:hover{color:#409EFF}.el-time-spinner__arrow.el-icon-arrow-up{top:10px}.el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__list{margin:0;list-style:none}.el-time-spinner__list::after,.el-time-spinner__list::before{content:'';display:block;width:100%;height:80px}.el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#606266}.el-time-spinner__item:hover:not(.disabled):not(.active){background:#F5F7FA;cursor:pointer}.el-time-spinner__item.active:not(.disabled){color:#303133;font-weight:700}.el-time-spinner__item.disabled{color:#C0C4CC;cursor:not-allowed}.el-time-panel{margin:5px 0;border:1px solid #E4E7ED;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:content-box;box-sizing:content-box}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content::after,.el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #E4E7ED;border-bottom:1px solid #E4E7ED}.el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#303133}.el-time-panel__btn.confirm{font-weight:800;color:#409EFF}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{position:relative;text-align:center;padding:10px}.el-time-range-picker__cell{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.el-time-range-picker__body{border-radius:2px;border:1px solid #E4E7ED}.el-popover{position:absolute;background:#FFF;min-width:150px;border-radius:4px;border:1px solid #EBEEF5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover,.el-cascader__dropdown,.el-color-picker__panel,.el-message-box,.el-notification{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#FFF;border-radius:4px;border:1px solid #EBEEF5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper::after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus,.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#F56C6C}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409EFF}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__status{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);font-size:24px!important}.el-message-box__status::before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67C23A}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#E6A23C}.el-message-box__status.el-icon-error{color:#F56C6C}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#F56C6C;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#C0C4CC}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#409EFF;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item::after{clear:both}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__content::after{clear:both}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#F56C6C;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:'*';color:#F56C6C;margin-right:4px}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#F56C6C}.el-form-item--feedback .el-input__validateIcon{display:inline-block}.el-tabs__header{padding:0;position:relative;margin:0 0 15px}.el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#409EFF;z-index:1;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;-webkit-transition:all .15s;transition:all .15s}.el-tabs__new-tab .el-icon-plus{-webkit-transform:scale(.8,.8);transform:scale(.8,.8)}.el-tabs__new-tab:hover{color:#409EFF}.el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.el-tabs__nav-wrap::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#E4E7ED;z-index:1}.el-tabs__nav-wrap.is-scrollable{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#909399}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;position:relative;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:left;z-index:2}.el-tabs__nav.is-stretch{min-width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.el-tabs__nav.is-stretch>*{-webkit-box-flex:1;-ms-flex:1;flex:1;text-align:center}.el-tabs__item{padding:0 20px;height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#303133;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:0}.el-tabs__item:focus.is-active.is-focus:not(:active){-webkit-box-shadow:0 0 2px 2px #409EFF inset;box-shadow:0 0 2px 2px #409EFF inset;border-radius:3px}.el-tabs__item .el-icon-close{border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.el-tabs__item .el-icon-close:before{-webkit-transform:scale(.9);transform:scale(.9);display:inline-block}.el-tabs--card>.el-tabs__header .el-tabs__active-bar,.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left,.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs__item .el-icon-close:hover{background-color:#C0C4CC;color:#FFF}.el-tabs__item.is-active{color:#409EFF}.el-tabs__item:hover{color:#409EFF;cursor:pointer}.el-tabs__item.is-disabled{color:#C0C4CC;cursor:default}.el-tabs__content{overflow:hidden;position:relative}.el-tabs--card>.el-tabs__header{border-bottom:1px solid #E4E7ED}.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid #E4E7ED;border-bottom:none;border-radius:4px 4px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #E4E7ED;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .el-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#FFF}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .el-icon-close{width:14px}.el-tabs--border-card{background:#FFF;border:1px solid #DCDFE6;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04);box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:#F5F7FA;border-bottom:1px solid #E4E7ED;margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.el-tabs--border-card>.el-tabs__header .el-tabs__item{-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin-top:-1px;color:#909399}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-col-offset-0,.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#409EFF;background-color:#FFF;border-right-color:#DCDFE6;border-left-color:#DCDFE6}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:#409EFF}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:#C0C4CC}.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),.el-tabs--top .el-tabs__item.is-top:nth-child(2){padding-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:last-child,.el-tabs--bottom .el-tabs__item.is-top:last-child,.el-tabs--top .el-tabs__item.is-bottom:last-child,.el-tabs--top .el-tabs__item.is-top:last-child{padding-right:0}.el-cascader-menu:last-child .el-cascader-node,.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid #DCDFE6}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid transparent}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{top:0;bottom:auto;width:2px;height:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{-webkit-transform:rotateZ(90deg);transform:rotateZ(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{left:auto;top:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{right:auto;bottom:0}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left::after{right:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left::after,.el-tabs--left .el-tabs__nav-wrap.is-right::after,.el-tabs--right .el-tabs__nav-wrap.is-left::after,.el-tabs--right .el-tabs__nav-wrap.is-right::after{height:100%;width:2px;bottom:auto;top:0}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{float:none}.el-tabs--left .el-tabs__item.is-left,.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-right{display:block}.el-tabs--left .el-tabs__header.is-left{float:left;margin-bottom:0;margin-right:10px}.el-button-group>.el-button:not(:last-child),.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid #E4E7ED;border-bottom:none;border-top:1px solid #E4E7ED;text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid #E4E7ED;border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid #E4E7ED;border-right-color:#fff;border-left:none;border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-radius:4px 0 0 4px;border-bottom:1px solid #E4E7ED;border-right:none}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid #dfe4ed}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 transparent}.el-tabs--right .el-tabs__header.is-right{float:right;margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right::after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid #E4E7ED}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid #E4E7ED;border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid #E4E7ED;border-left-color:#fff;border-right:none;border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-radius:0 4px 4px 0;border-bottom:1px solid #E4E7ED;border-left:none}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid #dfe4ed}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 transparent}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{-webkit-animation:slideInRight-enter .3s;animation:slideInRight-enter .3s}.slideInRight-leave{position:absolute;left:0;right:0;-webkit-animation:slideInRight-leave .3s;animation:slideInRight-leave .3s}.slideInLeft-enter{-webkit-animation:slideInLeft-enter .3s;animation:slideInLeft-enter .3s}.slideInLeft-leave{position:absolute;left:0;right:0;-webkit-animation:slideInLeft-leave .3s;animation:slideInLeft-leave .3s}@-webkit-keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@-webkit-keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}.el-tree{position:relative;cursor:default;background:#FFF;color:#606266}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#909399;font-size:14px}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:#409EFF}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node:focus>.el-tree-node__content{background-color:#F5F7FA}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:#409EFF;color:#fff}.el-tree-node__content:hover,.el-upload-list__item:hover{background-color:#F5F7FA}.el-tree-node__content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:#C0C4CC;font-size:12px;-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#C0C4CC}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#f0f7ff}.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#FFF;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#C0C4CC}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#FFF}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67C23A}.el-alert--success.is-light .el-alert__description{color:#67C23A}.el-alert--success.is-dark{background-color:#67C23A;color:#FFF}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#FFF}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#E6A23C}.el-alert--warning.is-light .el-alert__description{color:#E6A23C}.el-alert--warning.is-dark{background-color:#E6A23C;color:#FFF}.el-alert--error.is-light{background-color:#fef0f0;color:#F56C6C}.el-alert--error.is-light .el-alert__description{color:#F56C6C}.el-alert--error.is-dark{background-color:#F56C6C;color:#FFF}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert-fade-enter,.el-alert-fade-leave-active,.el-loading-fade-enter,.el-loading-fade-leave-active,.el-notification-fade-leave-active,.el-upload iframe{opacity:0}.el-carousel__arrow--right,.el-notification.right{right:16px}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #EBEEF5;position:fixed;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67C23A}.el-notification .el-icon-error{color:#F56C6C}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#E6A23C}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.el-input-number .el-input{display:block}.el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.el-input-number__decrease,.el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#F5F7FA;color:#606266;cursor:pointer;font-size:13px}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:#409EFF}.el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#409EFF}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #DCDFE6}.el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #DCDFE6}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:#E4E7ED;color:#E4E7ED}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:#E4E7ED;cursor:not-allowed}.el-input-number--medium{width:200px;line-height:34px}.el-input-number--medium .el-input-number__decrease,.el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.el-input-number--small{width:130px;line-height:30px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.9);transform:scale(.9)}.el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.el-input-number--mini{width:130px;line-height:26px}.el-input-number--mini .el-input-number__decrease,.el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.el-input-number--mini .el-input-number__decrease [class*=el-icon],.el-input-number--mini .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #DCDFE6}.el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #DCDFE6;border-radius:0 0 4px}.el-input-number.is-controls-right[class*=medium] [class*=decrease],.el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.el-input-number.is-controls-right[class*=mini] [class*=decrease],.el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-button-group::after,.el-button-group::before,.el-color-dropdown__main-wrapper::after,.el-link.is-underline:hover:after,.el-page-header__left::after,.el-progress-bar__inner::after,.el-row::after,.el-row::before,.el-slider::after,.el-slider::before,.el-slider__button-wrapper::after,.el-transfer-panel .el-transfer-panel__footer::after,.el-upload-cover::after,.el-upload-list--picture-card .el-upload-list__item-actions::after{content:""}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#FFF}.el-tooltip__popper.is-light{background:#FFF;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#FFF}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#FFF}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#FFF}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#FFF}.el-slider::after,.el-slider::before{display:table}.el-slider__button-wrapper .el-tooltip,.el-slider__button-wrapper::after{display:inline-block;vertical-align:middle}.el-slider::after{clear:both}.el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#E4E7ED;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.el-slider__runway.show-input{margin-right:160px;width:auto}.el-slider__runway.disabled{cursor:default}.el-slider__runway.disabled .el-slider__bar{background-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button{border-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button-wrapper.dragging,.el-slider__runway.disabled .el-slider__button-wrapper.hover,.el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.el-slider__runway.disabled .el-slider__button.dragging,.el-slider__runway.disabled .el-slider__button.hover,.el-slider__runway.disabled .el-slider__button:hover{-webkit-transform:scale(1);transform:scale(1);cursor:not-allowed}.el-slider__button-wrapper,.el-slider__stop{-webkit-transform:translateX(-50%);position:absolute}.el-slider__input{float:right;margin-top:3px;width:130px}.el-slider__input.el-input-number--mini{margin-top:5px}.el-slider__input.el-input-number--medium{margin-top:0}.el-slider__input.el-input-number--large{margin-top:-2px}.el-slider__bar{height:6px;background-color:#409EFF;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.el-slider__button-wrapper{height:36px;width:36px;z-index:1001;top:-15px;transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal}.el-image-viewer__btn,.el-slider__button,.el-step__icon-inner{-moz-user-select:none;-ms-user-select:none}.el-slider__button-wrapper::after{height:100%}.el-slider__button-wrapper.hover,.el-slider__button-wrapper:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button-wrapper.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__button{width:16px;height:16px;border:2px solid #409EFF;background-color:#FFF;border-radius:50%;-webkit-transition:.2s;transition:.2s;-webkit-user-select:none;user-select:none}.el-slider__button.dragging,.el-slider__button.hover,.el-slider__button:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.el-slider__button.hover,.el-slider__button:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__stop{height:6px;width:6px;border-radius:100%;background-color:#FFF;transform:translateX(-50%)}.el-slider__marks{top:0;left:12px;width:18px;height:100%}.el-slider__marks-text{position:absolute;-webkit-transform:translateX(-50%);transform:translateX(-50%);font-size:14px;color:#909399;margin-top:15px}.el-slider.is-vertical{position:relative}.el-slider.is-vertical .el-slider__runway{width:6px;height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:6px;height:auto;border-radius:0 0 3px 3px}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #DCDFE6;line-height:20px;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#C0C4CC}.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#409EFF}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-col-pull-0,.el-col-pull-1,.el-col-pull-10,.el-col-pull-11,.el-col-pull-12,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-2,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-push-0,.el-col-push-1,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-2,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-row,.el-upload-dragger,.el-upload-list__item{position:relative}.el-loading-spinner .el-loading-text{color:#409EFF;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409EFF;stroke-linecap:round}.el-loading-spinner i{color:#409EFF}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-row{-webkit-box-sizing:border-box;box-sizing:border-box}.el-row::after,.el-row::before{display:table}.el-row::after{clear:both}.el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.el-col-0,.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-top{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}[class*=el-col-]{float:left;-webkit-box-sizing:border-box;box-sizing:border-box}.el-col-0{width:0%}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0%}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0%}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0%}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0%}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0%}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-upload{display:inline-block;text-align:center;cursor:pointer;outline:0}.el-upload__input{display:none}.el-upload__tip{font-size:12px;color:#606266;margin-top:7px}.el-upload iframe{position:absolute;z-index:-1;top:0;left:0;filter:alpha(opacity=0)}.el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;cursor:pointer;line-height:146px;vertical-align:top}.el-upload--picture-card i{font-size:28px;color:#8c939d}.el-upload--picture-card:hover,.el-upload:focus{border-color:#409EFF;color:#409EFF}.el-upload:focus .el-upload-dragger{border-color:#409EFF}.el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:360px;height:180px;text-align:center;cursor:pointer;overflow:hidden}.el-upload-dragger .el-icon-upload{font-size:67px;color:#C0C4CC;margin:40px 0 16px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:1px solid #DCDFE6;margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:#606266;font-size:14px;text-align:center}.el-upload-dragger .el-upload__text em{color:#409EFF;font-style:normal}.el-upload-dragger:hover{border-color:#409EFF}.el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #409EFF}.el-upload-list{margin:0;padding:0;list-style:none}.el-upload-list__item{-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#606266;line-height:1.8;margin-top:5px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;width:100%}.el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item:first-child{margin-top:10px}.el-upload-list__item .el-icon-upload-success{color:#67C23A}.el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#606266}.el-upload-list__item .el-icon-close:hover{opacity:1}.el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:5px;font-size:12px;cursor:pointer;opacity:1;color:#409EFF}.el-upload-list__item:hover .el-icon-close{display:inline-block}.el-upload-list__item:hover .el-progress__text{display:none}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.el-upload-list__item.is-success .el-upload-list__item-name:focus,.el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#409EFF;cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon-close-tip{display:inline-block}.el-upload-list__item.is-success:active,.el-upload-list__item.is-success:not(.focusing):focus{outline-width:0}.el-upload-list__item.is-success:active .el-icon-close-tip,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:not(.focusing):focus .el-icon-close-tip{display:none}.el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.el-upload-list__item-name{color:#606266;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;-webkit-transition:color .3s;transition:color .3s;white-space:nowrap}.el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#909399;line-height:inherit}.el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#606266;display:none}.el-upload-list__item-delete:hover{color:#409EFF}.el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.el-upload-list--picture-card .el-upload-list__item .el-icon-check,.el-upload-list--picture-card .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture-card .el-upload-list__item .el-icon-close,.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);-webkit-transition:opacity .3s;transition:opacity .3s}.el-upload-list--picture-card .el-upload-list__item-actions::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-block}.el-upload-list--picture-card .el-progress{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);bottom:auto;width:126px}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{overflow:hidden;z-index:0;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.el-upload-list--picture .el-upload-list__item .el-icon-check,.el-upload-list--picture .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{background:0 0;-webkit-box-shadow:none;box-shadow:none;top:-2px;right:-12px}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name{line-height:70px;margin-top:0}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px;background-color:#FFF}.el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 1px 1px #ccc;box-shadow:0 1px 1px #ccc}.el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.el-upload-cover::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-cover img{display:block;width:100%;height:100%}.el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-cover__label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);color:#fff}.el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.el-upload-cover__interact .btn{display:inline-block;color:#FFF;font-size:14px;cursor:pointer;vertical-align:middle;-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);margin-top:60px}.el-upload-cover__interact .btn span{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{-webkit-transform:translateY(-13px);transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#FFF;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#FFF;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#303133}.el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67C23A}.el-progress.is-success .el-progress__text{color:#67C23A}.el-progress.is-warning .el-progress-bar__inner{background-color:#E6A23C}.el-badge__content,.el-progress.is-exception .el-progress-bar__inner{background-color:#F56C6C}.el-progress.is-warning .el-progress__text{color:#E6A23C}.el-progress.is-exception .el-progress__text{color:#F56C6C}.el-progress-bar{padding-right:50px;display:inline-block;vertical-align:middle;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__header,.el-message,.el-step__icon{-webkit-box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#EBEEF5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#409EFF;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;-webkit-transition:width .6s ease;transition:width .6s ease}.el-progress-bar__inner::after{display:inline-block;height:100%;vertical-align:middle}.el-progress-bar__innerText{display:inline-block;vertical-align:middle;color:#FFF;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-time-spinner{width:100%;white-space:nowrap}.el-spinner{display:inline-block;vertical-align:middle}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.el-message{min-width:380px;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#EBEEF5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,transform .4s,top .4s;transition:opacity .3s,transform .4s,top .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67C23A}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#E6A23C}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#F56C6C}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#C0C4CC;font-size:16px}.el-message__closeBtn:focus{outline-width:0}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67C23A}.el-message .el-icon-error{color:#F56C6C}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#E6A23C}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{border-radius:10px;color:#FFF;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #FFF}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#409EFF}.el-badge__content--success{background-color:#67C23A}.el-badge__content--warning{background-color:#E6A23C}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#F56C6C}.el-card{border-radius:4px;border:1px solid #EBEEF5;background-color:#FFF;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #EBEEF5;box-sizing:border-box}.el-card__body,.el-main{padding:20px}.el-rate{height:20px;line-height:1}.el-rate:active,.el-rate:focus{outline-width:0}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#C0C4CC;-webkit-transition:.3s;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-step.is-vertical,.el-steps{display:-webkit-box;display:-ms-flexbox}.el-rate__text{font-size:14px;vertical-align:middle}.el-steps{display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#F5F7FA}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column}.el-step{position:relative;-ms-flex-negative:1;flex-shrink:1}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{-ms-flex-preferred-size:auto!important;flex-basis:auto!important;-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main{padding-right:0}.el-step__head{position:relative;width:100%}.el-step__head.is-process{color:#303133;border-color:#303133}.el-step__head.is-wait{color:#C0C4CC;border-color:#C0C4CC}.el-step__head.is-success{color:#67C23A;border-color:#67C23A}.el-step__head.is-error{color:#F56C6C;border-color:#F56C6C}.el-step__head.is-finish{color:#409EFF;border-color:#409EFF}.el-step__icon{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:24px;height:24px;font-size:14px;box-sizing:border-box;background:#FFF;-webkit-transition:.15s ease-out;transition:.15s ease-out}.el-step.is-horizontal,.el-step__icon-inner{display:inline-block}.el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{-webkit-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{-webkit-transform:translateY(1px);transform:translateY(1px)}.el-step__line{position:absolute;border-color:inherit;background-color:#C0C4CC}.el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;-webkit-transition:.15s ease-out;transition:.15s ease-out;-webkit-box-sizing:border-box;box-sizing:border-box;width:0;height:0}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{font-weight:700;color:#303133}.el-step__title.is-wait{color:#C0C4CC}.el-step__title.is-success{color:#67C23A}.el-step__title.is-error{color:#F56C6C}.el-step__title.is-finish{color:#409EFF}.el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.el-step__description.is-process{color:#303133}.el-step__description.is-wait{color:#C0C4CC}.el-step__description.is-success{color:#67C23A}.el-step__description.is-error{color:#F56C6C}.el-step__description.is-finish{color:#409EFF}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:flex}.el-step.is-vertical .el-step__head{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{padding-left:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{-webkit-transform:scale(.8) translateY(1px);transform:scale(.8) translateY(1px)}.el-step.is-simple .el-step__main{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.el-step.is-simple .el-step__arrow{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-step.is-simple .el-step__arrow::after,.el-step.is-simple .el-step__arrow::before{content:'';display:inline-block;position:absolute;height:15px;width:1px;background:#C0C4CC}.el-step.is-simple .el-step__arrow::before{-webkit-transform:rotate(-45deg) translateY(-4px);transform:rotate(-45deg) translateY(-4px);-webkit-transform-origin:0 0;transform-origin:0 0}.el-step.is-simple .el-step__arrow::after{-webkit-transform:rotate(45deg) translateY(4px);transform:rotate(45deg) translateY(4px);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-carousel{position:relative}.el-carousel--horizontal{overflow-x:hidden}.el-carousel--vertical{overflow-y:hidden}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#FFF;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;margin:0;padding:0;z-index:2}.el-carousel__indicators--horizontal{bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.el-carousel__indicators--vertical{right:0;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#C0C4CC;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{background-color:transparent;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{display:inline-block;padding:12px 4px}.el-carousel__indicator--vertical{padding:4px 12px}.el-carousel__indicator--vertical .el-carousel__button{width:2px;height:15px}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#FFF;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.el-carousel__item,.el-carousel__mask{height:100%;position:absolute;top:0;left:0}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#FFF;opacity:.24;-webkit-transition:.2s;transition:.2s}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-fade-in-enter,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #EBEEF5;border-bottom:1px solid #EBEEF5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:48px;line-height:48px;background-color:#FFF;color:#303133;cursor:pointer;border-bottom:1px solid #EBEEF5;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409EFF}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#FFF;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:1px solid #EBEEF5}.el-cascader__search-input,.el-cascader__tags,.el-tag{-webkit-box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-cascader,.el-tag{display:inline-block}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#EBEEF5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#FFF;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#EBEEF5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#FFF}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#EBEEF5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#FFF;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#EBEEF5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#FFF}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.el-cascader{position:relative;font-size:14px;line-height:40px}.el-cascader:not(.is-disabled):hover .el-input__inner{cursor:pointer;border-color:#C0C4CC}.el-cascader .el-input .el-input__inner:focus,.el-cascader .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-cascader .el-input{cursor:pointer}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-icon-arrow-down{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:14px}.el-cascader .el-input .el-icon-arrow-down.is-reverse{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-cascader .el-input .el-icon-circle-close:hover{color:#909399}.el-cascader--medium{font-size:14px;line-height:36px}.el-cascader--small{font-size:13px;line-height:32px}.el-cascader--mini{font-size:12px;line-height:28px}.el-cascader.is-disabled .el-cascader__label{z-index:2;color:#C0C4CC}.el-cascader__dropdown{margin:5px 0;font-size:14px;background:#FFF;border:1px solid #E4E7ED;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-cascader__tags{position:absolute;left:0;right:30px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:normal;text-align:left;box-sizing:border-box}.el-cascader__tags .el-tag{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;max-width:100%;margin:2px 0 2px 6px;text-overflow:ellipsis;background:#f0f2f5}.el-cascader__tags .el-tag:not(.is-hit){border-color:transparent}.el-cascader__tags .el-tag>span{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis}.el-cascader__tags .el-tag .el-icon-close{-webkit-box-flex:0;-ms-flex:none;flex:none;background-color:#C0C4CC;color:#FFF}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:#909399}.el-cascader__suggestion-panel{border-radius:4px}.el-cascader__suggestion-list{max-height:204px;margin:0;padding:6px 0;font-size:14px;color:#606266;text-align:center}.el-cascader__suggestion-item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;padding:0 15px;text-align:left;outline:0;cursor:pointer}.el-cascader__suggestion-item:focus,.el-cascader__suggestion-item:hover{background:#F5F7FA}.el-cascader__suggestion-item.is-checked{color:#409EFF;font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{margin:10px 0;color:#C0C4CC}.el-cascader__search-input{-webkit-box-flex:1;-ms-flex:1;flex:1;height:24px;min-width:60px;margin:2px 0 2px 15px;padding:0;color:#606266;border:none;outline:0;box-sizing:border-box}.el-cascader__search-input::-webkit-input-placeholder{color:#C0C4CC}.el-cascader__search-input:-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::placeholder{color:#C0C4CC}.el-color-predefine{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:12px;margin-top:8px;width:280px}.el-color-predefine__colors{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-color-predefine__color-selector{margin:0 0 8px 8px;width:20px;height:20px;border-radius:4px;cursor:pointer}.el-color-predefine__color-selector:nth-child(10n+1){margin-left:0}.el-color-predefine__color-selector.selected{-webkit-box-shadow:0 0 3px 2px #409EFF;box-shadow:0 0 3px 2px #409EFF}.el-color-predefine__color-selector>div{display:-webkit-box;display:-ms-flexbox;display:flex;height:100%;border-radius:3px}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.el-color-hue-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);height:100%}.el-color-hue-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to bottom,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-svpanel{position:relative;width:280px;height:180px}.el-color-svpanel__black,.el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.el-color-svpanel__white{background:-webkit-gradient(linear,left top,right top,from(#fff),to(rgba(255,255,255,0)));background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.el-color-svpanel__black{background:-webkit-gradient(linear,left bottom,left top,from(#000),to(rgba(0,0,0,0)));background:linear-gradient(to top,#000,rgba(0,0,0,0))}.el-color-svpanel__cursor{position:absolute}.el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);transform:translate(-2px,-2px)}.el-color-alpha-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-alpha-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);height:100%}.el-color-alpha-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to bottom,rgba(255,255,255,0) 0,#fff 100%)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-dropdown{width:300px}.el-color-dropdown__main-wrapper{margin-bottom:6px}.el-color-dropdown__main-wrapper::after{display:table;clear:both}.el-color-dropdown__btns{margin-top:6px;text-align:right}.el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.el-color-dropdown__btn:hover{color:#409EFF;border-color:#409EFF}.el-color-dropdown__link-btn{cursor:pointer;color:#409EFF;text-decoration:none;padding:15px;font-size:12px}.el-color-dropdown__link-btn:hover{color:tint(#409EFF,20%)}.el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.el-color-picker--medium{height:36px}.el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.el-color-picker--small{height:32px}.el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.el-color-picker--small .el-color-picker__empty,.el-color-picker--small .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker--mini{height:28px}.el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.el-color-picker--mini .el-color-picker__empty,.el-color-picker--mini .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:rgba(255,255,255,.7)}.el-color-picker__trigger{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.el-color-picker__color{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.el-color-picker__icon,.el-input,.el-textarea{display:inline-block;width:100%}.el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.el-color-picker__empty{font-size:12px;color:#999;position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.el-color-picker__icon{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);color:#FFF;text-align:center;font-size:12px}.el-input__prefix,.el-input__suffix{position:absolute;top:0;text-align:center}.el-color-picker__panel{position:absolute;z-index:10;padding:6px;-webkit-box-sizing:content-box;box-sizing:content-box;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-input__inner,.el-textarea__inner,.el-transfer-panel{-webkit-box-sizing:border-box}.el-textarea{position:relative;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#909399;font-size:12px}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input,.el-input__inner{font-size:inherit}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;height:40px;line-height:40px;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__inner::-ms-reveal{display:none}.el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{height:100%;right:5px;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{height:100%;left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-image-viewer__btn,.el-image__preview,.el-link,.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-transfer{font-size:14px}.el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#FFF;background-color:#409EFF;font-size:0}.el-button-group>.el-button+.el-button,.el-transfer-panel__item+.el-transfer-panel__item,.el-transfer__button [class*=el-icon-]+span{margin-left:0}.el-divider__text,.el-image__error,.el-link,.el-timeline,.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer__button.is-with-texts{border-radius:4px}.el-transfer__button.is-disabled,.el-transfer__button.is-disabled:hover{border:1px solid #DCDFE6;background-color:#F5F7FA;color:#C0C4CC}.el-transfer__button:first-child{margin-bottom:10px}.el-transfer__button:nth-child(2){margin:0}.el-transfer-panel{border:1px solid #EBEEF5;border-radius:4px;overflow:hidden;background:#FFF;display:inline-block;vertical-align:middle;width:200px;max-height:100%;box-sizing:border-box;position:relative}.el-transfer-panel__body{height:246px}.el-transfer-panel__body.is-with-footer{padding-bottom:40px}.el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block!important}.el-transfer-panel__item.el-checkbox{color:#606266}.el-transfer-panel__item:hover{color:#409EFF}.el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;padding-left:24px;line-height:30px}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;margin:15px;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;width:auto}.el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.el-transfer-panel__filter .el-input__icon{margin-left:5px}.el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#F5F7FA;margin:0;padding-left:15px;border-bottom:1px solid #EBEEF5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#000}.el-container,.el-header{-webkit-box-sizing:border-box}.el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#303133;font-weight:400}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#909399;font-size:12px;font-weight:400}.el-transfer-panel .el-transfer-panel__footer{height:40px;background:#FFF;margin:0;padding:0;border-top:1px solid #EBEEF5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.el-transfer-panel .el-transfer-panel__footer::after{display:inline-block;height:100%;vertical-align:middle}.el-container,.el-timeline-item__node{display:-webkit-box;display:-ms-flexbox}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#606266}.el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#909399;text-align:center}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.el-transfer-panel .el-checkbox__inner::after{height:6px;width:3px;left:4px}.el-container{display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;box-sizing:border-box;min-width:0}.el-container.is-vertical,.el-drawer,.el-empty,.el-result{-webkit-box-orient:vertical;-webkit-box-direction:normal}.el-container.is-vertical{-ms-flex-direction:column;flex-direction:column}.el-header{padding:0 20px;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-main{display:block;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-timeline{margin:0;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline-item{position:relative;padding-bottom:20px}.el-timeline-item__wrapper{position:relative;padding-left:28px;top:-3px}.el-timeline-item__tail{position:absolute;left:4px;height:100%;border-left:2px solid #E4E7ED}.el-timeline-item__icon{color:#FFF;font-size:13px}.el-timeline-item__node{position:absolute;background-color:#E4E7ED;border-radius:50%;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image__error,.el-timeline-item__dot{display:-webkit-box;display:-ms-flexbox}.el-timeline-item__node--normal{left:-1px;width:12px;height:12px}.el-timeline-item__node--large{left:-2px;width:14px;height:14px}.el-timeline-item__node--primary{background-color:#409EFF}.el-timeline-item__node--success{background-color:#67C23A}.el-timeline-item__node--warning{background-color:#E6A23C}.el-timeline-item__node--danger{background-color:#F56C6C}.el-timeline-item__node--info{background-color:#909399}.el-timeline-item__dot{position:absolute;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-timeline-item__content{color:#303133}.el-timeline-item__timestamp{color:#909399;line-height:1;font-size:13px}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-link{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;vertical-align:middle;position:relative;text-decoration:none;outline:0;padding:0;font-weight:500}.el-link.is-underline:hover:after{position:absolute;left:0;right:0;height:0;bottom:0;border-bottom:1px solid #409EFF}.el-link.el-link--default:after,.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:#409EFF}.el-link.is-disabled{cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default{color:#606266}.el-link.el-link--default:hover{color:#409EFF}.el-link.el-link--default.is-disabled{color:#C0C4CC}.el-link.el-link--primary{color:#409EFF}.el-link.el-link--primary:hover{color:#66b1ff}.el-link.el-link--primary.is-disabled{color:#a0cfff}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:#F56C6C}.el-link.el-link--danger{color:#F56C6C}.el-link.el-link--danger:hover{color:#f78989}.el-link.el-link--danger.is-disabled{color:#fab6b6}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:#67C23A}.el-link.el-link--success{color:#67C23A}.el-link.el-link--success:hover{color:#85ce61}.el-link.el-link--success.is-disabled{color:#b3e19d}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:#E6A23C}.el-link.el-link--warning{color:#E6A23C}.el-link.el-link--warning:hover{color:#ebb563}.el-link.el-link--warning.is-disabled{color:#f3d19e}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:#909399}.el-link.el-link--info{color:#909399}.el-link.el-link--info:hover{color:#a6a9ad}.el-link.el-link--info.is-disabled{color:#c8c9cc}.el-divider{background-color:#DCDFE6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#FFF;padding:0 20px;font-weight:500;color:#303133}.el-image__error,.el-image__placeholder{background:#F5F7FA}.el-divider__text.is-left{left:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-divider__text.is-center{left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-image__error,.el-image__inner,.el-image__placeholder{width:100%;height:100%}.el-image{position:relative;display:inline-block;overflow:hidden}.el-image__inner{vertical-align:top}.el-image__inner--center{position:relative;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:block}.el-image__error{display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#C0C4CC;vertical-align:middle}.el-image-viewer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.el-image-viewer__btn{position:absolute;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:50%;opacity:.8;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;user-select:none}.el-button,.el-checkbox,.el-checkbox-button__inner,.el-empty__image img,.el-radio{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-image-viewer__close{top:40px;right:40px;width:40px;height:40px;font-size:24px;color:#fff;background-color:#606266}.el-image-viewer__canvas{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image-viewer__actions{left:50%;bottom:30px;-webkit-transform:translateX(-50%);transform:translateX(-50%);width:282px;height:44px;padding:0 23px;background-color:#606266;border-color:#fff;border-radius:22px}.el-image-viewer__actions__inner{width:100%;height:100%;text-align:justify;cursor:default;font-size:23px;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.el-image-viewer__next,.el-image-viewer__prev{width:44px;height:44px;font-size:24px;color:#fff;background-color:#606266;border-color:#fff;top:50%}.el-image-viewer__prev{-webkit-transform:translateY(-50%);transform:translateY(-50%);left:40px}.el-image-viewer__next{-webkit-transform:translateY(-50%);transform:translateY(-50%);right:40px;text-indent:2px}.el-image-viewer__mask{position:absolute;width:100%;height:100%;top:0;left:0;opacity:.5;background:#000}.viewer-fade-enter-active{-webkit-animation:viewer-fade-in .3s;animation:viewer-fade-in .3s}.viewer-fade-leave-active{-webkit-animation:viewer-fade-out .3s;animation:viewer-fade-out .3s}@-webkit-keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button,.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini{font-size:12px;border-radius:3px}.el-button--mini.is-circle{padding:7px}.el-button--text{border-color:transparent;color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;border-color:transparent;background-color:transparent}.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover{border-color:transparent}.el-button-group .el-button--danger:last-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:last-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:last-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:last-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:last-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child),.el-button-group>.el-dropdown>.el-button{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:first-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:first-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:first-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:first-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-right-color:rgba(255,255,255,.5)}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button.is-disabled{z-index:1}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button.is-active,.el-button-group>.el-button:not(.is-disabled):active,.el-button-group>.el-button:not(.is-disabled):focus,.el-button-group>.el-button:not(.is-disabled):hover{z-index:1}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0}.el-calendar{background-color:#fff}.el-calendar__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #EBEEF5}.el-backtop,.el-page-header{display:-webkit-box;display:-ms-flexbox}.el-calendar__title{color:#000;-ms-flex-item-align:center;align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{padding:12px 0;color:#606266;font-weight:400}.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{color:#C0C4CC}.el-backtop,.el-calendar-table td.is-today{color:#409EFF}.el-calendar-table td{border-bottom:1px solid #EBEEF5;border-right:1px solid #EBEEF5;vertical-align:top;-webkit-transition:background-color .2s ease;transition:background-color .2s ease}.el-calendar-table td.is-selected{background-color:#F2F8FE}.el-calendar-table tr:first-child td{border-top:1px solid #EBEEF5}.el-calendar-table tr td:first-child{border-left:1px solid #EBEEF5}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{-webkit-box-sizing:border-box;box-sizing:border-box;padding:8px;height:85px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:#F2F8FE}.el-backtop{position:fixed;background-color:#FFF;width:40px;height:40px;border-radius:50%;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;font-size:20px;-webkit-box-shadow:0 0 6px rgba(0,0,0,.12);box-shadow:0 0 6px rgba(0,0,0,.12);cursor:pointer;z-index:5}.el-backtop:hover{background-color:#F2F6FC}.el-page-header{display:flex;line-height:24px}.el-page-header__left{display:-webkit-box;display:-ms-flexbox;display:flex;cursor:pointer;margin-right:40px;position:relative}.el-page-header__left::after{position:absolute;width:1px;height:16px;right:-20px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);background-color:#DCDFE6}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-page-header__left .el-icon-back{font-size:18px;margin-right:6px;-ms-flex-item-align:center;align-self:center}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{font-size:18px;color:#303133}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#FFF;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s ease-in .05s;transition:-webkit-transform .15s ease-in .05s;transition:transform .15s ease-in .05s;transition:transform .15s ease-in .05s,-webkit-transform .15s ease-in .05s;-webkit-transform-origin:center;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{display:inline-block;position:relative}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #8cc5ff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-avatar,.el-cascader-panel,.el-radio,.el-radio--medium.is-bordered .el-radio__label,.el-radio__label{font-size:14px}.el-radio{color:#606266;font-weight:500;line-height:1;cursor:pointer;white-space:nowrap;outline:0;margin-right:30px}.el-cascader-node>.el-radio,.el-radio:last-child{margin-right:0}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px}.el-cascader-menu,.el-cascader-menu__list,.el-radio__inner{-webkit-box-sizing:border-box}.el-radio.is-bordered.is-checked{border-color:#409EFF}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#EBEEF5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#F5F7FA;border-color:#E4E7ED}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio__input{white-space:nowrap;cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#F5F7FA}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#C0C4CC}.el-radio__input.is-disabled+span.el-radio__label{color:#C0C4CC;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#409EFF;background:#409EFF}.el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#409EFF}.el-radio__input.is-focus .el-radio__inner{border-color:#409EFF}.el-radio__inner{border:1px solid #DCDFE6;border-radius:100%;width:14px;height:14px;background-color:#FFF;cursor:pointer;box-sizing:border-box}.el-radio__inner:hover{border-color:#409EFF}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#FFF;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-radio__label{padding-left:10px}.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-cascader-panel{display:-webkit-box;display:-ms-flexbox;display:flex;border-radius:4px}.el-cascader-panel.is-bordered{border:1px solid #E4E7ED;border-radius:4px}.el-cascader-menu{min-width:180px;box-sizing:border-box;color:#606266;border-right:solid 1px #E4E7ED}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu__wrap{height:204px}.el-cascader-menu__list{position:relative;min-height:100%;margin:0;padding:6px 0;list-style:none;box-sizing:border-box}.el-cascader-menu__hover-zone{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.el-cascader-menu__empty-text{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-align:center;color:#C0C4CC}.el-cascader-node{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0 30px 0 20px;height:34px;line-height:34px;outline:0}.el-cascader-node.is-selectable.in-active-path{color:#606266}.el-cascader-node.in-active-path,.el-cascader-node.is-active,.el-cascader-node.is-selectable.in-checked-path{color:#409EFF;font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):focus,.el-cascader-node:not(.is-disabled):hover{background:#F5F7FA}.el-cascader-node.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{-webkit-box-flex:1;-ms-flex:1;flex:1;padding:0 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-avatar{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden;color:#fff;background:#C0C4CC;width:40px;height:40px;line-height:40px}.el-drawer,.el-drawer__body>*{-webkit-box-sizing:border-box}.el-avatar>img{display:block;height:100%;vertical-align:middle}.el-empty__image img,.el-empty__image svg{vertical-align:top;height:100%;width:100%}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:4px}.el-avatar--icon{font-size:18px}.el-avatar--large{width:40px;height:40px;line-height:40px}.el-avatar--medium{width:36px;height:36px;line-height:36px}.el-avatar--small{width:28px;height:28px;line-height:28px}@-webkit-keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@-webkit-keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@-webkit-keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@-webkit-keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}@keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}.el-drawer{position:absolute;box-sizing:border-box;background-color:#FFF;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);overflow:hidden;outline:0}.el-drawer.rtl{-webkit-animation:rtl-drawer-out .3s;animation:rtl-drawer-out .3s;right:0}.el-drawer__open .el-drawer.rtl{-webkit-animation:rtl-drawer-in .3s 1ms;animation:rtl-drawer-in .3s 1ms}.el-drawer.ltr{-webkit-animation:ltr-drawer-out .3s;animation:ltr-drawer-out .3s;left:0}.el-drawer__open .el-drawer.ltr{-webkit-animation:ltr-drawer-in .3s 1ms;animation:ltr-drawer-in .3s 1ms}.el-drawer.ttb{-webkit-animation:ttb-drawer-out .3s;animation:ttb-drawer-out .3s;top:0}.el-drawer__open .el-drawer.ttb{-webkit-animation:ttb-drawer-in .3s 1ms;animation:ttb-drawer-in .3s 1ms}.el-drawer.btt{-webkit-animation:btt-drawer-out .3s;animation:btt-drawer-out .3s;bottom:0}.el-drawer__open .el-drawer.btt{-webkit-animation:btt-drawer-in .3s 1ms;animation:btt-drawer-in .3s 1ms}.el-drawer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;margin:0}.el-drawer__header{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#72767b;display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:32px;padding:20px 20px 0}.el-drawer__header>:first-child{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-drawer__title{margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;line-height:inherit;font-size:1rem}.el-drawer__close-btn{border:none;cursor:pointer;font-size:20px;color:inherit;background-color:transparent}.el-drawer__body{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto}.el-drawer__body>*{box-sizing:border-box}.el-drawer.ltr,.el-drawer.rtl{height:100%;top:0;bottom:0}.el-drawer.btt,.el-drawer.ttb{width:100%;left:0;right:0}.el-drawer__container{position:relative;left:0;right:0;top:0;bottom:0;height:100%;width:100%}.el-drawer-fade-enter-active{-webkit-animation:el-drawer-fade-in .3s;animation:el-drawer-fade-in .3s}.el-drawer-fade-leave-active{animation:el-drawer-fade-in .3s reverse}.el-statistic{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:#000;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum";text-align:center}.el-statistic .head{margin-bottom:4px;color:#606266;font-size:13px}.el-statistic .con{font-family:Sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#303133}.el-statistic .con .number{font-size:20px;padding:0 4px}.el-statistic .con span{display:inline-block;margin:0;line-height:100%}.el-popconfirm__main,.el-skeleton__image{display:-ms-flexbox;-webkit-box-align:center;display:-webkit-box}.el-popconfirm__main{display:flex;-ms-flex-align:center;align-items:center}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin:0}@-webkit-keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}@keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}.el-skeleton{width:100%}.el-skeleton__first-line,.el-skeleton__paragraph{height:16px;margin-top:16px;background:#f2f2f2}.el-skeleton.is-animated .el-skeleton__item{background:-webkit-gradient(linear,left top,right top,color-stop(25%,#f2f2f2),color-stop(37%,#e6e6e6),color-stop(63%,#f2f2f2));background:linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%);background-size:400% 100%;-webkit-animation:el-skeleton-loading 1.4s ease infinite;animation:el-skeleton-loading 1.4s ease infinite}.el-skeleton__item{background:#f2f2f2;display:inline-block;height:16px;border-radius:4px;width:100%}.el-skeleton__circle{border-radius:50%;width:36px;height:36px;line-height:36px}.el-skeleton__circle--lg{width:40px;height:40px;line-height:40px}.el-skeleton__circle--md{width:28px;height:28px;line-height:28px}.el-skeleton__button{height:40px;width:64px;border-radius:4px}.el-skeleton__p{width:100%}.el-skeleton__p.is-last{width:61%}.el-skeleton__p.is-first{width:33%}.el-skeleton__text{width:100%;height:13px}.el-skeleton__caption{height:12px}.el-skeleton__h1{height:20px}.el-skeleton__h3{height:18px}.el-skeleton__h5{height:16px}.el-skeleton__image{width:unset;display:flex;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:0}.el-skeleton__image svg{fill:#DCDDE0;width:22%;height:22%}.el-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 0}.el-empty__image{width:160px}.el-empty__image img{user-select:none;-o-object-fit:contain;object-fit:contain}.el-empty__image svg{fill:#DCDDE0}.el-empty__description{margin-top:20px}.el-empty__description p{margin:0;font-size:14px;color:#909399}.el-empty__bottom,.el-result__title{margin-top:20px}.el-descriptions{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;color:#303133}.el-descriptions__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:20px}.el-descriptions__title{font-size:16px;font-weight:700}.el-descriptions--mini,.el-descriptions--small{font-size:12px}.el-descriptions__body{color:#606266;background-color:#FFF}.el-descriptions__body .el-descriptions__table{border-collapse:collapse;width:100%;table-layout:fixed}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:left;font-weight:400;line-height:1.5}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-left{text-align:left}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-center{text-align:center}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-right{text-align:right}.el-descriptions .is-bordered{table-layout:auto}.el-descriptions .is-bordered .el-descriptions-item__cell{border:1px solid #EBEEF5;padding:12px 10px}.el-descriptions :not(.is-bordered) .el-descriptions-item__cell{padding-bottom:12px}.el-descriptions--medium.is-bordered .el-descriptions-item__cell{padding:10px}.el-descriptions--medium:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:10px}.el-descriptions--small.is-bordered .el-descriptions-item__cell{padding:8px 10px}.el-descriptions--small:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:8px}.el-descriptions--mini.is-bordered .el-descriptions-item__cell{padding:6px 10px}.el-descriptions--mini:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:6px}.el-descriptions-item{vertical-align:top}.el-descriptions-item__container{display:-webkit-box;display:-ms-flexbox;display:flex}.el-descriptions-item__container .el-descriptions-item__content,.el-descriptions-item__container .el-descriptions-item__label{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.el-descriptions-item__container .el-descriptions-item__content{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-descriptions-item__label.has-colon::after{content:':';position:relative;top:-.5px}.el-descriptions-item__label.is-bordered-label{font-weight:700;color:#909399;background:#fafafa}.el-descriptions-item__label:not(.is-bordered-label){margin-right:10px}.el-descriptions-item__content{word-break:break-word;overflow-wrap:break-word}.el-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 30px}.el-result__icon svg{width:64px;height:64px}.el-result__title p{margin:0;font-size:20px;color:#303133;line-height:1.3}.el-result__subtitle{margin-top:10px}.el-result__subtitle p{margin:0;font-size:14px;color:#606266;line-height:1.3}.el-result__extra{margin-top:30px}.el-result .icon-success{fill:#67C23A}.el-result .icon-error{fill:#F56C6C}.el-result .icon-info{fill:#909399}.el-result .icon-warning{fill:#E6A23C} \ No newline at end of file diff --git a/erp_client_sb/src/main/resources/static/libs/element-ui.js b/erp_client_sb/src/main/resources/static/libs/element-ui.js new file mode 100644 index 0000000..9b2e984 --- /dev/null +++ b/erp_client_sb/src/main/resources/static/libs/element-ui.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("static/libs/vue")):"function"==typeof define&&define.amd?define("ELEMENT",["static/libs/vue"],t):"object"==typeof exports?exports.ELEMENT=t(require("static/libs/vue")):e.ELEMENT=t(e.Vue)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e, t, i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e, t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e, t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=51)}([function(t, n){t.exports=e},function(e, t, n){var i=n(4);e.exports=function(e, t, n){return void 0===n?i(e,t,!1):i(e,n,!1!==t)}},function(e, t, n){var i;!function(r){"use strict";var o={},s=/d{1,4}|M{1,4}|yy(?:yy)?|S{1,3}|Do|ZZ|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g,a="[^\\s]+",l=/\[([^]*?)\]/gm,u=function(){};function c(e, t){for(var n=[],i=0,r=e.length; i3?0:(e-e%10!=10)*e%10]}};var g={D:function(e){return e.getDay()},DD:function(e){return d(e.getDay())},Do:function(e, t){return t.DoFn(e.getDate())},d:function(e){return e.getDate()},dd:function(e){return d(e.getDate())},ddd:function(e, t){return t.dayNamesShort[e.getDay()]},dddd:function(e, t){return t.dayNames[e.getDay()]},M:function(e){return e.getMonth()+1},MM:function(e){return d(e.getMonth()+1)},MMM:function(e, t){return t.monthNamesShort[e.getMonth()]},MMMM:function(e, t){return t.monthNames[e.getMonth()]},yy:function(e){return d(String(e.getFullYear()),4).substr(2)},yyyy:function(e){return d(e.getFullYear(),4)},h:function(e){return e.getHours()%12||12},hh:function(e){return d(e.getHours()%12||12)},H:function(e){return e.getHours()},HH:function(e){return d(e.getHours())},m:function(e){return e.getMinutes()},mm:function(e){return d(e.getMinutes())},s:function(e){return e.getSeconds()},ss:function(e){return d(e.getSeconds())},S:function(e){return Math.round(e.getMilliseconds()/100)},SS:function(e){return d(Math.round(e.getMilliseconds()/10),2)},SSS:function(e){return d(e.getMilliseconds(),3)},a:function(e, t){return e.getHours()<12?t.amPm[0]:t.amPm[1]},A:function(e, t){return e.getHours()<12?t.amPm[0].toUpperCase():t.amPm[1].toUpperCase()},ZZ:function(e){var t=e.getTimezoneOffset();return(t>0?"-":"+")+d(100*Math.floor(Math.abs(t)/60)+Math.abs(t)%60,4)}},y={d:["\\d\\d?",function(e, t){e.day=t}],Do:["\\d\\d?"+a,function(e, t){e.day=parseInt(t,10)}],M:["\\d\\d?",function(e, t){e.month=t-1}],yy:["\\d\\d?",function(e, t){var n=+(""+(new Date).getFullYear()).substr(0,2);e.year=""+(t>68?n-1:n)+t}],h:["\\d\\d?",function(e, t){e.hour=t}],m:["\\d\\d?",function(e, t){e.minute=t}],s:["\\d\\d?",function(e, t){e.second=t}],yyyy:["\\d{4}",function(e, t){e.year=t}],S:["\\d",function(e, t){e.millisecond=100*t}],SS:["\\d{2}",function(e, t){e.millisecond=10*t}],SSS:["\\d{3}",function(e, t){e.millisecond=t}],D:["\\d\\d?",u],ddd:[a,u],MMM:[a,h("monthNamesShort")],MMMM:[a,h("monthNames")],a:[a,function(e, t, n){var i=t.toLowerCase();i===n.amPm[0]?e.isPm=!1:i===n.amPm[1]&&(e.isPm=!0)}],ZZ:["[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z",function(e, t){var n,i=(t+"").match(/([+-]|\d\d)/gi);i&&(n=60*i[1]+parseInt(i[2],10),e.timezoneOffset="+"===i[0]?n:-n)}]};y.dd=y.d,y.dddd=y.ddd,y.DD=y.D,y.mm=y.m,y.hh=y.H=y.HH=y.h,y.MM=y.M,y.ss=y.s,y.A=y.a,o.masks={default:"ddd MMM dd yyyy HH:mm:ss",shortDate:"M/D/yy",mediumDate:"MMM d, yyyy",longDate:"MMMM d, yyyy",fullDate:"dddd, MMMM d, yyyy",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"},o.format=function(e, t, n){var i=n||o.i18n;if("number"==typeof e&&(e=new Date(e)),"[object Date]"!==Object.prototype.toString.call(e)||isNaN(e.getTime()))throw new Error("Invalid Date in fecha.format");t=o.masks[t]||t||o.masks.default;var r=[];return(t=(t=t.replace(l,function(e, t){return r.push(t),"@@@"})).replace(s,function(t){return t in g?g[t](e,i):t.slice(1,t.length-1)})).replace(/@@@/g,function(){return r.shift()})},o.parse=function(e, t, n){var i=n||o.i18n;if("string"!=typeof t)throw new Error("Invalid format in fecha.parse");if(t=o.masks[t]||t,e.length>1e3)return null;var r={},a=[],u=[];t=t.replace(l,function(e, t){return u.push(t),"@@@"});var c,h=(c=t,c.replace(/[|\\{()[^$+*?.-]/g,"\\$&")).replace(s,function(e){if(y[e]){var t=y[e];return a.push(t[1]),"("+t[0]+")"}return e});h=h.replace(/@@@/g,function(){return u.shift()});var d=e.match(new RegExp(h,"i"));if(!d)return null;for(var f=1; fe?u():!0!==t&&(r=setTimeout(i?function(){r=void 0}:u,void 0===i?e-a:e))}}},function(e, t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e, t){var n=/^(attrs|props|on|nativeOn|class|style|hook)$/;function i(e, t){return function(){e&&e.apply(this,arguments),t&&t.apply(this,arguments)}}e.exports=function(e){return e.reduce(function(e, t){var r,o,s,a,l;for(s in t)if(r=e[s],o=t[s],r&&n.test(s))if("class"===s&&("string"==typeof r&&(l=r,e[s]=r={},r[l]=!0),"string"==typeof o&&(l=o,t[s]=o={},o[l]=!0)),"on"===s||"nativeOn"===s||"hook"===s)for(a in o)r[a]=i(r[a],o[a]);else if(Array.isArray(r))e[s]=r.concat(o);else if(Array.isArray(o))e[s]=[r].concat(o);else for(a in o)r[a]=o[a];else e[s]=t[s];return e},{})}},function(e, t){var n={}.hasOwnProperty;e.exports=function(e, t){return n.call(e,t)}},function(e, t, n){"use strict";t.__esModule=!0;var i,r=n(57),o=(i=r)&&i.__esModule?i:{default:i};t.default=o.default||function(e){for(var t=1; t>>1,z=[["ary",k],["bind",g],["bindKey",y],["curry",_],["curryRight",w],["flip",D],["partial",x],["partialRight",C],["rearg",S]],R="[object Arguments]",H="[object Array]",j="[object AsyncFunction]",W="[object Boolean]",q="[object Date]",Y="[object DOMException]",K="[object Error]",U="[object Function]",G="[object GeneratorFunction]",X="[object Map]",Z="[object Number]",J="[object Null]",Q="[object Object]",ee="[object Proxy]",te="[object RegExp]",ne="[object Set]",ie="[object String]",re="[object Symbol]",oe="[object Undefined]",se="[object WeakMap]",ae="[object WeakSet]",le="[object ArrayBuffer]",ue="[object DataView]",ce="[object Float32Array]",he="[object Float64Array]",de="[object Int8Array]",fe="[object Int16Array]",pe="[object Int32Array]",me="[object Uint8Array]",ve="[object Uint8ClampedArray]",ge="[object Uint16Array]",ye="[object Uint32Array]",be=/\b__p \+= '';/g,_e=/\b(__p \+=) '' \+/g,we=/(__e\(.*?\)|\b__t\)) \+\n'';/g,xe=/&(?:amp|lt|gt|quot|#39);/g,Ce=/[&<>"']/g,ke=RegExp(xe.source),Se=RegExp(Ce.source),De=/<%-([\s\S]+?)%>/g,Ee=/<%([\s\S]+?)%>/g,$e=/<%=([\s\S]+?)%>/g,Te=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Me=/^\w*$/,Ne=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Oe=/[\\^$.*+?()[\]{}|]/g,Pe=RegExp(Oe.source),Ie=/^\s+|\s+$/g,Ae=/^\s+/,Fe=/\s+$/,Le=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Ve=/\{\n\/\* \[wrapped with (.+)\] \*/,Be=/,? & /,ze=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Re=/\\(\\)?/g,He=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,je=/\w*$/,We=/^[-+]0x[0-9a-f]+$/i,qe=/^0b[01]+$/i,Ye=/^\[object .+?Constructor\]$/,Ke=/^0o[0-7]+$/i,Ue=/^(?:0|[1-9]\d*)$/,Ge=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Xe=/($^)/,Ze=/['\n\r\u2028\u2029\\]/g,Je="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",Qe="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",et="[\\ud800-\\udfff]",tt="["+Qe+"]",nt="["+Je+"]",it="\\d+",rt="[\\u2700-\\u27bf]",ot="[a-z\\xdf-\\xf6\\xf8-\\xff]",st="[^\\ud800-\\udfff"+Qe+it+"\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde]",at="\\ud83c[\\udffb-\\udfff]",lt="[^\\ud800-\\udfff]",ut="(?:\\ud83c[\\udde6-\\uddff]){2}",ct="[\\ud800-\\udbff][\\udc00-\\udfff]",ht="[A-Z\\xc0-\\xd6\\xd8-\\xde]",dt="(?:"+ot+"|"+st+")",ft="(?:"+ht+"|"+st+")",pt="(?:"+nt+"|"+at+")"+"?",mt="[\\ufe0e\\ufe0f]?"+pt+("(?:\\u200d(?:"+[lt,ut,ct].join("|")+")[\\ufe0e\\ufe0f]?"+pt+")*"),vt="(?:"+[rt,ut,ct].join("|")+")"+mt,gt="(?:"+[lt+nt+"?",nt,ut,ct,et].join("|")+")",yt=RegExp("['’]","g"),bt=RegExp(nt,"g"),_t=RegExp(at+"(?="+at+")|"+gt+mt,"g"),wt=RegExp([ht+"?"+ot+"+(?:['’](?:d|ll|m|re|s|t|ve))?(?="+[tt,ht,"$"].join("|")+")",ft+"+(?:['’](?:D|LL|M|RE|S|T|VE))?(?="+[tt,ht+dt,"$"].join("|")+")",ht+"?"+dt+"+(?:['’](?:d|ll|m|re|s|t|ve))?",ht+"+(?:['’](?:D|LL|M|RE|S|T|VE))?","\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",it,vt].join("|"),"g"),xt=RegExp("[\\u200d\\ud800-\\udfff"+Je+"\\ufe0e\\ufe0f]"),Ct=/[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,kt=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],St=-1,Dt={};Dt[ce]=Dt[he]=Dt[de]=Dt[fe]=Dt[pe]=Dt[me]=Dt[ve]=Dt[ge]=Dt[ye]=!0,Dt[R]=Dt[H]=Dt[le]=Dt[W]=Dt[ue]=Dt[q]=Dt[K]=Dt[U]=Dt[X]=Dt[Z]=Dt[Q]=Dt[te]=Dt[ne]=Dt[ie]=Dt[se]=!1;var Et={};Et[R]=Et[H]=Et[le]=Et[ue]=Et[W]=Et[q]=Et[ce]=Et[he]=Et[de]=Et[fe]=Et[pe]=Et[X]=Et[Z]=Et[Q]=Et[te]=Et[ne]=Et[ie]=Et[re]=Et[me]=Et[ve]=Et[ge]=Et[ye]=!0,Et[K]=Et[U]=Et[se]=!1;var $t={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Tt=parseFloat,Mt=parseInt,Nt="object"==typeof e&&e&&e.Object===Object&&e,Ot="object"==typeof self&&self&&self.Object===Object&&self,Pt=Nt||Ot||Function("return this")(),It=t&&!t.nodeType&&t,At=It&&"object"==typeof i&&i&&!i.nodeType&&i,Ft=At&&At.exports===It,Lt=Ft&&Nt.process,Vt=function(){try{var e=At&&At.require&&At.require("util").types;return e||Lt&&Lt.binding&&Lt.binding("util")}catch(e){}}(),Bt=Vt&&Vt.isArrayBuffer,zt=Vt&&Vt.isDate,Rt=Vt&&Vt.isMap,Ht=Vt&&Vt.isRegExp,jt=Vt&&Vt.isSet,Wt=Vt&&Vt.isTypedArray;function qt(e, t, n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}function Yt(e, t, n, i){for(var r=-1,o=null==e?0:e.length; ++r-1}function Jt(e, t, n){for(var i=-1,r=null==e?0:e.length; ++i-1;);return n}function wn(e, t){for(var n=e.length; n--&&ln(t,e[n],0)>-1;);return n}var xn=fn({"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss","Ā":"A","Ă":"A","Ą":"A","ā":"a","ă":"a","ą":"a","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","ć":"c","ĉ":"c","ċ":"c","č":"c","Ď":"D","Đ":"D","ď":"d","đ":"d","Ē":"E","Ĕ":"E","Ė":"E","Ę":"E","Ě":"E","ē":"e","ĕ":"e","ė":"e","ę":"e","ě":"e","Ĝ":"G","Ğ":"G","Ġ":"G","Ģ":"G","ĝ":"g","ğ":"g","ġ":"g","ģ":"g","Ĥ":"H","Ħ":"H","ĥ":"h","ħ":"h","Ĩ":"I","Ī":"I","Ĭ":"I","Į":"I","İ":"I","ĩ":"i","ī":"i","ĭ":"i","į":"i","ı":"i","Ĵ":"J","ĵ":"j","Ķ":"K","ķ":"k","ĸ":"k","Ĺ":"L","Ļ":"L","Ľ":"L","Ŀ":"L","Ł":"L","ĺ":"l","ļ":"l","ľ":"l","ŀ":"l","ł":"l","Ń":"N","Ņ":"N","Ň":"N","Ŋ":"N","ń":"n","ņ":"n","ň":"n","ŋ":"n","Ō":"O","Ŏ":"O","Ő":"O","ō":"o","ŏ":"o","ő":"o","Ŕ":"R","Ŗ":"R","Ř":"R","ŕ":"r","ŗ":"r","ř":"r","Ś":"S","Ŝ":"S","Ş":"S","Š":"S","ś":"s","ŝ":"s","ş":"s","š":"s","Ţ":"T","Ť":"T","Ŧ":"T","ţ":"t","ť":"t","ŧ":"t","Ũ":"U","Ū":"U","Ŭ":"U","Ů":"U","Ű":"U","Ų":"U","ũ":"u","ū":"u","ŭ":"u","ů":"u","ű":"u","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","Ż":"Z","Ž":"Z","ź":"z","ż":"z","ž":"z","IJ":"IJ","ij":"ij","Œ":"Oe","œ":"oe","ʼn":"'n","ſ":"s"}),Cn=fn({"&":"&","<":"<",">":">",'"':""","'":"'"});function kn(e){return"\\"+$t[e]}function Sn(e){return xt.test(e)}function Dn(e){var t=-1,n=Array(e.size);return e.forEach(function(e, i){n[++t]=[i,e]}),n}function En(e, t){return function(n){return e(t(n))}}function $n(e, t){for(var n=-1,i=e.length,r=0,o=[]; ++n",""":'"',"'":"'"});var An=function e(t){var n,i=(t=null==t?Pt:An.defaults(Pt.Object(),t,An.pick(Pt,kt))).Array,r=t.Date,Je=t.Error,Qe=t.Function,et=t.Math,tt=t.Object,nt=t.RegExp,it=t.String,rt=t.TypeError,ot=i.prototype,st=Qe.prototype,at=tt.prototype,lt=t["__core-js_shared__"],ut=st.toString,ct=at.hasOwnProperty,ht=0,dt=(n=/[^.]+$/.exec(lt&<.keys&<.keys.IE_PROTO||""))?"Symbol(src)_1."+n:"",ft=at.toString,pt=ut.call(tt),mt=Pt._,vt=nt("^"+ut.call(ct).replace(Oe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),gt=Ft?t.Buffer:o,_t=t.Symbol,xt=t.Uint8Array,$t=gt?gt.allocUnsafe:o,Nt=En(tt.getPrototypeOf,tt),Ot=tt.create,It=at.propertyIsEnumerable,At=ot.splice,Lt=_t?_t.isConcatSpreadable:o,Vt=_t?_t.iterator:o,on=_t?_t.toStringTag:o,fn=function(){try{var e=zo(tt,"defineProperty");return e({},"",{}),e}catch(e){}}(),Fn=t.clearTimeout!==Pt.clearTimeout&&t.clearTimeout,Ln=r&&r.now!==Pt.Date.now&&r.now,Vn=t.setTimeout!==Pt.setTimeout&&t.setTimeout,Bn=et.ceil,zn=et.floor,Rn=tt.getOwnPropertySymbols,Hn=gt?gt.isBuffer:o,jn=t.isFinite,Wn=ot.join,qn=En(tt.keys,tt),Yn=et.max,Kn=et.min,Un=r.now,Gn=t.parseInt,Xn=et.random,Zn=ot.reverse,Jn=zo(t,"DataView"),Qn=zo(t,"Map"),ei=zo(t,"Promise"),ti=zo(t,"Set"),ni=zo(t,"WeakMap"),ii=zo(tt,"create"),ri=ni&&new ni,oi={},si=hs(Jn),ai=hs(Qn),li=hs(ei),ui=hs(ti),ci=hs(ni),hi=_t?_t.prototype:o,di=hi?hi.valueOf:o,fi=hi?hi.toString:o;function pi(e){if($a(e)&&!ga(e)&&!(e instanceof yi)){if(e instanceof gi)return e;if(ct.call(e,"__wrapped__"))return ds(e)}return new gi(e)}var mi=function(){function e(){}return function(t){if(!Ea(t))return{};if(Ot)return Ot(t);e.prototype=t;var n=new e;return e.prototype=o,n}}();function vi(){}function gi(e, t){this.__wrapped__=e,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=o}function yi(e){this.__wrapped__=e,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=L,this.__views__=[]}function bi(e){var t=-1,n=null==e?0:e.length;for(this.clear(); ++t=t?e:t)),e}function Fi(e, t, n, i, r, s){var a,l=t&d,u=t&f,c=t&p;if(n&&(a=r?n(e,i,r,s):n(e)),a!==o)return a;if(!Ea(e))return e;var h=ga(e);if(h){if(a=function(e){var t=e.length,n=new e.constructor(t);return t&&"string"==typeof e[0]&&ct.call(e,"index")&&(n.index=e.index,n.input=e.input),n}(e),!l)return io(e,a)}else{var m=jo(e),v=m==U||m==G;if(wa(e))return Zr(e,l);if(m==Q||m==R||v&&!r){if(a=u||v?{}:qo(e),!l)return u?function(e, t){return ro(e,Ho(e),t)}(e,function(e, t){return e&&ro(t,ol(t),e)}(a,e)):function(e, t){return ro(e,Ro(e),t)}(e,Oi(a,e))}else{if(!Et[m])return r?e:{};a=function(e, t, n){var i,r,o,s=e.constructor;switch(t){case le:return Jr(e);case W:case q:return new s(+e);case ue:return function(e, t){var n=t?Jr(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}(e,n);case ce:case he:case de:case fe:case pe:case me:case ve:case ge:case ye:return Qr(e,n);case X:return new s;case Z:case ie:return new s(e);case te:return(o=new(r=e).constructor(r.source,je.exec(r))).lastIndex=r.lastIndex,o;case ne:return new s;case re:return i=e,di?tt(di.call(i)):{}}}(e,m,l)}}s||(s=new Ci);var g=s.get(e);if(g)return g;if(s.set(e,a),Pa(e))return e.forEach(function(i){a.add(Fi(i,t,n,i,e,s))}),a;if(Ta(e))return e.forEach(function(i, r){a.set(r,Fi(i,t,n,r,e,s))}),a;var y=h?o:(c?u?Po:Oo:u?ol:rl)(e);return Kt(y||e,function(i, r){y&&(i=e[r=i]),Ti(a,r,Fi(i,t,n,r,e,s))}),a}function Li(e, t, n){var i=n.length;if(null==e)return!i;for(e=tt(e); i--;){var r=n[i],s=t[r],a=e[r];if(a===o&&!(r in e)||!s(a))return!1}return!0}function Vi(e, t, n){if("function"!=typeof e)throw new rt(l);return rs(function(){e.apply(o,n)},t)}function Bi(e, t, n, i){var r=-1,o=Zt,a=!0,l=e.length,u=[],c=t.length;if(!l)return u;n&&(t=Qt(t,gn(n))),i?(o=Jt,a=!1):t.length>=s&&(o=bn,a=!1,t=new xi(t));e:for(; ++r-1},_i.prototype.set=function(e, t){var n=this.__data__,i=Mi(n,e);return i<0?(++this.size,n.push([e,t])):n[i][1]=t,this},wi.prototype.clear=function(){this.size=0,this.__data__={hash:new bi,map:new(Qn||_i),string:new bi}},wi.prototype.delete=function(e){var t=Vo(this,e).delete(e);return this.size-=t?1:0,t},wi.prototype.get=function(e){return Vo(this,e).get(e)},wi.prototype.has=function(e){return Vo(this,e).has(e)},wi.prototype.set=function(e, t){var n=Vo(this,e),i=n.size;return n.set(e,t),this.size+=n.size==i?0:1,this},xi.prototype.add=xi.prototype.push=function(e){return this.__data__.set(e,u),this},xi.prototype.has=function(e){return this.__data__.has(e)},Ci.prototype.clear=function(){this.__data__=new _i,this.size=0},Ci.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},Ci.prototype.get=function(e){return this.__data__.get(e)},Ci.prototype.has=function(e){return this.__data__.has(e)},Ci.prototype.set=function(e, t){var n=this.__data__;if(n instanceof _i){var i=n.__data__;if(!Qn||i.length0&&n(a)?t>1?qi(a,t-1,n,i,r):en(r,a):i||(r[r.length]=a)}return r}var Yi=lo(),Ki=lo(!0);function Ui(e, t){return e&&Yi(e,t,rl)}function Gi(e, t){return e&&Ki(e,t,rl)}function Xi(e, t){return Xt(t,function(t){return ka(e[t])})}function Zi(e, t){for(var n=0,i=(t=Kr(t,e)).length; null!=e&&nt}function tr(e, t){return null!=e&&ct.call(e,t)}function nr(e, t){return null!=e&&t in tt(e)}function ir(e, t, n){for(var r=n?Jt:Zt,s=e[0].length,a=e.length,l=a,u=i(a),c=1/0,h=[]; l--;){var d=e[l];l&&t&&(d=Qt(d,gn(t))),c=Kn(d.length,c),u[l]=!n&&(t||s>=120&&d.length>=120)?new xi(l&&d):o}d=e[0];var f=-1,p=u[0];e:for(; ++f=a)return l;var u=n[i];return l*("desc"==u?-1:1)}}return e.index-t.index}(e,t,n)})}function br(e, t, n){for(var i=-1,r=t.length,o={}; ++i-1;)a!==e&&At.call(a,l,1),At.call(e,l,1);return e}function wr(e, t){for(var n=e?t.length:0,i=n-1; n--;){var r=t[n];if(n==i||r!==o){var o=r;Ko(r)?At.call(e,r,1):Br(e,r)}}return e}function xr(e, t){return e+zn(Xn()*(t-e+1))}function Cr(e, t){var n="";if(!e||t<1||t>I)return n;do{t%2&&(n+=e),(t=zn(t/2))&&(e+=e)}while(t);return n}function kr(e, t){return os(ts(e,t,Ml),e+"")}function Sr(e){return Si(fl(e))}function Dr(e, t){var n=fl(e);return ls(n,Ai(t,0,n.length))}function Er(e, t, n, i){if(!Ea(e))return e;for(var r=-1,s=(t=Kr(t,e)).length,a=s-1,l=e; null!=l&&++ro?0:o+t),(n=n>o?o:n)<0&&(n+=o),o=t>n?0:n-t>>>0,t>>>=0;for(var s=i(o); ++r>>1,s=e[o];null!==s&&!Aa(s)&&(n?s<=t:s=s){var c=t?null:ko(e);if(c)return Mn(c);a=!1,r=bn,u=new xi}else u=t?[]:l;e:for(; ++i=i?e:Nr(e,t,n)}var Xr=Fn||function(e){return Pt.clearTimeout(e)};function Zr(e, t){if(t)return e.slice();var n=e.length,i=$t?$t(n):new e.constructor(n);return e.copy(i),i}function Jr(e){var t=new e.constructor(e.byteLength);return new xt(t).set(new xt(e)),t}function Qr(e, t){var n=t?Jr(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}function eo(e, t){if(e!==t){var n=e!==o,i=null===e,r=e==e,s=Aa(e),a=t!==o,l=null===t,u=t==t,c=Aa(t);if(!l&&!c&&!s&&e>t||s&&a&&u&&!l&&!c||i&&a&&u||!n&&u||!r)return 1;if(!i&&!s&&!c&&e1?n[r-1]:o,a=r>2?n[2]:o;for(s=e.length>3&&"function"==typeof s?(r--,s):o,a&&Uo(n[0],n[1],a)&&(s=r<3?o:s,r=1),t=tt(t); ++i-1?r[s?t[a]:a]:o}}function po(e){return No(function(t){var n=t.length,i=n,r=gi.prototype.thru;for(e&&t.reverse(); i--;){var s=t[i];if("function"!=typeof s)throw new rt(l);if(r&&!a&&"wrapper"==Ao(s))var a=new gi([],!0)}for(i=a?i:n; ++i1&&_.reverse(),d&&cl))return!1;var c=s.get(e);if(c&&s.get(t))return c==t;var h=-1,d=!0,f=n&v?new xi:o;for(s.set(e,t),s.set(t,e); ++h-1&&e%1==0&&e1?"& ":"")+t[i],t=t.join(n>2?", ":" "),e.replace(Le,"{\n/* [wrapped with "+t+"] */\n")}(i,function(e, t){return Kt(z,function(n){var i="_."+n[0];t&n[1]&&!Zt(e,i)&&e.push(i)}),e.sort()}(function(e){var t=e.match(Ve);return t?t[1].split(Be):[]}(i),n)))}function as(e){var t=0,n=0;return function(){var i=Un(),r=M-(i-n);if(n=i,r>0){if(++t>=T)return arguments[0]}else t=0;return e.apply(o,arguments)}}function ls(e, t){var n=-1,i=e.length,r=i-1;for(t=t===o?i:t; ++n1?e[t-1]:o;return n="function"==typeof n?(e.pop(),n):o,Os(e,n)});function Bs(e){var t=pi(e);return t.__chain__=!0,t}function zs(e, t){return t(e)}var Rs=No(function(e){var t=e.length,n=t?e[0]:0,i=this.__wrapped__,r=function(t){return Ii(t,e)};return!(t>1||this.__actions__.length)&&i instanceof yi&&Ko(n)?((i=i.slice(n,+n+(t?1:0))).__actions__.push({func:zs,args:[r],thisArg:o}),new gi(i,this.__chain__).thru(function(e){return t&&!e.length&&e.push(o),e})):this.thru(r)});var Hs=oo(function(e, t, n){ct.call(e,n)?++e[n]:Pi(e,n,1)});var js=fo(vs),Ws=fo(gs);function qs(e, t){return(ga(e)?Kt:zi)(e,Lo(t,3))}function Ys(e, t){return(ga(e)?Ut:Ri)(e,Lo(t,3))}var Ks=oo(function(e, t, n){ct.call(e,n)?e[n].push(t):Pi(e,n,[t])});var Us=kr(function(e, t, n){var r=-1,o="function"==typeof t,s=ba(e)?i(e.length):[];return zi(e,function(e){s[++r]=o?qt(t,e,n):rr(e,t,n)}),s}),Gs=oo(function(e, t, n){Pi(e,n,t)});function Xs(e, t){return(ga(e)?Qt:fr)(e,Lo(t,3))}var Zs=oo(function(e, t, n){e[n?0:1].push(t)},function(){return[[],[]]});var Js=kr(function(e, t){if(null==e)return[];var n=t.length;return n>1&&Uo(e,t[0],t[1])?t=[]:n>2&&Uo(t[0],t[1],t[2])&&(t=[t[0]]),yr(e,qi(t,1),[])}),Qs=Ln||function(){return Pt.Date.now()};function ea(e, t, n){return t=n?o:t,t=e&&null==t?e.length:t,Do(e,k,o,o,o,o,t)}function ta(e, t){var n;if("function"!=typeof t)throw new rt(l);return e=Ra(e),function(){return--e>0&&(n=t.apply(this,arguments)),e<=1&&(t=o),n}}var na=kr(function(e, t, n){var i=g;if(n.length){var r=$n(n,Fo(na));i|=x}return Do(e,i,t,n,r)}),ia=kr(function(e, t, n){var i=g|y;if(n.length){var r=$n(n,Fo(ia));i|=x}return Do(t,i,e,n,r)});function ra(e, t, n){var i,r,s,a,u,c,h=0,d=!1,f=!1,p=!0;if("function"!=typeof e)throw new rt(l);function m(t){var n=i,s=r;return i=r=o,h=t,a=e.apply(s,n)}function v(e){var n=e-c;return c===o||n>=t||n<0||f&&e-h>=s}function g(){var e=Qs();if(v(e))return y(e);u=rs(g,function(e){var n=t-(e-c);return f?Kn(n,s-(e-h)):n}(e))}function y(e){return u=o,p&&i?m(e):(i=r=o,a)}function b(){var e=Qs(),n=v(e);if(i=arguments,r=this,c=e,n){if(u===o)return function(e){return h=e,u=rs(g,t),d?m(e):a}(c);if(f)return u=rs(g,t),m(c)}return u===o&&(u=rs(g,t)),a}return t=ja(t)||0,Ea(n)&&(d=!!n.leading,s=(f="maxWait"in n)?Yn(ja(n.maxWait)||0,t):s,p="trailing"in n?!!n.trailing:p),b.cancel=function(){u!==o&&Xr(u),h=0,i=c=r=u=o},b.flush=function(){return u===o?a:y(Qs())},b}var oa=kr(function(e, t){return Vi(e,1,t)}),sa=kr(function(e, t, n){return Vi(e,ja(t)||0,n)});function aa(e, t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new rt(l);var n=function(){var i=arguments,r=t?t.apply(this,i):i[0],o=n.cache;if(o.has(r))return o.get(r);var s=e.apply(this,i);return n.cache=o.set(r,s)||o,s};return n.cache=new(aa.Cache||wi),n}function la(e){if("function"!=typeof e)throw new rt(l);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}aa.Cache=wi;var ua=Ur(function(e, t){var n=(t=1==t.length&&ga(t[0])?Qt(t[0],gn(Lo())):Qt(qi(t,1),gn(Lo()))).length;return kr(function(i){for(var r=-1,o=Kn(i.length,n); ++r=t}),va=or(function(){return arguments}())?or:function(e){return $a(e)&&ct.call(e,"callee")&&!It.call(e,"callee")},ga=i.isArray,ya=Bt?gn(Bt):function(e){return $a(e)&&Qi(e)==le};function ba(e){return null!=e&&Da(e.length)&&!ka(e)}function _a(e){return $a(e)&&ba(e)}var wa=Hn||jl,xa=zt?gn(zt):function(e){return $a(e)&&Qi(e)==q};function Ca(e){if(!$a(e))return!1;var t=Qi(e);return t==K||t==Y||"string"==typeof e.message&&"string"==typeof e.name&&!Na(e)}function ka(e){if(!Ea(e))return!1;var t=Qi(e);return t==U||t==G||t==j||t==ee}function Sa(e){return"number"==typeof e&&e==Ra(e)}function Da(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=I}function Ea(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function $a(e){return null!=e&&"object"==typeof e}var Ta=Rt?gn(Rt):function(e){return $a(e)&&jo(e)==X};function Ma(e){return"number"==typeof e||$a(e)&&Qi(e)==Z}function Na(e){if(!$a(e)||Qi(e)!=Q)return!1;var t=Nt(e);if(null===t)return!0;var n=ct.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&ut.call(n)==pt}var Oa=Ht?gn(Ht):function(e){return $a(e)&&Qi(e)==te};var Pa=jt?gn(jt):function(e){return $a(e)&&jo(e)==ne};function Ia(e){return"string"==typeof e||!ga(e)&&$a(e)&&Qi(e)==ie}function Aa(e){return"symbol"==typeof e||$a(e)&&Qi(e)==re}var Fa=Wt?gn(Wt):function(e){return $a(e)&&Da(e.length)&&!!Dt[Qi(e)]};var La=wo(dr),Va=wo(function(e, t){return e<=t});function Ba(e){if(!e)return[];if(ba(e))return Ia(e)?Pn(e):io(e);if(Vt&&e[Vt])return function(e){for(var t,n=[]; !(t=e.next()).done;)n.push(t.value);return n}(e[Vt]());var t=jo(e);return(t==X?Dn:t==ne?Mn:fl)(e)}function za(e){return e?(e=ja(e))===P||e===-P?(e<0?-1:1)*A:e==e?e:0:0===e?e:0}function Ra(e){var t=za(e),n=t%1;return t==t?n?t-n:t:0}function Ha(e){return e?Ai(Ra(e),0,L):0}function ja(e){if("number"==typeof e)return e;if(Aa(e))return F;if(Ea(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=Ea(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(Ie,"");var n=qe.test(e);return n||Ke.test(e)?Mt(e.slice(2),n?2:8):We.test(e)?F:+e}function Wa(e){return ro(e,ol(e))}function qa(e){return null==e?"":Lr(e)}var Ya=so(function(e, t){if(Jo(t)||ba(t))ro(t,rl(t),e);else for(var n in t)ct.call(t,n)&&Ti(e,n,t[n])}),Ka=so(function(e, t){ro(t,ol(t),e)}),Ua=so(function(e, t, n, i){ro(t,ol(t),e,i)}),Ga=so(function(e, t, n, i){ro(t,rl(t),e,i)}),Xa=No(Ii);var Za=kr(function(e, t){e=tt(e);var n=-1,i=t.length,r=i>2?t[2]:o;for(r&&Uo(t[0],t[1],r)&&(i=1); ++n1),t}),ro(e,Po(e),n),i&&(n=Fi(n,d|f|p,To));for(var r=t.length; r--;)Br(n,t[r]);return n});var ul=No(function(e, t){return null==e?{}:function(e, t){return br(e,t,function(t, n){return el(e,n)})}(e,t)});function cl(e, t){if(null==e)return{};var n=Qt(Po(e),function(e){return[e]});return t=Lo(t),br(e,n,function(e, n){return t(e,n[0])})}var hl=So(rl),dl=So(ol);function fl(e){return null==e?[]:yn(e,rl(e))}var pl=co(function(e, t, n){return t=t.toLowerCase(),e+(n?ml(t):t)});function ml(e){return Cl(qa(e).toLowerCase())}function vl(e){return(e=qa(e))&&e.replace(Ge,xn).replace(bt,"")}var gl=co(function(e, t, n){return e+(n?"-":"")+t.toLowerCase()}),yl=co(function(e, t, n){return e+(n?" ":"")+t.toLowerCase()}),bl=uo("toLowerCase");var _l=co(function(e, t, n){return e+(n?"_":"")+t.toLowerCase()});var wl=co(function(e, t, n){return e+(n?" ":"")+Cl(t)});var xl=co(function(e, t, n){return e+(n?" ":"")+t.toUpperCase()}),Cl=uo("toUpperCase");function kl(e, t, n){return e=qa(e),(t=n?o:t)===o?function(e){return Ct.test(e)}(e)?function(e){return e.match(wt)||[]}(e):function(e){return e.match(ze)||[]}(e):e.match(t)||[]}var Sl=kr(function(e, t){try{return qt(e,o,t)}catch(e){return Ca(e)?e:new Je(e)}}),Dl=No(function(e, t){return Kt(t,function(t){t=cs(t),Pi(e,t,na(e[t],e))}),e});function El(e){return function(){return e}}var $l=po(),Tl=po(!0);function Ml(e){return e}function Nl(e){return ur("function"==typeof e?e:Fi(e,d))}var Ol=kr(function(e, t){return function(n){return rr(n,e,t)}}),Pl=kr(function(e, t){return function(n){return rr(e,n,t)}});function Il(e, t, n){var i=rl(t),r=Xi(t,i);null!=n||Ea(t)&&(r.length||!i.length)||(n=t,t=e,e=this,r=Xi(t,rl(t)));var o=!(Ea(n)&&"chain"in n&&!n.chain),s=ka(e);return Kt(r,function(n){var i=t[n];e[n]=i,s&&(e.prototype[n]=function(){var t=this.__chain__;if(o||t){var n=e(this.__wrapped__);return(n.__actions__=io(this.__actions__)).push({func:i,args:arguments,thisArg:e}),n.__chain__=t,n}return i.apply(e,en([this.value()],arguments))})}),e}function Al(){}var Fl=yo(Qt),Ll=yo(Gt),Vl=yo(rn);function Bl(e){return Go(e)?dn(cs(e)):function(e){return function(t){return Zi(t,e)}}(e)}var zl=_o(),Rl=_o(!0);function Hl(){return[]}function jl(){return!1}var Wl=go(function(e, t){return e+t},0),ql=Co("ceil"),Yl=go(function(e, t){return e/t},1),Kl=Co("floor");var Ul,Gl=go(function(e, t){return e*t},1),Xl=Co("round"),Zl=go(function(e, t){return e-t},0);return pi.after=function(e, t){if("function"!=typeof t)throw new rt(l);return e=Ra(e),function(){if(--e<1)return t.apply(this,arguments)}},pi.ary=ea,pi.assign=Ya,pi.assignIn=Ka,pi.assignInWith=Ua,pi.assignWith=Ga,pi.at=Xa,pi.before=ta,pi.bind=na,pi.bindAll=Dl,pi.bindKey=ia,pi.castArray=function(){if(!arguments.length)return[];var e=arguments[0];return ga(e)?e:[e]},pi.chain=Bs,pi.chunk=function(e, t, n){t=(n?Uo(e,t,n):t===o)?1:Yn(Ra(t),0);var r=null==e?0:e.length;if(!r||t<1)return[];for(var s=0,a=0,l=i(Bn(r/t)); sr?0:r+n),(i=i===o||i>r?r:Ra(i))<0&&(i+=r),i=n>i?0:Ha(i); n>>0)?(e=qa(e))&&("string"==typeof t||null!=t&&!Oa(t))&&!(t=Lr(t))&&Sn(e)?Gr(Pn(e),0,n):e.split(t,n):[]},pi.spread=function(e, t){if("function"!=typeof e)throw new rt(l);return t=null==t?0:Yn(Ra(t),0),kr(function(n){var i=n[t],r=Gr(n,0,t);return i&&en(r,i),qt(e,this,r)})},pi.tail=function(e){var t=null==e?0:e.length;return t?Nr(e,1,t):[]},pi.take=function(e, t, n){return e&&e.length?Nr(e,0,(t=n||t===o?1:Ra(t))<0?0:t):[]},pi.takeRight=function(e, t, n){var i=null==e?0:e.length;return i?Nr(e,(t=i-(t=n||t===o?1:Ra(t)))<0?0:t,i):[]},pi.takeRightWhile=function(e, t){return e&&e.length?Rr(e,Lo(t,3),!1,!0):[]},pi.takeWhile=function(e, t){return e&&e.length?Rr(e,Lo(t,3)):[]},pi.tap=function(e, t){return t(e),e},pi.throttle=function(e, t, n){var i=!0,r=!0;if("function"!=typeof e)throw new rt(l);return Ea(n)&&(i="leading"in n?!!n.leading:i,r="trailing"in n?!!n.trailing:r),ra(e,t,{leading:i,maxWait:t,trailing:r})},pi.thru=zs,pi.toArray=Ba,pi.toPairs=hl,pi.toPairsIn=dl,pi.toPath=function(e){return ga(e)?Qt(e,cs):Aa(e)?[e]:io(us(qa(e)))},pi.toPlainObject=Wa,pi.transform=function(e, t, n){var i=ga(e),r=i||wa(e)||Fa(e);if(t=Lo(t,4),null==n){var o=e&&e.constructor;n=r?i?new o:[]:Ea(e)&&ka(o)?mi(Nt(e)):{}}return(r?Kt:Ui)(e,function(e, i, r){return t(n,e,i,r)}),n},pi.unary=function(e){return ea(e,1)},pi.union=$s,pi.unionBy=Ts,pi.unionWith=Ms,pi.uniq=function(e){return e&&e.length?Vr(e):[]},pi.uniqBy=function(e, t){return e&&e.length?Vr(e,Lo(t,2)):[]},pi.uniqWith=function(e, t){return t="function"==typeof t?t:o,e&&e.length?Vr(e,o,t):[]},pi.unset=function(e, t){return null==e||Br(e,t)},pi.unzip=Ns,pi.unzipWith=Os,pi.update=function(e, t, n){return null==e?e:zr(e,t,Yr(n))},pi.updateWith=function(e, t, n, i){return i="function"==typeof i?i:o,null==e?e:zr(e,t,Yr(n),i)},pi.values=fl,pi.valuesIn=function(e){return null==e?[]:yn(e,ol(e))},pi.without=Ps,pi.words=kl,pi.wrap=function(e, t){return ca(Yr(t),e)},pi.xor=Is,pi.xorBy=As,pi.xorWith=Fs,pi.zip=Ls,pi.zipObject=function(e, t){return Wr(e||[],t||[],Ti)},pi.zipObjectDeep=function(e, t){return Wr(e||[],t||[],Er)},pi.zipWith=Vs,pi.entries=hl,pi.entriesIn=dl,pi.extend=Ka,pi.extendWith=Ua,Il(pi,pi),pi.add=Wl,pi.attempt=Sl,pi.camelCase=pl,pi.capitalize=ml,pi.ceil=ql,pi.clamp=function(e, t, n){return n===o&&(n=t,t=o),n!==o&&(n=(n=ja(n))==n?n:0),t!==o&&(t=(t=ja(t))==t?t:0),Ai(ja(e),t,n)},pi.clone=function(e){return Fi(e,p)},pi.cloneDeep=function(e){return Fi(e,d|p)},pi.cloneDeepWith=function(e, t){return Fi(e,d|p,t="function"==typeof t?t:o)},pi.cloneWith=function(e, t){return Fi(e,p,t="function"==typeof t?t:o)},pi.conformsTo=function(e, t){return null==t||Li(e,t,rl(t))},pi.deburr=vl,pi.defaultTo=function(e, t){return null==e||e!=e?t:e},pi.divide=Yl,pi.endsWith=function(e, t, n){e=qa(e),t=Lr(t);var i=e.length,r=n=n===o?i:Ai(Ra(n),0,i);return(n-=t.length)>=0&&e.slice(n,r)==t},pi.eq=fa,pi.escape=function(e){return(e=qa(e))&&Se.test(e)?e.replace(Ce,Cn):e},pi.escapeRegExp=function(e){return(e=qa(e))&&Pe.test(e)?e.replace(Oe,"\\$&"):e},pi.every=function(e, t, n){var i=ga(e)?Gt:Hi;return n&&Uo(e,t,n)&&(t=o),i(e,Lo(t,3))},pi.find=js,pi.findIndex=vs,pi.findKey=function(e, t){return sn(e,Lo(t,3),Ui)},pi.findLast=Ws,pi.findLastIndex=gs,pi.findLastKey=function(e, t){return sn(e,Lo(t,3),Gi)},pi.floor=Kl,pi.forEach=qs,pi.forEachRight=Ys,pi.forIn=function(e, t){return null==e?e:Yi(e,Lo(t,3),ol)},pi.forInRight=function(e, t){return null==e?e:Ki(e,Lo(t,3),ol)},pi.forOwn=function(e, t){return e&&Ui(e,Lo(t,3))},pi.forOwnRight=function(e, t){return e&&Gi(e,Lo(t,3))},pi.get=Qa,pi.gt=pa,pi.gte=ma,pi.has=function(e, t){return null!=e&&Wo(e,t,tr)},pi.hasIn=el,pi.head=bs,pi.identity=Ml,pi.includes=function(e, t, n, i){e=ba(e)?e:fl(e),n=n&&!i?Ra(n):0;var r=e.length;return n<0&&(n=Yn(r+n,0)),Ia(e)?n<=r&&e.indexOf(t,n)>-1:!!r&&ln(e,t,n)>-1},pi.indexOf=function(e, t, n){var i=null==e?0:e.length;if(!i)return-1;var r=null==n?0:Ra(n);return r<0&&(r=Yn(i+r,0)),ln(e,t,r)},pi.inRange=function(e, t, n){return t=za(t),n===o?(n=t,t=0):n=za(n),function(e, t, n){return e>=Kn(t,n)&&e=-I&&e<=I},pi.isSet=Pa,pi.isString=Ia,pi.isSymbol=Aa,pi.isTypedArray=Fa,pi.isUndefined=function(e){return e===o},pi.isWeakMap=function(e){return $a(e)&&jo(e)==se},pi.isWeakSet=function(e){return $a(e)&&Qi(e)==ae},pi.join=function(e, t){return null==e?"":Wn.call(e,t)},pi.kebabCase=gl,pi.last=Cs,pi.lastIndexOf=function(e, t, n){var i=null==e?0:e.length;if(!i)return-1;var r=i;return n!==o&&(r=(r=Ra(n))<0?Yn(i+r,0):Kn(r,i-1)),t==t?function(e, t, n){for(var i=n+1; i--;)if(e[i]===t)return i;return i}(e,t,r):an(e,cn,r,!0)},pi.lowerCase=yl,pi.lowerFirst=bl,pi.lt=La,pi.lte=Va,pi.max=function(e){return e&&e.length?ji(e,Ml,er):o},pi.maxBy=function(e, t){return e&&e.length?ji(e,Lo(t,2),er):o},pi.mean=function(e){return hn(e,Ml)},pi.meanBy=function(e, t){return hn(e,Lo(t,2))},pi.min=function(e){return e&&e.length?ji(e,Ml,dr):o},pi.minBy=function(e, t){return e&&e.length?ji(e,Lo(t,2),dr):o},pi.stubArray=Hl,pi.stubFalse=jl,pi.stubObject=function(){return{}},pi.stubString=function(){return""},pi.stubTrue=function(){return!0},pi.multiply=Gl,pi.nth=function(e, t){return e&&e.length?gr(e,Ra(t)):o},pi.noConflict=function(){return Pt._===this&&(Pt._=mt),this},pi.noop=Al,pi.now=Qs,pi.pad=function(e, t, n){e=qa(e);var i=(t=Ra(t))?On(e):0;if(!t||i>=t)return e;var r=(t-i)/2;return bo(zn(r),n)+e+bo(Bn(r),n)},pi.padEnd=function(e, t, n){e=qa(e);var i=(t=Ra(t))?On(e):0;return t&&it){var i=e;e=t,t=i}if(n||e%1||t%1){var r=Xn();return Kn(e+r*(t-e+Tt("1e-"+((r+"").length-1))),t)}return xr(e,t)},pi.reduce=function(e, t, n){var i=ga(e)?tn:pn,r=arguments.length<3;return i(e,Lo(t,4),n,r,zi)},pi.reduceRight=function(e, t, n){var i=ga(e)?nn:pn,r=arguments.length<3;return i(e,Lo(t,4),n,r,Ri)},pi.repeat=function(e, t, n){return t=(n?Uo(e,t,n):t===o)?1:Ra(t),Cr(qa(e),t)},pi.replace=function(){var e=arguments,t=qa(e[0]);return e.length<3?t:t.replace(e[1],e[2])},pi.result=function(e, t, n){var i=-1,r=(t=Kr(t,e)).length;for(r||(r=1,e=o); ++iI)return[];var n=L,i=Kn(e,L);t=Lo(t),e-=L;for(var r=vn(i,t); ++n=s)return e;var l=n-On(i);if(l<1)return i;var u=a?Gr(a,0,l).join(""):e.slice(0,l);if(r===o)return u+i;if(a&&(l+=u.length-l),Oa(r)){if(e.slice(l).search(r)){var c,h=u;for(r.global||(r=nt(r.source,qa(je.exec(r))+"g")),r.lastIndex=0; c=r.exec(h);) var d=c.index;u=u.slice(0,d===o?l:d)}}else if(e.indexOf(Lr(r),l)!=l){var f=u.lastIndexOf(r);f>-1&&(u=u.slice(0,f))}return u+i},pi.unescape=function(e){return(e=qa(e))&&ke.test(e)?e.replace(xe,In):e},pi.uniqueId=function(e){var t=++ht;return qa(e)+t},pi.upperCase=xl,pi.upperFirst=Cl,pi.each=qs,pi.eachRight=Ys,pi.first=bs,Il(pi,(Ul={},Ui(pi,function(e, t){ct.call(pi.prototype,t)||(Ul[t]=e)}),Ul),{chain:!1}),pi.VERSION="4.17.10",Kt(["bind","bindKey","curry","curryRight","partial","partialRight"],function(e){pi[e].placeholder=pi}),Kt(["drop","take"],function(e, t){yi.prototype[e]=function(n){n=n===o?1:Yn(Ra(n),0);var i=this.__filtered__&&!t?new yi(this):this.clone();return i.__filtered__?i.__takeCount__=Kn(n,i.__takeCount__):i.__views__.push({size:Kn(n,L),type:e+(i.__dir__<0?"Right":"")}),i},yi.prototype[e+"Right"]=function(t){return this.reverse()[e](t).reverse()}}),Kt(["filter","map","takeWhile"],function(e, t){var n=t+1,i=n==N||3==n;yi.prototype[e]=function(e){var t=this.clone();return t.__iteratees__.push({iteratee:Lo(e,3),type:n}),t.__filtered__=t.__filtered__||i,t}}),Kt(["head","last"],function(e, t){var n="take"+(t?"Right":"");yi.prototype[e]=function(){return this[n](1).value()[0]}}),Kt(["initial","tail"],function(e, t){var n="drop"+(t?"":"Right");yi.prototype[e]=function(){return this.__filtered__?new yi(this):this[n](1)}}),yi.prototype.compact=function(){return this.filter(Ml)},yi.prototype.find=function(e){return this.filter(e).head()},yi.prototype.findLast=function(e){return this.reverse().find(e)},yi.prototype.invokeMap=kr(function(e, t){return"function"==typeof e?new yi(this):this.map(function(n){return rr(n,e,t)})}),yi.prototype.reject=function(e){return this.filter(la(Lo(e)))},yi.prototype.slice=function(e, t){e=Ra(e);var n=this;return n.__filtered__&&(e>0||t<0)?new yi(n):(e<0?n=n.takeRight(-e):e&&(n=n.drop(e)),t!==o&&(n=(t=Ra(t))<0?n.dropRight(-t):n.take(t-e)),n)},yi.prototype.takeRightWhile=function(e){return this.reverse().takeWhile(e).reverse()},yi.prototype.toArray=function(){return this.take(L)},Ui(yi.prototype,function(e, t){var n=/^(?:filter|find|map|reject)|While$/.test(t),i=/^(?:head|last)$/.test(t),r=pi[i?"take"+("last"==t?"Right":""):t],s=i||/^find/.test(t);r&&(pi.prototype[t]=function(){var t=this.__wrapped__,a=i?[1]:arguments,l=t instanceof yi,u=a[0],c=l||ga(t),h=function(e){var t=r.apply(pi,en([e],a));return i&&d?t[0]:t};c&&n&&"function"==typeof u&&1!=u.length&&(l=c=!1);var d=this.__chain__,f=!!this.__actions__.length,p=s&&!d,m=l&&!f;if(!s&&c){t=m?t:new yi(this);var v=e.apply(t,a);return v.__actions__.push({func:zs,args:[h],thisArg:o}),new gi(v,d)}return p&&m?e.apply(this,a):(v=this.thru(h),p?i?v.value()[0]:v.value():v)})}),Kt(["pop","push","shift","sort","splice","unshift"],function(e){var t=ot[e],n=/^(?:push|sort|unshift)$/.test(e)?"tap":"thru",i=/^(?:pop|shift)$/.test(e);pi.prototype[e]=function(){var e=arguments;if(i&&!this.__chain__){var r=this.value();return t.apply(ga(r)?r:[],e)}return this[n](function(n){return t.apply(ga(n)?n:[],e)})}}),Ui(yi.prototype,function(e, t){var n=pi[t];if(n){var i=n.name+"";(oi[i]||(oi[i]=[])).push({name:t,func:n})}}),oi[mo(o,y).name]=[{name:"wrapper",func:o}],yi.prototype.clone=function(){var e=new yi(this.__wrapped__);return e.__actions__=io(this.__actions__),e.__dir__=this.__dir__,e.__filtered__=this.__filtered__,e.__iteratees__=io(this.__iteratees__),e.__takeCount__=this.__takeCount__,e.__views__=io(this.__views__),e},yi.prototype.reverse=function(){if(this.__filtered__){var e=new yi(this);e.__dir__=-1,e.__filtered__=!0}else(e=this.clone()).__dir__*=-1;return e},yi.prototype.value=function(){var e=this.__wrapped__.value(),t=this.__dir__,n=ga(e),i=t<0,r=n?e.length:0,o=function(e, t, n){for(var i=-1,r=n.length; ++i=this.__values__.length;return{done:e,value:e?o:this.__values__[this.__index__++]}},pi.prototype.plant=function(e){for(var t,n=this; n instanceof vi;){var i=ds(n);i.__index__=0,i.__values__=o,t?r.__wrapped__=i:t=i;var r=i;n=n.__wrapped__}return r.__wrapped__=e,t},pi.prototype.reverse=function(){var e=this.__wrapped__;if(e instanceof yi){var t=e;return this.__actions__.length&&(t=new yi(this)),(t=t.reverse()).__actions__.push({func:zs,args:[Es],thisArg:o}),new gi(t,this.__chain__)}return this.thru(Es)},pi.prototype.toJSON=pi.prototype.valueOf=pi.prototype.value=function(){return Hr(this.__wrapped__,this.__actions__)},pi.prototype.first=pi.prototype.head,Vt&&(pi.prototype[Vt]=function(){return this}),pi}();Pt._=An,(r=function(){return An}.call(t,n,t,i))===o||(i.exports=r)}).call(this)}).call(this,n(37),n(89)(e))},function(e, t){var n=e.exports={version:"2.6.2"};"number"==typeof __e&&(__e=n)},function(e, t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e, t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e, t, n){var i=n(4),r=n(1);e.exports={throttle:i,debounce:r}},function(e, t, n){var i=n(16);e.exports=function(e){if(!i(e))throw TypeError(e+" is not an object!");return e}},function(e, t){e.exports=function(e, t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e, t, n){var i=n(40),r=n(31);e.exports=Object.keys||function(e){return i(e,r)}},function(e, t){e.exports=!0},function(e, t){var n=0,i=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+i).toString(36))}},function(e, t){t.f={}.propertyIsEnumerable},function(e, t, n){var i=n(5),r=n(15),o=n(60),s=n(9),a=n(7),l=function(e, t, n){var u,c,h,d=e&l.F,f=e&l.G,p=e&l.S,m=e&l.P,v=e&l.B,g=e&l.W,y=f?r:r[t]||(r[t]={}),b=y.prototype,_=f?i:p?i[t]:(i[t]||{}).prototype;for(u in f&&(n=t),n)(c=!d&&_&&void 0!==_[u])&&a(y,u)||(h=c?_[u]:n[u],y[u]=f&&"function"!=typeof _[u]?n[u]:v&&c?o(h,i):g&&_[u]==h?function(e){var t=function(t, n, i){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,i)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(h):m&&"function"==typeof h?o(Function.call,h):h,m&&((y.virtual||(y.virtual={}))[u]=h,e&l.R&&b&&!b[u]&&s(b,u,h)))};l.F=1,l.G=2,l.S=4,l.P=8,l.B=16,l.W=32,l.U=64,l.R=128,e.exports=l},function(e, t, n){var i=n(16);e.exports=function(e, t){if(!i(e))return e;var n,r;if(t&&"function"==typeof(n=e.toString)&&!i(r=n.call(e)))return r;if("function"==typeof(n=e.valueOf)&&!i(r=n.call(e)))return r;if(!t&&"function"==typeof(n=e.toString)&&!i(r=n.call(e)))return r;throw TypeError("Can't convert object to primitive value")}},function(e, t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e, t){var n=Math.ceil,i=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?i:n)(e)}},function(e, t, n){var i=n(30)("keys"),r=n(23);e.exports=function(e){return i[e]||(i[e]=r(e))}},function(e, t, n){var i=n(15),r=n(5),o=r["__core-js_shared__"]||(r["__core-js_shared__"]={});(e.exports=function(e, t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:i.version,mode:n(22)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(e, t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e, t){t.f=Object.getOwnPropertySymbols},function(e, t){e.exports={}},function(e, t, n){var i=n(10).f,r=n(7),o=n(13)("toStringTag");e.exports=function(e, t, n){e&&!r(e=n?e:e.prototype,o)&&i(e,o,{configurable:!0,value:t})}},function(e, t, n){t.f=n(13)},function(e, t, n){var i=n(5),r=n(15),o=n(22),s=n(35),a=n(10).f;e.exports=function(e){var t=r.Symbol||(r.Symbol=o?{}:i.Symbol||{});"_"==e.charAt(0)||e in t||a(t,e,{value:s.f(e)})}},function(e, t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e, t, n){e.exports=!n(11)&&!n(17)(function(){return 7!=Object.defineProperty(n(39)("div"),"a",{get:function(){return 7}}).a})},function(e, t, n){var i=n(16),r=n(5).document,o=i(r)&&i(r.createElement);e.exports=function(e){return o?r.createElement(e):{}}},function(e, t, n){var i=n(7),r=n(12),o=n(63)(!1),s=n(29)("IE_PROTO");e.exports=function(e, t){var n,a=r(e),l=0,u=[];for(n in a)n!=s&&i(a,n)&&u.push(n);for(; t.length>l;)i(a,n=t[l++])&&(~o(u,n)||u.push(n));return u}},function(e, t, n){var i=n(42);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==i(e)?e.split(""):Object(e)}},function(e, t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e, t, n){var i=n(27);e.exports=function(e){return Object(i(e))}},function(e, t, n){"use strict";var i=n(22),r=n(25),o=n(45),s=n(9),a=n(33),l=n(70),u=n(34),c=n(73),h=n(13)("iterator"),d=!([].keys&&"next"in[].keys()),f=function(){return this};e.exports=function(e, t, n, p, m, v, g){l(n,t,p);var y,b,_,w=function(e){if(!d&&e in S)return S[e];switch(e){case"keys":case"values":return function(){return new n(this,e)}}return function(){return new n(this,e)}},x=t+" Iterator",C="values"==m,k=!1,S=e.prototype,D=S[h]||S["@@iterator"]||m&&S[m],E=D||w(m),$=m?C?w("entries"):E:void 0,T="Array"==t&&S.entries||D;if(T&&(_=c(T.call(new e)))!==Object.prototype&&_.next&&(u(_,x,!0),i||"function"==typeof _[h]||s(_,h,f)),C&&D&&"values"!==D.name&&(k=!0,E=function(){return D.call(this)}),i&&!g||!d&&!k&&S[h]||s(S,h,E),a[t]=E,a[x]=f,m)if(y={values:C?E:w("values"),keys:v?E:w("keys"),entries:$},g)for(b in y)b in S||o(S,b,y[b]);else r(r.P+r.F*(d||k),t,y);return y}},function(e, t, n){e.exports=n(9)},function(e, t, n){var i=n(19),r=n(71),o=n(31),s=n(29)("IE_PROTO"),a=function(){},l=function(){var e,t=n(39)("iframe"),i=o.length;for(t.style.display="none",n(72).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java new file mode 100644 index 0000000..ebff3fd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.ruoyi.common.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java new file mode 100644 index 0000000..589d123 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java @@ -0,0 +1,55 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 通用http工具封装 + * + * @author ruoyi + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java new file mode 100644 index 0000000..534d21c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java @@ -0,0 +1,293 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.http.MediaType; + +/** + * 通用http发送方法 + * + * @author ruoyi + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); + } + } + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数 + * @param contentType 内容类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param, String contentType) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) + { + return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + public static String sendSSLPost(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..edfe419 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.utils.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; + +/** + * 获取地址类 + * + * @author ruoyi + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (RuoYiConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java new file mode 100644 index 0000000..8e89e30 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java @@ -0,0 +1,382 @@ +package com.ruoyi.common.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import javax.servlet.http.HttpServletRequest; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..ccab288 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.poi; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @param cell 单元格对象 + * @param wb 工作簿对象 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args, Cell cell, Workbook wb); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..c4191ef --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -0,0 +1,1893 @@ +package com.ruoyi.common.utils.poi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileTypeUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.ImageUtils; +import com.ruoyi.common.utils.reflect.ReflectUtils; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String SEPARATOR = ","; + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * 用于dictType属性数据存储,避免重复查缓存 + */ + public Map sysDictMap = new HashMap(); + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要显示列属性 + */ + public String[] includeFields; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 仅在Excel中显示列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void showColumn(String... fields) + { + this.includeFields = fields; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + Row subRow = sheet.createRow(rownum); + int column = 0; + int subFieldSize = subFields != null ? subFields.size() : 0; + for (Object[] objects : fields) + { + Field field = (Field) objects[0]; + Excel attr = (Excel) objects[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + Cell cell = subRow.createCell(column); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (subFieldSize > 1) + { + CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1); + sheet.addMergedRegion(cellAddress); + } + column += subFieldSize; + } + else + { + Cell cell = subRow.createCell(column++); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + } + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) + { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) + { + List list = null; + try + { + list = importExcel(StringUtils.EMPTY, is, titleNum); + } + catch (Exception e) + { + log.error("导入Excel异常{}", e.getMessage()); + throw new UtilException(e.getMessage()); + } + finally + { + IOUtils.closeQuietly(is); + } + return list; + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map> pictures = null; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (s.matches("^\\d+\\.0$")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (StringUtils.isNotEmpty(attr.dictType())) + { + if (!sysDictMap.containsKey(attr.dictType() + val)) + { + String dictValue = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + sysDictMap.put(attr.dictType() + val, dictValue); + } + val = sysDictMap.get(attr.dictType() + val); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr, null); + } + else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) + { + StringBuilder propertyString = new StringBuilder(); + List images = pictures.get(row.getRowNum() + "_" + entry.getKey()); + for (PictureData picture : images) + { + byte[] data = picture.getData(); + String fileName = FileUtils.writeImportBytes(data); + propertyString.append(fileName).append(SEPARATOR); + } + val = StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) + { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) + { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int currentRowNum = rownum + 1; // 从标题行后开始 + + for (int i = startNo; i < endNo; i++) + { + row = sheet.createRow(currentRowNum); + T vo = (T) list.get(i); + int column = 0; + int maxSubListSize = getCurrentMaxSubListSize(vo); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, excel); + if (subList != null && !subList.isEmpty()) + { + int subIndex = 0; + for (Object subVo : subList) + { + Row subRow = sheet.getRow(currentRowNum + subIndex); + if (subRow == null) + { + subRow = sheet.createRow(currentRowNum + subIndex); + } + + int subColumn = column; + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + addCell(subExcel, subRow, (T) subVo, subField, subColumn++); + } + subIndex++; + } + column += subFields.size(); + } + } + catch (Exception e) + { + log.error("填充集合数据失败", e); + } + } + else + { + // 创建单元格并设置值 + addCell(excel, row, vo, field, column); + if (maxSubListSize > 1 && excel.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column)); + } + column++; + } + } + currentRowNum += maxSubListSize; + } + } + + /** + * 获取子列表最大数 + */ + private int getCurrentMaxSubListSize(T vo) + { + int maxSubListSize = 1; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, (Excel) os[1]); + if (subList != null && !subList.isEmpty()) + { + maxSubListSize = Math.max(maxSubListSize, subList.size()); + } + } + catch (Exception e) + { + log.error("获取集合大小失败", e); + } + } + } + return maxSubListSize; + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setDataFormat(dataFormat.getFormat("######0.00")); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + // 设置表格头单元格文本形式 + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + List subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + annotationDataStyles(styles, subField, subExcel); + } + } + else + { + annotationDataStyles(styles, field, excel); + } + } + return styles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param styles 自定义样式列表 + * @param field 属性列信息 + * @param excel 注解信息 + */ + public void annotationDataStyles(Map styles, Field field, Excel excel) + { + String key = StringUtils.format("data_{}_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType(), excel.wrapText()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + style.setWrapText(excel.wrapText()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + if (ColumnType.TEXT == excel.cellType()) + { + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + } + styles.put(key, style); + } + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String propertyValue = Convert.toStr(value); + if (StringUtils.isNotEmpty(propertyValue)) + { + List imagePaths = StringUtils.str2List(propertyValue, SEPARATOR); + for (String imagePath : imagePaths) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0 || attr.comboReadDict()) + { + String[] comboArray = attr.combo(); + if (attr.comboReadDict()) + { + if (!sysDictMap.containsKey("combo_" + attr.dictType())) + { + String labels = DictUtils.getDictLabels(attr.dictType()); + sysDictMap.put("combo_" + attr.dictType(), labels); + } + String val = sysDictMap.get("combo_" + attr.dictType()); + comboArray = StringUtils.split(val, DictUtils.SEPARATOR); + } + if (comboArray.length > 15 || StringUtils.join(comboArray).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) + { + if (subMergedLastRowNum >= subMergedFirstRowNum) + { + sheet.addMergedRegion(new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column)); + } + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.getCellStyle().setDataFormat(this.wb.getCreationHelper().createDataFormat().getFormat(dateFormat)); + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) + { + if (!sysDictMap.containsKey(dictType + value)) + { + String lable = convertDictByExp(Convert.toStr(value), dictType, separator); + sysDictMap.put(dictType + value, lable); + } + cell.setCellValue(sysDictMap.get(dictType + value)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + Name name = wb.createName(); + name.setNameName(hideSheetName + "_data"); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data"); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) + { + try + { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class }); + value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(statistics.get(key)); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + return UUID.randomUUID() + "_" + filename + ".xlsx"; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = RuoYiConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + field.setAccessible(true); + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (StringUtils.isNotEmpty(includeFields)) + { + for (Field field : tempFields) + { + if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class)) + { + addField(fields, field); + } + } + } + else if (StringUtils.isNotEmpty(excludeFields)) + { + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + addField(fields, field); + } + } + } + else + { + for (Field field : tempFields) + { + addField(fields, field); + } + } + return fields; + } + + /** + * 添加字段信息 + */ + public void addField(List fields, Field field) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (StringUtils.isNotEmpty(includeFields)) + { + if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + else + { + if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + } + } + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty() && sheet.getDrawingPatriarch() != null) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + HSSFClientAnchor anchor = (HSSFClientAnchor) pic.getAnchor(); + String picIndex = anchor.getRow1() + "_" + anchor.getCol1(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + return sheetIndexPicMap; + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + for (XSSFShape shape : drawing.getShapes()) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) + { + Object value; + try + { + value = subMethod.invoke(obj, new Object[] {}); + } + catch (Exception e) + { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..b19953e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,410 @@ +package com.ruoyi.common.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java new file mode 100644 index 0000000..ca1cd92 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.ruoyi.common.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java new file mode 100644 index 0000000..c1c58db --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.utils.sign; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Md5加密方法 + * + * @author ruoyi + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..4e3f603 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java @@ -0,0 +1,164 @@ +package com.ruoyi.common.utils.spring; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + Object proxy = AopContext.currentProxy(); + if (((Advised) proxy).getTargetSource().getTargetClass() == invoker.getClass()) + { + return (T) proxy; + } + return invoker; + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..48720dc --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java @@ -0,0 +1,70 @@ +package com.ruoyi.common.utils.sql; + +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 限制orderBy最大长度 + */ + private static final int ORDER_BY_MAX_LENGTH = 500; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) + { + throw new UtilException("参数已超过最大限制,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) + { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java new file mode 100644 index 0000000..2c84427 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils.uuid; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java new file mode 100644 index 0000000..bf99611 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java new file mode 100644 index 0000000..a5585d6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.ruoyi.common.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.ruoyi.common.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java new file mode 100644 index 0000000..7bfdf04 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java @@ -0,0 +1,27 @@ +package com.ruoyi.common.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java new file mode 100644 index 0000000..42f425c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.xss; + +import com.ruoyi.common.utils.StringUtils; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + StringBuilder sHtml = new StringBuilder(); + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) + { + sHtml.append(matcher.group()); + } + return pattern.matcher(sHtml).matches(); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/resources/api.properties b/ruoyi-common/src/main/resources/api.properties new file mode 100644 index 0000000..82620f9 --- /dev/null +++ b/ruoyi-common/src/main/resources/api.properties @@ -0,0 +1,3 @@ +api.datacenterId=1 +api.workerId=1 +api.domainName=http://xxx.com \ No newline at end of file diff --git a/ruoyi-common/src/main/resources/shop.properties b/ruoyi-common/src/main/resources/shop.properties new file mode 100644 index 0000000..18eb0ea --- /dev/null +++ b/ruoyi-common/src/main/resources/shop.properties @@ -0,0 +1,19 @@ +shop.qiniu.resourcesUrl=https://qiniu.pxdj.tashowz.com/ +shop.qiniu.accessKey=M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF +shop.qiniu.secretKey=Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl +shop.qiniu.bucket=pxdj-prod +# \u5177\u4F53\u67E5\u770BQiniuZone.java +shop.qiniu.zone=HUA_NAN + +shop.aLiDaYu.accessKeyId= +shop.aLiDaYu.accessKeySecret= +shop.aLiDaYu.signName= + +shop.tokenAesKey=q5e6w6w9f4q497te + +# \u672C\u5730\u6587\u4EF6\u4E0A\u4F20\u5B58\u50A8\u8DEF\u5F84 +shop.imgUpload.imagePath=D:/Mall4j/IMG/ +# 1.\u672C\u5730\u6587\u4EF6\u4E0A\u4F20 2.\u4E03\u725B\u4E91 \u5982\u679C\u9009\u62E9\u4E86\u4E03\u725B\u4E91\u5C31\u4E0D\u9700\u8981\u518D\u914D\u7F6E\u672C\u5730\u6587\u4EF6\u4E0A\u4F20 +shop.imgUpload.uploadType=2 +# \u672C\u5730\u6587\u4EF6\u4E0A\u4F20\u7684\u8DEF\u5F84 +shop.imgUpload.resourceUrl=http://192.168.5.11:8086/mall4j/img/ \ No newline at end of file diff --git a/ruoyi-common/target/classes/api.properties b/ruoyi-common/target/classes/api.properties new file mode 100644 index 0000000..82620f9 --- /dev/null +++ b/ruoyi-common/target/classes/api.properties @@ -0,0 +1,3 @@ +api.datacenterId=1 +api.workerId=1 +api.domainName=http://xxx.com \ No newline at end of file diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Anonymous.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Anonymous.class new file mode 100644 index 0000000..4df8326 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Anonymous.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataScope.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataScope.class new file mode 100644 index 0000000..051a3f3 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataScope.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataSource.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataSource.class new file mode 100644 index 0000000..d11a3e3 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/DataSource.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$ColumnType.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$ColumnType.class new file mode 100644 index 0000000..a678fb8 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$ColumnType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$Type.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$Type.class new file mode 100644 index 0000000..311fe81 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$Type.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel.class new file mode 100644 index 0000000..c83b1f6 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excels.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excels.class new file mode 100644 index 0000000..b9a8015 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Excels.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Log.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Log.class new file mode 100644 index 0000000..8c6aeda Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Log.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/RateLimiter.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/RateLimiter.class new file mode 100644 index 0000000..aed252c Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/RateLimiter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/RepeatSubmit.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/RepeatSubmit.class new file mode 100644 index 0000000..016e281 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/RepeatSubmit.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/annotation/Sensitive.class b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Sensitive.class new file mode 100644 index 0000000..0826afd Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/annotation/Sensitive.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/config/FileUploadConfig.class b/ruoyi-common/target/classes/com/ruoyi/common/config/FileUploadConfig.class new file mode 100644 index 0000000..d083884 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/config/FileUploadConfig.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/config/Qiniu.class b/ruoyi-common/target/classes/com/ruoyi/common/config/Qiniu.class new file mode 100644 index 0000000..4a69fcf Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/config/Qiniu.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/config/RuoYiConfig.class b/ruoyi-common/target/classes/com/ruoyi/common/config/RuoYiConfig.class new file mode 100644 index 0000000..f7c0316 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/config/RuoYiConfig.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.class b/ruoyi-common/target/classes/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.class new file mode 100644 index 0000000..422ed6d Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/CacheConstants.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/CacheConstants.class new file mode 100644 index 0000000..98477c2 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/CacheConstants.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/Constants.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/Constants.class new file mode 100644 index 0000000..cc8ab2d Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/Constants.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/GenConstants.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/GenConstants.class new file mode 100644 index 0000000..fefe61c Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/GenConstants.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/HttpStatus.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/HttpStatus.class new file mode 100644 index 0000000..6db3b9d Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/HttpStatus.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants$Status.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants$Status.class new file mode 100644 index 0000000..a5ba666 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants$Status.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants.class new file mode 100644 index 0000000..c6f9ee9 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/constant/UserConstants.class b/ruoyi-common/target/classes/com/ruoyi/common/constant/UserConstants.class new file mode 100644 index 0000000..a16ee2f Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/constant/UserConstants.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController$1.class b/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController$1.class new file mode 100644 index 0000000..ef9dbba Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController$1.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController.class b/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController.class new file mode 100644 index 0000000..985bb7b Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/AjaxResult.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/AjaxResult.class new file mode 100644 index 0000000..8629d54 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/AjaxResult.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/BaseEntity.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/BaseEntity.class new file mode 100644 index 0000000..6187927 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/BaseEntity.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/R.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/R.class new file mode 100644 index 0000000..eb936d1 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/R.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeEntity.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeEntity.class new file mode 100644 index 0000000..6f0d4b7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeEntity.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeSelect.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeSelect.class new file mode 100644 index 0000000..a5c65d7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeSelect.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDept.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDept.class new file mode 100644 index 0000000..78eef07 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDept.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictData.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictData.class new file mode 100644 index 0000000..52459db Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictData.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictType.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictType.class new file mode 100644 index 0000000..962a55e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysMenu.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysMenu.class new file mode 100644 index 0000000..5370df4 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysMenu.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysRole.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysRole.class new file mode 100644 index 0000000..9d1d9cb Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysRole.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysUser.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysUser.class new file mode 100644 index 0000000..91e427c Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysUser.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginBody.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginBody.class new file mode 100644 index 0000000..663f654 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginBody.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginUser.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginUser.class new file mode 100644 index 0000000..f254d72 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginUser.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/RegisterBody.class b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/RegisterBody.class new file mode 100644 index 0000000..aa2484e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/RegisterBody.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/page/PageDomain.class b/ruoyi-common/target/classes/com/ruoyi/common/core/page/PageDomain.class new file mode 100644 index 0000000..9bb7c90 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/page/PageDomain.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableDataInfo.class b/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableDataInfo.class new file mode 100644 index 0000000..6653683 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableDataInfo.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableSupport.class b/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableSupport.class new file mode 100644 index 0000000..fd3c76f Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/page/TableSupport.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/redis/RedisCache.class b/ruoyi-common/target/classes/com/ruoyi/common/core/redis/RedisCache.class new file mode 100644 index 0000000..e676831 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/redis/RedisCache.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/text/CharsetKit.class b/ruoyi-common/target/classes/com/ruoyi/common/core/text/CharsetKit.class new file mode 100644 index 0000000..8bac4af Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/text/CharsetKit.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/text/Convert.class b/ruoyi-common/target/classes/com/ruoyi/common/core/text/Convert.class new file mode 100644 index 0000000..cd5c3ee Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/text/Convert.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/core/text/StrFormatter.class b/ruoyi-common/target/classes/com/ruoyi/common/core/text/StrFormatter.class new file mode 100644 index 0000000..21457eb Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/core/text/StrFormatter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessStatus.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessStatus.class new file mode 100644 index 0000000..b114935 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessStatus.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessType.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessType.class new file mode 100644 index 0000000..23d18f3 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/DataSourceType.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/DataSourceType.class new file mode 100644 index 0000000..0e9255b Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/DataSourceType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/DesensitizedType.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/DesensitizedType.class new file mode 100644 index 0000000..9c8ca28 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/DesensitizedType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/HttpMethod.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/HttpMethod.class new file mode 100644 index 0000000..035d56f Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/HttpMethod.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/LimitType.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/LimitType.class new file mode 100644 index 0000000..29344ef Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/LimitType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/OperatorType.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/OperatorType.class new file mode 100644 index 0000000..e96d029 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/OperatorType.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/QiniuZone.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/QiniuZone.class new file mode 100644 index 0000000..24100d1 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/QiniuZone.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/enums/UserStatus.class b/ruoyi-common/target/classes/com/ruoyi/common/enums/UserStatus.class new file mode 100644 index 0000000..0e7c511 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/enums/UserStatus.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/DemoModeException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/DemoModeException.class new file mode 100644 index 0000000..d4f08ec Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/DemoModeException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/GlobalException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/GlobalException.class new file mode 100644 index 0000000..c071c49 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/GlobalException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/ServiceException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/ServiceException.class new file mode 100644 index 0000000..8092774 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/ServiceException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/UtilException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/UtilException.class new file mode 100644 index 0000000..e811c26 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/UtilException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/base/BaseException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/base/BaseException.class new file mode 100644 index 0000000..782d1f8 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/base/BaseException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileException.class new file mode 100644 index 0000000..740bb88 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.class new file mode 100644 index 0000000..d5b726c Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileSizeLimitExceededException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileSizeLimitExceededException.class new file mode 100644 index 0000000..b137b11 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileSizeLimitExceededException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileUploadException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileUploadException.class new file mode 100644 index 0000000..bb40536 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileUploadException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidFlashExtensionException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidFlashExtensionException.class new file mode 100644 index 0000000..f8491df Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidFlashExtensionException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidImageExtensionException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidImageExtensionException.class new file mode 100644 index 0000000..9f91778 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidImageExtensionException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidMediaExtensionException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidMediaExtensionException.class new file mode 100644 index 0000000..55ee3d7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidMediaExtensionException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidVideoExtensionException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidVideoExtensionException.class new file mode 100644 index 0000000..dd67c8e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidVideoExtensionException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException.class new file mode 100644 index 0000000..15debe0 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException$Code.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException$Code.class new file mode 100644 index 0000000..e2bedc0 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException$Code.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException.class new file mode 100644 index 0000000..3eb83b3 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/BlackListException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/BlackListException.class new file mode 100644 index 0000000..abe2843 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/BlackListException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaException.class new file mode 100644 index 0000000..26fca8e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaExpireException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaExpireException.class new file mode 100644 index 0000000..6c93dff Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/CaptchaExpireException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserException.class new file mode 100644 index 0000000..9144e9b Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserNotExistsException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserNotExistsException.class new file mode 100644 index 0000000..9a98746 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserNotExistsException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordNotMatchException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordNotMatchException.class new file mode 100644 index 0000000..0c57a24 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordNotMatchException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.class b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.class new file mode 100644 index 0000000..bad56eb Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/PropertyPreExcludeFilter.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/PropertyPreExcludeFilter.class new file mode 100644 index 0000000..6a34c93 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/PropertyPreExcludeFilter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatableFilter.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatableFilter.class new file mode 100644 index 0000000..91d9a70 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatableFilter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper$1.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper$1.class new file mode 100644 index 0000000..62b9f8a Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper$1.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper.class new file mode 100644 index 0000000..ef43945 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/RepeatedlyRequestWrapper.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/XssFilter.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssFilter.class new file mode 100644 index 0000000..1036bff Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssFilter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper$1.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper$1.class new file mode 100644 index 0000000..dcb874f Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper$1.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper.class b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper.class new file mode 100644 index 0000000..4a41eb9 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/filter/XssHttpServletRequestWrapper.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/Arith.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/Arith.class new file mode 100644 index 0000000..8fb5d96 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/Arith.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/DateUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/DateUtils.class new file mode 100644 index 0000000..041ece7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/DateUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/DesensitizedUtil.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/DesensitizedUtil.class new file mode 100644 index 0000000..b600cb5 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/DesensitizedUtil.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/DictUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/DictUtils.class new file mode 100644 index 0000000..2f834e4 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/DictUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/ExceptionUtil.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/ExceptionUtil.class new file mode 100644 index 0000000..ab001d9 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/ExceptionUtil.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/LogUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/LogUtils.class new file mode 100644 index 0000000..360963e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/LogUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/MessageUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/MessageUtils.class new file mode 100644 index 0000000..a2331ee Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/MessageUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/PageUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/PageUtils.class new file mode 100644 index 0000000..08a6961 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/PageUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/SecurityUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/SecurityUtils.class new file mode 100644 index 0000000..7f14110 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/SecurityUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/ServletUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/ServletUtils.class new file mode 100644 index 0000000..5bfae25 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/ServletUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/StringUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/StringUtils.class new file mode 100644 index 0000000..77ce9e8 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/StringUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/Threads.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/Threads.class new file mode 100644 index 0000000..df7a7ee Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/Threads.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanUtils.class new file mode 100644 index 0000000..c51fc3e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanValidators.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanValidators.class new file mode 100644 index 0000000..c4563cd Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/bean/BeanValidators.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileTypeUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileTypeUtils.class new file mode 100644 index 0000000..f7afd62 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileTypeUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUploadUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUploadUtils.class new file mode 100644 index 0000000..297a4f8 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUploadUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUtils.class new file mode 100644 index 0000000..5e83ec1 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/FileUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/file/ImageUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/ImageUtils.class new file mode 100644 index 0000000..6237526 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/ImageUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/file/MimeTypeUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/MimeTypeUtils.class new file mode 100644 index 0000000..ac2380a Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/file/MimeTypeUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/html/EscapeUtil.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/html/EscapeUtil.class new file mode 100644 index 0000000..5842bbc Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/html/EscapeUtil.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/html/HTMLFilter.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/html/HTMLFilter.class new file mode 100644 index 0000000..1399a91 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/html/HTMLFilter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpHelper.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpHelper.class new file mode 100644 index 0000000..6d84868 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpHelper.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyHostnameVerifier.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyHostnameVerifier.class new file mode 100644 index 0000000..db47e06 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyHostnameVerifier.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyTrustManager.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyTrustManager.class new file mode 100644 index 0000000..a0f5b81 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils$TrustAnyTrustManager.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils.class new file mode 100644 index 0000000..e997c01 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/http/HttpUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/AddressUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/AddressUtils.class new file mode 100644 index 0000000..08ee620 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/AddressUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/IpUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/IpUtils.class new file mode 100644 index 0000000..85bfceb Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/ip/IpUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.class new file mode 100644 index 0000000..d898a98 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelUtil.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelUtil.class new file mode 100644 index 0000000..2ef5599 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/poi/ExcelUtil.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/reflect/ReflectUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/reflect/ReflectUtils.class new file mode 100644 index 0000000..8ddcc7b Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/reflect/ReflectUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Base64.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Base64.class new file mode 100644 index 0000000..0a7ede7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Base64.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Md5Utils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Md5Utils.class new file mode 100644 index 0000000..616de5b Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/sign/Md5Utils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/spring/SpringUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/spring/SpringUtils.class new file mode 100644 index 0000000..2c94cb7 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/spring/SpringUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/sql/SqlUtil.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/sql/SqlUtil.class new file mode 100644 index 0000000..0018ade Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/sql/SqlUtil.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/IdUtils.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/IdUtils.class new file mode 100644 index 0000000..5e14070 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/IdUtils.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/Seq.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/Seq.class new file mode 100644 index 0000000..4993821 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/Seq.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID$Holder.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID$Holder.class new file mode 100644 index 0000000..d5b6bde Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID$Holder.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID.class b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID.class new file mode 100644 index 0000000..0aba04e Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/utils/uuid/UUID.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/xss/Xss.class b/ruoyi-common/target/classes/com/ruoyi/common/xss/Xss.class new file mode 100644 index 0000000..c219826 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/xss/Xss.class differ diff --git a/ruoyi-common/target/classes/com/ruoyi/common/xss/XssValidator.class b/ruoyi-common/target/classes/com/ruoyi/common/xss/XssValidator.class new file mode 100644 index 0000000..4f3bda2 Binary files /dev/null and b/ruoyi-common/target/classes/com/ruoyi/common/xss/XssValidator.class differ diff --git a/ruoyi-common/target/classes/shop.properties b/ruoyi-common/target/classes/shop.properties new file mode 100644 index 0000000..18eb0ea --- /dev/null +++ b/ruoyi-common/target/classes/shop.properties @@ -0,0 +1,19 @@ +shop.qiniu.resourcesUrl=https://qiniu.pxdj.tashowz.com/ +shop.qiniu.accessKey=M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF +shop.qiniu.secretKey=Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl +shop.qiniu.bucket=pxdj-prod +# \u5177\u4F53\u67E5\u770BQiniuZone.java +shop.qiniu.zone=HUA_NAN + +shop.aLiDaYu.accessKeyId= +shop.aLiDaYu.accessKeySecret= +shop.aLiDaYu.signName= + +shop.tokenAesKey=q5e6w6w9f4q497te + +# \u672C\u5730\u6587\u4EF6\u4E0A\u4F20\u5B58\u50A8\u8DEF\u5F84 +shop.imgUpload.imagePath=D:/Mall4j/IMG/ +# 1.\u672C\u5730\u6587\u4EF6\u4E0A\u4F20 2.\u4E03\u725B\u4E91 \u5982\u679C\u9009\u62E9\u4E86\u4E03\u725B\u4E91\u5C31\u4E0D\u9700\u8981\u518D\u914D\u7F6E\u672C\u5730\u6587\u4EF6\u4E0A\u4F20 +shop.imgUpload.uploadType=2 +# \u672C\u5730\u6587\u4EF6\u4E0A\u4F20\u7684\u8DEF\u5F84 +shop.imgUpload.resourceUrl=http://192.168.5.11:8086/mall4j/img/ \ No newline at end of file diff --git a/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..8920530 --- /dev/null +++ b/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,124 @@ +com\ruoyi\common\filter\XssHttpServletRequestWrapper.class +com\ruoyi\common\utils\ip\IpUtils.class +com\ruoyi\common\utils\uuid\IdUtils.class +com\ruoyi\common\annotation\RepeatSubmit.class +com\ruoyi\common\constant\GenConstants.class +com\ruoyi\common\core\domain\TreeEntity.class +com\ruoyi\common\exception\base\BaseException.class +com\ruoyi\common\utils\ip\AddressUtils.class +com\ruoyi\common\constant\HttpStatus.class +com\ruoyi\common\utils\DesensitizedUtil.class +com\ruoyi\common\utils\sign\Base64.class +com\ruoyi\common\xss\Xss.class +com\ruoyi\common\exception\user\UserNotExistsException.class +com\ruoyi\common\core\domain\model\LoginBody.class +com\ruoyi\common\exception\user\UserPasswordNotMatchException.class +com\ruoyi\common\annotation\Log.class +com\ruoyi\common\core\controller\BaseController$1.class +com\ruoyi\common\utils\file\ImageUtils.class +com\ruoyi\common\config\RuoYiConfig.class +com\ruoyi\common\enums\BusinessStatus.class +com\ruoyi\common\utils\sign\Md5Utils.class +com\ruoyi\common\config\Qiniu.class +com\ruoyi\common\utils\http\HttpUtils$TrustAnyHostnameVerifier.class +com\ruoyi\common\exception\file\FileSizeLimitExceededException.class +com\ruoyi\common\utils\Arith.class +com\ruoyi\common\annotation\Excel$ColumnType.class +com\ruoyi\common\annotation\DataScope.class +com\ruoyi\common\core\domain\entity\SysDictType.class +com\ruoyi\common\core\text\StrFormatter.class +com\ruoyi\common\annotation\Excels.class +com\ruoyi\common\config\serializer\SensitiveJsonSerializer.class +com\ruoyi\common\core\domain\entity\SysDictData.class +com\ruoyi\common\utils\http\HttpUtils$TrustAnyTrustManager.class +com\ruoyi\common\enums\UserStatus.class +com\ruoyi\common\exception\user\CaptchaExpireException.class +com\ruoyi\common\annotation\Excel$Type.class +com\ruoyi\common\exception\job\TaskException$Code.class +com\ruoyi\common\utils\spring\SpringUtils.class +com\ruoyi\common\utils\file\FileUploadUtils.class +com\ruoyi\common\filter\RepeatedlyRequestWrapper$1.class +com\ruoyi\common\utils\bean\BeanUtils.class +com\ruoyi\common\utils\sql\SqlUtil.class +com\ruoyi\common\config\FileUploadConfig.class +com\ruoyi\common\utils\file\MimeTypeUtils.class +com\ruoyi\common\enums\BusinessType.class +com\ruoyi\common\filter\PropertyPreExcludeFilter.class +com\ruoyi\common\core\domain\model\LoginUser.class +com\ruoyi\common\enums\DesensitizedType.class +com\ruoyi\common\exception\file\InvalidExtensionException$InvalidMediaExtensionException.class +com\ruoyi\common\exception\file\FileUploadException.class +com\ruoyi\common\utils\poi\ExcelUtil.class +com\ruoyi\common\exception\user\UserException.class +com\ruoyi\common\utils\MessageUtils.class +com\ruoyi\common\enums\DataSourceType.class +com\ruoyi\common\exception\file\InvalidExtensionException$InvalidVideoExtensionException.class +com\ruoyi\common\enums\HttpMethod.class +com\ruoyi\common\utils\PageUtils.class +com\ruoyi\common\utils\LogUtils.class +com\ruoyi\common\annotation\DataSource.class +com\ruoyi\common\core\page\PageDomain.class +com\ruoyi\common\filter\XssHttpServletRequestWrapper$1.class +com\ruoyi\common\utils\ExceptionUtil.class +com\ruoyi\common\utils\http\HttpHelper.class +com\ruoyi\common\enums\LimitType.class +com\ruoyi\common\annotation\Sensitive.class +com\ruoyi\common\filter\RepeatedlyRequestWrapper.class +com\ruoyi\common\exception\file\InvalidExtensionException$InvalidImageExtensionException.class +com\ruoyi\common\core\controller\BaseController.class +com\ruoyi\common\utils\StringUtils.class +com\ruoyi\common\utils\poi\ExcelHandlerAdapter.class +com\ruoyi\common\exception\DemoModeException.class +com\ruoyi\common\enums\OperatorType.class +com\ruoyi\common\exception\UtilException.class +com\ruoyi\common\xss\XssValidator.class +com\ruoyi\common\utils\DateUtils.class +com\ruoyi\common\core\domain\entity\SysMenu.class +com\ruoyi\common\core\page\TableDataInfo.class +com\ruoyi\common\exception\GlobalException.class +com\ruoyi\common\exception\job\TaskException.class +com\ruoyi\common\constant\UserConstants.class +com\ruoyi\common\core\text\Convert.class +com\ruoyi\common\core\domain\entity\SysUser.class +com\ruoyi\common\utils\file\FileUtils.class +com\ruoyi\common\exception\user\BlackListException.class +com\ruoyi\common\core\domain\TreeSelect.class +com\ruoyi\common\filter\RepeatableFilter.class +com\ruoyi\common\constant\Constants.class +com\ruoyi\common\core\domain\entity\SysRole.class +com\ruoyi\common\annotation\Anonymous.class +com\ruoyi\common\exception\user\CaptchaException.class +com\ruoyi\common\core\domain\R.class +com\ruoyi\common\utils\bean\BeanValidators.class +com\ruoyi\common\core\domain\model\RegisterBody.class +com\ruoyi\common\exception\file\FileException.class +com\ruoyi\common\core\domain\BaseEntity.class +com\ruoyi\common\utils\SecurityUtils.class +com\ruoyi\common\core\redis\RedisCache.class +com\ruoyi\common\annotation\Excel.class +com\ruoyi\common\utils\uuid\UUID$Holder.class +com\ruoyi\common\annotation\RateLimiter.class +com\ruoyi\common\core\domain\entity\SysDept.class +com\ruoyi\common\core\page\TableSupport.class +com\ruoyi\common\utils\uuid\UUID.class +com\ruoyi\common\utils\http\HttpUtils.class +com\ruoyi\common\exception\file\InvalidExtensionException.class +com\ruoyi\common\constant\ScheduleConstants.class +com\ruoyi\common\exception\ServiceException.class +com\ruoyi\common\utils\file\FileTypeUtils.class +com\ruoyi\common\constant\ScheduleConstants$Status.class +com\ruoyi\common\utils\html\EscapeUtil.class +com\ruoyi\common\exception\file\InvalidExtensionException$InvalidFlashExtensionException.class +com\ruoyi\common\utils\ServletUtils.class +com\ruoyi\common\utils\html\HTMLFilter.class +com\ruoyi\common\utils\Threads.class +com\ruoyi\common\utils\uuid\Seq.class +com\ruoyi\common\filter\XssFilter.class +com\ruoyi\common\core\domain\AjaxResult.class +com\ruoyi\common\exception\file\FileNameLengthLimitExceededException.class +com\ruoyi\common\core\text\CharsetKit.class +com\ruoyi\common\utils\reflect\ReflectUtils.class +com\ruoyi\common\utils\DictUtils.class +com\ruoyi\common\constant\CacheConstants.class +com\ruoyi\common\exception\user\UserPasswordRetryLimitExceedException.class +com\ruoyi\common\enums\QiniuZone.class diff --git a/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..189d248 --- /dev/null +++ b/ruoyi-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,110 @@ +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\uuid\IdUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\HttpStatus.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\AjaxResult.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\filter\XssFilter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\page\TableSupport.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\DictUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\Log.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\LogUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\GenConstants.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\file\ImageUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\file\FileTypeUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\model\LoginUser.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\HttpMethod.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\SecurityUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\uuid\UUID.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\html\HTMLFilter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\spring\SpringUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\model\LoginBody.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\ExceptionUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\bean\BeanValidators.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\ip\AddressUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\UserPasswordNotMatchException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\file\FileUploadUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\poi\ExcelHandlerAdapter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\PageUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\file\MimeTypeUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\R.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\file\FileNameLengthLimitExceededException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\file\InvalidExtensionException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\text\Convert.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\BaseEntity.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\CaptchaException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysUser.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\Excel.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\sql\SqlUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\UserNotExistsException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\xss\Xss.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysDictType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\base\BaseException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\RateLimiter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\TreeEntity.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysMenu.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\Excels.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysDictData.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\BlackListException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\TreeSelect.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\BusinessType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\ip\IpUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\xss\XssValidator.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\DataSourceType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\filter\PropertyPreExcludeFilter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\DataSource.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\Threads.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\Sensitive.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\file\FileUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\uuid\Seq.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\config\RuoYiConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\DesensitizedUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\sign\Base64.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\config\FileUploadConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\page\PageDomain.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\Arith.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysRole.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\UserConstants.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\reflect\ReflectUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\Constants.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\ServiceException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\Anonymous.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\text\CharsetKit.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\CacheConstants.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\constant\ScheduleConstants.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\controller\BaseController.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\UserException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\poi\ExcelUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\OperatorType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\model\RegisterBody.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\ServletUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\LimitType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\html\EscapeUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\CaptchaExpireException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\job\TaskException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\file\FileUploadException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\UtilException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\UserStatus.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\filter\RepeatableFilter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\StringUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\redis\RedisCache.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\bean\BeanUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\config\serializer\SensitiveJsonSerializer.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\user\UserPasswordRetryLimitExceedException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\MessageUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\page\TableDataInfo.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\http\HttpHelper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\sign\Md5Utils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\DateUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\filter\XssHttpServletRequestWrapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\DesensitizedType.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\DataScope.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\filter\RepeatedlyRequestWrapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\DemoModeException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\QiniuZone.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\GlobalException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\utils\http\HttpUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\text\StrFormatter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\config\Qiniu.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\core\domain\entity\SysDept.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\annotation\RepeatSubmit.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\file\FileException.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\enums\BusinessStatus.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-common\src\main\java\com\ruoyi\common\exception\file\FileSizeLimitExceededException.java diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml new file mode 100644 index 0000000..e42a12f --- /dev/null +++ b/ruoyi-framework/pom.xml @@ -0,0 +1,64 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.alibaba + druid-spring-boot-starter + + + + + pro.fessional + kaptcha + + + servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + + com.ruoyi + ruoyi-system + + + + + \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..b2337c9 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,184 @@ +package com.ruoyi.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + List scopeCustomIds = new ArrayList(); + user.getRoles().forEach(role -> { + if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + scopeCustomIds.add(Convert.toStr(role.getRoleId())); + } + }); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE)) + { + continue; + } + if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + if (scopeCustomIds.size() > 1) + { + // 多个自定数据权限使用in查询,避免多次拼接。 + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds))); + } + else + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId())); + } + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据 + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..8c2c9f4 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Objects; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * + * @author ruoyi + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect +{ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" + + "|| @within(com.ruoyi.common.annotation.DataSource)") + public void dsPointCut() + { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) + { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + return point.proceed(); + } + finally + { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..bacf820 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java @@ -0,0 +1,256 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Collection; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessStatus; +import com.ruoyi.common.enums.HttpMethod; +import com.ruoyi.common.filter.PropertyPreExcludeFilter; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段 */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** 计算操作消耗时间 */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + /** + * 处理请求前执行 + */ + @Before(value = "@annotation(controllerLog)") + public void doBefore(JoinPoint joinPoint, Log controllerLog) + { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(); + operLog.setOperIp(ip); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + if (loginUser != null) + { + operLog.setOperName(loginUser.getUsername()); + SysUser currentUser = loginUser.getUser(); + if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) + { + operLog.setDeptName(currentUser.getDept().getDeptName()); + } + } + + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 设置消耗时间 + operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + // 保存数据库 + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + catch (Exception exp) + { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + finally + { + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) + { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog, log.excludeParamNames()); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception + { + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + String requestMethod = operLog.getRequestMethod(); + if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) + { + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + } + else + { + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) + { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params += jsonObj.toString() + " "; + } + catch (Exception e) + { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) + { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..b720bc1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; + +/** + * 限流处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RateLimiterAspect +{ + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try + { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr()).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..1d4dc1f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java @@ -0,0 +1,30 @@ +package com.ruoyi.framework.config; + +import java.util.TimeZone; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author ruoyi + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.ruoyi.**.mapper") +public class ApplicationConfig +{ + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..43e78ae --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.ruoyi.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CorsInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CorsInterceptor.java new file mode 100644 index 0000000..6161b26 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CorsInterceptor.java @@ -0,0 +1,49 @@ +package com.ruoyi.framework.config; + +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CorsInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + //表示接受任意域名的请求,也可以指定域名 + response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin")); + + //该字段可选,是个布尔值,表示是否可以携带cookie + response.setHeader("Access-Control-Allow-Credentials", "true"); + + response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"); + + response.setHeader("Access-Control-Allow-Headers", "*"); + + + //这里可以不加,但是其他语言开发的话记得处理options请求 + /** + * 非简单请求是对那种对服务器有特殊要求的请求, + * 比如请求方式是PUT或者DELETE,或者Content-Type字段类型是application/json。 + * 都会在正式通信之前,增加一次HTTP请求,称之为预检。浏览器会先询问服务器,当前网页所在域名是否在服务器的许可名单之中, + * 服务器允许之后,浏览器会发出正式的XMLHttpRequest请求 + */ + if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { + return true; + } + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java new file mode 100644 index 0000000..f6abac1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java @@ -0,0 +1,126 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.DruidProperties; +import com.ruoyi.framework.datasource.DynamicDataSource; + +/** + * druid 配置多数据源 + * + * @author ruoyi + */ +@Configuration +public class DruidConfig +{ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) + { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..4adbb7f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.ruoyi.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.ruoyi.common.constant.Constants; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java new file mode 100644 index 0000000..bb14c04 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java @@ -0,0 +1,58 @@ +package com.ruoyi.framework.config; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.DispatcherType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.filter.RepeatableFilter; +import com.ruoyi.common.filter.XssFilter; +import com.ruoyi.common.utils.StringUtils; + +/** + * Filter配置 + * + * @author ruoyi + */ +@Configuration +public class FilterConfig +{ + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java new file mode 100644 index 0000000..163fd01 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java @@ -0,0 +1,43 @@ +package com.ruoyi.framework.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; +import com.ruoyi.common.constant.Constants; + +/** + * 资源文件配置加载 + * + * @author ruoyi + */ +@Configuration +public class I18nConfig implements WebMvcConfigurer +{ + @Bean + public LocaleResolver localeResolver() + { + SessionLocaleResolver slr = new SessionLocaleResolver(); + // 默认语言 + slr.setDefaultLocale(Constants.DEFAULT_LOCALE); + return slr; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() + { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + // 参数名 + lci.setParamName("lang"); + return lci; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java new file mode 100644 index 0000000..ebc0a1f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.config; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 拦截器配置 + * + * 不用权限可以访问url /api/v1/pub/ + * 要登录可以访问url /api/v1/pri/ + */ + +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + /** + * 拦截全部路径,这个跨域需要放在最上面 + */ + registry.addInterceptor(new CorsInterceptor()).addPathPatterns("/**"); + + WebMvcConfigurer.super.addInterceptors(registry); + + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..7f8e1d5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java @@ -0,0 +1,68 @@ +package com.ruoyi.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java new file mode 100644 index 0000000..057c941 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java @@ -0,0 +1,132 @@ +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 org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +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; + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + public static String setTypeAliasesPackage(String typeAliasesPackage) + { + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + List allResult = new ArrayList(); + try + { + for (String aliasesPackage : typeAliasesPackage.split(",")) + { + List result = new ArrayList(); + 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 hashResult = new HashSet(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 resources = new ArrayList(); + 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 SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + return sessionFactory.getObject(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java new file mode 100644 index 0000000..3f4f485 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java @@ -0,0 +1,69 @@ +package com.ruoyi.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author ruoyi + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..0f48b11 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() + { + CorsConfiguration config = new CorsConfiguration(); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java new file mode 100644 index 0000000..c71f41b --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -0,0 +1,142 @@ +package com.ruoyi.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.ruoyi.framework.config.properties.PermitAllUrlProperties; +import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; +import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; +import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author ruoyi + */ +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +@Configuration +public class SecurityConfig +{ + /** + * 自定义用户认证逻辑 + */ + @Autowired + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + /** + * 允许匿名访问的地址 + */ + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 身份验证实现 + */ + @Bean + public AuthenticationManager authenticationManager() + { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + daoAuthenticationProvider.setUserDetailsService(userDetailsService); + daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder()); + return new ProviderManager(daoAuthenticationProvider); + } + + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception + { + return httpSecurity + // CSRF禁用,因为不使用session + .csrf(csrf -> csrf.disable()) + // 禁用HTTP响应标头 + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + }) + // 认证失败处理类 + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + // 基于token,所以不需要session + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 注解标记允许匿名访问的url + .authorizeHttpRequests((requests) -> { + permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + requests.antMatchers("/login", "/register", "/captchaImage").permitAll() + .antMatchers("/monitor/client/api/**").permitAll() + .antMatchers("/monitor/client/logs/upload").permitAll() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated(); + }) + // 添加Logout filter + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + // 添加JWT filter + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + // 添加CORS filter + .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) + .addFilterBefore(corsFilter, LogoutFilter.class) + .build(); + } + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java new file mode 100644 index 0000000..b5b7de3 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java @@ -0,0 +1,32 @@ +package com.ruoyi.framework.config; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 服务相关配置 + * + * @author ruoyi + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..7840141 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java @@ -0,0 +1,63 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author ruoyi + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小 + private int corePoolSize = 50; + + // 最大可创建的线程数 + private int maxPoolSize = 200; + + // 队列最大长度 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) + { + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..c8a5c8a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author ruoyi + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.connectTimeout}") + private int connectTimeout; + + @Value("${spring.datasource.druid.socketTimeout}") + private int socketTimeout; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ + datasource.setConnectTimeout(connectTimeout); + + /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ + datasource.setSocketTimeout(socketTimeout); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..29118fa --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,73 @@ +package com.ruoyi.framework.config.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.ruoyi.common.annotation.Anonymous; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author ruoyi + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware +{ + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() + { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException + { + this.applicationContext = context; + } + + public List getUrls() + { + return urls; + } + + public void setUrls(List urls) + { + this.urls = urls; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..e70b8cf --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * @author ruoyi + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() + { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..9770af6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.ruoyi.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author ruoyi + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..c49eaf4 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,56 @@ +package com.ruoyi.framework.interceptor; + +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * + * @author ruoyi + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request, annotation)) + { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request 请求信息 + * @param annotation 防重复注解参数 + * @return 结果 + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..9dc9511 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,110 @@ +package com.ruoyi.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.filter.RepeatedlyRequestWrapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author ruoyi + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java new file mode 100644 index 0000000..7387a02 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java @@ -0,0 +1,55 @@ +package com.ruoyi.framework.manager; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import com.ruoyi.common.utils.Threads; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 异步任务管理器 + * + * @author ruoyi + */ +public class AsyncManager +{ + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager(){} + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() + { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..e36ca3c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java @@ -0,0 +1,39 @@ +package com.ruoyi.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import javax.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author ruoyi + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..267e305 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.ruoyi.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.LogUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysLogininforService; +import com.ruoyi.system.service.ISysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; + +/** + * 异步工厂(产生任务用) + * + * @author ruoyi + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..6c776ce --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..5472f3d --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.ruoyi.common.core.text.Convert; + +/** + * 权限信息 + * + * @author ruoyi + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..3eb2495 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,44 @@ +package com.ruoyi.framework.security.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; + +/** + * token过滤器 验证token有效性 + * + * @author ruoyi + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + tokenService.verifyToken(loginUser); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..93b7032 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,34 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 认证失败处理类 返回未授权 + * + * @author ruoyi + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..2f89a91 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,53 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; + +/** + * 自定义退出处理类 返回成功 + * + * @author ruoyi + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java new file mode 100644 index 0000000..63b03da --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java @@ -0,0 +1,240 @@ +package com.ruoyi.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.web.domain.server.Cpu; +import com.ruoyi.framework.web.domain.server.Jvm; +import com.ruoyi.framework.web.domain.server.Mem; +import com.ruoyi.framework.web.domain.server.Sys; +import com.ruoyi.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息 + * + * @author ruoyi + */ +public class Server +{ + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() + { + return cpu; + } + + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + public Mem getMem() + { + return mem; + } + + public void setMem(Mem mem) + { + this.mem = mem; + } + + public Jvm getJvm() + { + return jvm; + } + + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + public Sys getSys() + { + return sys; + } + + public void setSys(Sys sys) + { + this.sys = sys; + } + + public List getSysFiles() + { + return sysFiles; + } + + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) + { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) + { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } + else + { + return String.format("%d B", size); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..a13a66c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java @@ -0,0 +1,101 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * CPU相关信息 + * + * @author ruoyi + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() + { + return cpuNum; + } + + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) + { + this.sys = sys; + } + + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) + { + this.used = used; + } + + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) + { + this.wait = wait; + } + + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) + { + this.free = free; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..1fdc6ac --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java @@ -0,0 +1,130 @@ +package com.ruoyi.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.DateUtils; + +/** + * JVM相关信息 + * + * @author ruoyi + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) + { + this.max = max; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) + { + this.free = free; + } + + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getHome() + { + return home; + } + + public void setHome(String home) + { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() + { + return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..13eec52 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java @@ -0,0 +1,61 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * 內存相关信息 + * + * @author ruoyi + */ +public class Mem +{ + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) + { + this.total = total; + } + + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) + { + this.used = used; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) + { + this.free = free; + } + + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..45d64d9 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java @@ -0,0 +1,84 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author ruoyi + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() + { + return computerName; + } + + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + public String getComputerIp() + { + return computerIp; + } + + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + public String getUserDir() + { + return userDir; + } + + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + public String getOsName() + { + return osName; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsArch() + { + return osArch; + } + + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..1320cde --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java @@ -0,0 +1,114 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() + { + return dirName; + } + + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + public String getSysTypeName() + { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getTotal() + { + return total; + } + + public void setTotal(String total) + { + this.total = total; + } + + public String getFree() + { + return free; + } + + public void setFree(String free) + { + this.free = free; + } + + public String getUsed() + { + return used; + } + + public void setUsed(String used) + { + this.used = used; + } + + public double getUsage() + { + return usage; + } + + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..3cb17d6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,145 @@ +package com.ruoyi.framework.web.exception; + +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.DemoModeException; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String value = Convert.toStr(e.getValue()); + if (StringUtils.isNotEmpty(value)) + { + value = EscapeUtil.clean(value); + } + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java new file mode 100644 index 0000000..07d259a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java @@ -0,0 +1,159 @@ +package com.ruoyi.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author ruoyi + */ +@Service("ss") +public class PermissionService +{ + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (StringUtils.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permission); + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (StringUtils.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permissions); + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (StringUtils.isEmpty(role)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (StringUtils.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(Constants.ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..fe16427 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -0,0 +1,181 @@ +package com.ruoyi.framework.web.service; + +import javax.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.exception.user.BlackListException; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.exception.user.UserNotExistsException; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) + { + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 用户验证 + Authentication authentication = null; + try + { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + redisCache.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // IP黑名单校验 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr()); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..6728c7b --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -0,0 +1,86 @@ +package com.ruoyi.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..c4d0fa5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java @@ -0,0 +1,88 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..8305bcb --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java @@ -0,0 +1,117 @@ +package com.ruoyi.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 注册校验方法 + * + * @author ruoyi + */ +@Component +public class SysRegisterService +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) + { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 验证码开关 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + else if (!userService.checkUserNameUnique(sysUser)) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + else + { + sysUser.setNickName(username); + sysUser.setPwdUpdateDate(DateUtils.getNowDate()); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + msg = "注册失败,请联系系统管理人员"; + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java new file mode 100644 index 0000000..680cab6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -0,0 +1,232 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser 登录信息 + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) + { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..5dcdf90 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,66 @@ +package com.ruoyi.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户验证处理 + * + * @author ruoyi + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataScopeAspect.class b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataScopeAspect.class new file mode 100644 index 0000000..dd5850a Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataScopeAspect.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataSourceAspect.class b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataSourceAspect.class new file mode 100644 index 0000000..0aa4283 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/DataSourceAspect.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/LogAspect.class b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/LogAspect.class new file mode 100644 index 0000000..759fe3d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/LogAspect.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/RateLimiterAspect.class b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/RateLimiterAspect.class new file mode 100644 index 0000000..52c7d9d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/aspectj/RateLimiterAspect.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/ApplicationConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ApplicationConfig.class new file mode 100644 index 0000000..b623e12 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ApplicationConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/CaptchaConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/CaptchaConfig.class new file mode 100644 index 0000000..3d10c47 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/CaptchaConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/CorsInterceptor.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/CorsInterceptor.class new file mode 100644 index 0000000..acd83cf Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/CorsInterceptor.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig$1.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig$1.class new file mode 100644 index 0000000..b8ab21d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig$1.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig.class new file mode 100644 index 0000000..7674723 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/DruidConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.class new file mode 100644 index 0000000..2b9bb58 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/FilterConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/FilterConfig.class new file mode 100644 index 0000000..83bbc71 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/FilterConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/I18nConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/I18nConfig.class new file mode 100644 index 0000000..b7a4672 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/I18nConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/InterceptorConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/InterceptorConfig.class new file mode 100644 index 0000000..fd0ca4d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/InterceptorConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/KaptchaTextCreator.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/KaptchaTextCreator.class new file mode 100644 index 0000000..37faa14 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/KaptchaTextCreator.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/MyBatisConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/MyBatisConfig.class new file mode 100644 index 0000000..a3c5449 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/MyBatisConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/RedisConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/RedisConfig.class new file mode 100644 index 0000000..c57bb77 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/RedisConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/ResourcesConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ResourcesConfig.class new file mode 100644 index 0000000..c019a6e Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ResourcesConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/SecurityConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/SecurityConfig.class new file mode 100644 index 0000000..07502aa Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/SecurityConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/ServerConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ServerConfig.class new file mode 100644 index 0000000..0c6dd4b Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ServerConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig$1.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig$1.class new file mode 100644 index 0000000..734d155 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig$1.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig.class new file mode 100644 index 0000000..7abc58c Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/ThreadPoolConfig.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/DruidProperties.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/DruidProperties.class new file mode 100644 index 0000000..07c86b2 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/DruidProperties.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/PermitAllUrlProperties.class b/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/PermitAllUrlProperties.class new file mode 100644 index 0000000..5b7dc6d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/config/properties/PermitAllUrlProperties.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSource.class b/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSource.class new file mode 100644 index 0000000..0caef00 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSource.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.class b/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.class new file mode 100644 index 0000000..b0200ba Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.class b/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.class new file mode 100644 index 0000000..e7c56f3 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.class b/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.class new file mode 100644 index 0000000..3cac11c Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/manager/AsyncManager.class b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/AsyncManager.class new file mode 100644 index 0000000..bf04e31 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/AsyncManager.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/manager/ShutdownManager.class b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/ShutdownManager.class new file mode 100644 index 0000000..1c35ebc Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/ShutdownManager.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$1.class b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$1.class new file mode 100644 index 0000000..90a6851 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$1.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$2.class b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$2.class new file mode 100644 index 0000000..7f83f00 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory$2.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory.class b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory.class new file mode 100644 index 0000000..da2e1cf Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/manager/factory/AsyncFactory.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/AuthenticationContextHolder.class b/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/AuthenticationContextHolder.class new file mode 100644 index 0000000..e8af966 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/AuthenticationContextHolder.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/PermissionContextHolder.class b/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/PermissionContextHolder.class new file mode 100644 index 0000000..e3f0e80 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/security/context/PermissionContextHolder.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.class b/ruoyi-framework/target/classes/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.class new file mode 100644 index 0000000..9e9f361 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.class b/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.class new file mode 100644 index 0000000..3d92460 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.class b/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.class new file mode 100644 index 0000000..afde3fb Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/Server.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/Server.class new file mode 100644 index 0000000..417ecf9 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/Server.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Cpu.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Cpu.class new file mode 100644 index 0000000..9e477ad Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Cpu.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Jvm.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Jvm.class new file mode 100644 index 0000000..fb56a52 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Jvm.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Mem.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Mem.class new file mode 100644 index 0000000..8c524b5 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Mem.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Sys.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Sys.class new file mode 100644 index 0000000..35db060 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/Sys.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/SysFile.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/SysFile.class new file mode 100644 index 0000000..c3aeb42 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/domain/server/SysFile.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/exception/GlobalExceptionHandler.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000..68aab0d Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/exception/GlobalExceptionHandler.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/PermissionService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/PermissionService.class new file mode 100644 index 0000000..c28bdc3 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/PermissionService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysLoginService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysLoginService.class new file mode 100644 index 0000000..59adf8a Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysLoginService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPasswordService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPasswordService.class new file mode 100644 index 0000000..64d67bb Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPasswordService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPermissionService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPermissionService.class new file mode 100644 index 0000000..a19f64e Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysPermissionService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysRegisterService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysRegisterService.class new file mode 100644 index 0000000..2982a40 Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/SysRegisterService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/TokenService.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/TokenService.class new file mode 100644 index 0000000..82e181e Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/TokenService.class differ diff --git a/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/UserDetailsServiceImpl.class b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/UserDetailsServiceImpl.class new file mode 100644 index 0000000..75a185e Binary files /dev/null and b/ruoyi-framework/target/classes/com/ruoyi/framework/web/service/UserDetailsServiceImpl.class differ diff --git a/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..ac8d636 --- /dev/null +++ b/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,51 @@ +com\ruoyi\framework\config\CaptchaConfig.class +com\ruoyi\framework\config\ServerConfig.class +com\ruoyi\framework\web\service\SysLoginService.class +com\ruoyi\framework\security\handle\AuthenticationEntryPointImpl.class +com\ruoyi\framework\security\context\AuthenticationContextHolder.class +com\ruoyi\framework\web\service\PermissionService.class +com\ruoyi\framework\web\domain\server\Cpu.class +com\ruoyi\framework\web\domain\server\Mem.class +com\ruoyi\framework\config\MyBatisConfig.class +com\ruoyi\framework\config\DruidConfig$1.class +com\ruoyi\framework\manager\factory\AsyncFactory$1.class +com\ruoyi\framework\manager\factory\AsyncFactory.class +com\ruoyi\framework\web\service\SysRegisterService.class +com\ruoyi\framework\config\FilterConfig.class +com\ruoyi\framework\manager\ShutdownManager.class +com\ruoyi\framework\datasource\DynamicDataSource.class +com\ruoyi\framework\config\properties\PermitAllUrlProperties.class +com\ruoyi\framework\web\service\SysPermissionService.class +com\ruoyi\framework\web\exception\GlobalExceptionHandler.class +com\ruoyi\framework\config\ThreadPoolConfig$1.class +com\ruoyi\framework\manager\factory\AsyncFactory$2.class +com\ruoyi\framework\config\SecurityConfig.class +com\ruoyi\framework\web\service\UserDetailsServiceImpl.class +com\ruoyi\framework\config\InterceptorConfig.class +com\ruoyi\framework\aspectj\RateLimiterAspect.class +com\ruoyi\framework\config\KaptchaTextCreator.class +com\ruoyi\framework\config\ResourcesConfig.class +com\ruoyi\framework\config\CorsInterceptor.class +com\ruoyi\framework\datasource\DynamicDataSourceContextHolder.class +com\ruoyi\framework\interceptor\RepeatSubmitInterceptor.class +com\ruoyi\framework\web\domain\server\Jvm.class +com\ruoyi\framework\config\properties\DruidProperties.class +com\ruoyi\framework\aspectj\DataSourceAspect.class +com\ruoyi\framework\aspectj\LogAspect.class +com\ruoyi\framework\config\I18nConfig.class +com\ruoyi\framework\config\FastJson2JsonRedisSerializer.class +com\ruoyi\framework\web\domain\server\SysFile.class +com\ruoyi\framework\manager\AsyncManager.class +com\ruoyi\framework\web\service\SysPasswordService.class +com\ruoyi\framework\interceptor\impl\SameUrlDataInterceptor.class +com\ruoyi\framework\security\context\PermissionContextHolder.class +com\ruoyi\framework\security\filter\JwtAuthenticationTokenFilter.class +com\ruoyi\framework\web\service\TokenService.class +com\ruoyi\framework\config\ThreadPoolConfig.class +com\ruoyi\framework\config\DruidConfig.class +com\ruoyi\framework\web\domain\server\Sys.class +com\ruoyi\framework\config\ApplicationConfig.class +com\ruoyi\framework\config\RedisConfig.class +com\ruoyi\framework\web\domain\Server.class +com\ruoyi\framework\security\handle\LogoutSuccessHandlerImpl.class +com\ruoyi\framework\aspectj\DataScopeAspect.class diff --git a/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..15ca690 --- /dev/null +++ b/ruoyi-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,47 @@ +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\InterceptorConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\security\context\AuthenticationContextHolder.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\SysLoginService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\SysPasswordService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\interceptor\RepeatSubmitInterceptor.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\CaptchaConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\exception\GlobalExceptionHandler.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\DataScopeAspect.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\FastJson2JsonRedisSerializer.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\server\SysFile.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\properties\PermitAllUrlProperties.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\security\filter\JwtAuthenticationTokenFilter.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\SysPermissionService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\DruidConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\UserDetailsServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\I18nConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSource.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\SysRegisterService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\RateLimiterAspect.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\DataSourceAspect.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\TokenService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\server\Cpu.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ServerConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\manager\factory\AsyncFactory.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\CorsInterceptor.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\Server.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\manager\ShutdownManager.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\server\Mem.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\KaptchaTextCreator.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ApplicationConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\server\Sys.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\MyBatisConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\security\handle\AuthenticationEntryPointImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\security\context\PermissionContextHolder.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\FilterConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSourceContextHolder.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\interceptor\impl\SameUrlDataInterceptor.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\manager\AsyncManager.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\RedisConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ResourcesConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\SecurityConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ThreadPoolConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\domain\server\Jvm.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\security\handle\LogoutSuccessHandlerImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\config\properties\DruidProperties.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\LogAspect.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-framework\src\main\java\com\ruoyi\framework\web\service\PermissionService.java diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml new file mode 100644 index 0000000..54b9809 --- /dev/null +++ b/ruoyi-generator/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + com.ruoyi + ruoyi-common + + + + + com.alibaba + druid-spring-boot-starter + + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java new file mode 100644 index 0000000..c01857c --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java @@ -0,0 +1,87 @@ +package com.ruoyi.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀 */ + public static boolean autoRemovePre; + + /** 表前缀 */ + public static String tablePrefix; + + /** 是否允许生成文件覆盖到本地(自定义路径) */ + public static boolean allowOverwrite; + + public static String getAuthor() + { + return author; + } + + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } + + public static boolean isAllowOverwrite() + { + return allowOverwrite; + } + + @Value("${allowOverwrite}") + public void setAllowOverwrite(boolean allowOverwrite) + { + GenConfig.allowOverwrite = allowOverwrite; + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java new file mode 100644 index 0000000..1ef0d35 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java @@ -0,0 +1,263 @@ +package com.ruoyi.generator.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.alibaba.druid.DbType; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.sql.SqlUtil; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.service.IGenTableColumnService; +import com.ruoyi.generator.service.IGenTableService; + +/** + * 代码生成 操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 获取代码生成信息 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList, SecurityUtils.getUsername()); + return success(); + } + + /** + * 创建表结构(保存) + */ + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "创建表", businessType = BusinessType.OTHER) + @PostMapping("/createTable") + public AjaxResult createTableSave(String sql) + { + try + { + SqlUtil.filterKeyword(sql); + List sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql); + List tableNames = new ArrayList<>(); + for (SQLStatement sqlStatement : sqlStatements) + { + if (sqlStatement instanceof MySqlCreateTableStatement) + { + MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement; + if (genTableService.createTable(createTableStatement.toString())) + { + String tableName = createTableStatement.getTableName().replaceAll("`", ""); + tableNames.add(tableName); + } + } + } + List tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()])); + String operName = SecurityUtils.getUsername(); + genTableService.importGenTable(tableList, operName); + return AjaxResult.success(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + return AjaxResult.error("创建表结构异常"); + } + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + if (!GenConfig.isAllowOverwrite()) + { + return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地"); + } + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java new file mode 100644 index 0000000..022a54d --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java @@ -0,0 +1,385 @@ +package com.ruoyi.generator.domain; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 业务表 gen_table + * + * @author ruoyi + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 前端类型(element-ui模版 element-plus模版) */ + private String tplWebType; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid + private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private Long parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getTplWebType() + { + return tplWebType; + } + + public void setTplWebType(String tplWebType) + { + this.tplWebType = tplWebType; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public Long getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(Long parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + public boolean isSub() + { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() + { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..d1733b6 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java @@ -0,0 +1,373 @@ +package com.ruoyi.generator.domain; + +import javax.validation.constraints.NotBlank; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author ruoyi + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + public boolean isPk() + { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + public boolean isList() + { + return isList(this.isList); + } + + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) + { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() + { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + return this.columnComment; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..951e166 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author ruoyi + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..937656d --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java @@ -0,0 +1,91 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 数据层 + * + * @author ruoyi + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); + + /** + * 创建表 + * + * @param sql 表结构 + * @return 结果 + */ + public int createTable(String sql); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..0679689 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,68 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; + +/** + * 业务字段 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..fe4312f --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java @@ -0,0 +1,531 @@ +package com.ruoyi.generator.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.text.CharsetKit; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; +import com.ruoyi.generator.mapper.GenTableMapper; +import com.ruoyi.generator.util.GenUtils; +import com.ruoyi.generator.util.VelocityInitializer; +import com.ruoyi.generator.util.VelocityUtils; + +/** + * 业务 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) + { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + for (GenTableColumn genTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) + { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + @Override + public boolean createTable(String sql) + { + return genTableMapper.createTable(sql) == 0; + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList, String operName) + { + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) + { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) + { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + } + else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + Long parentMenuId = paramsObj.getLongValue(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..3037f70 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author ruoyi + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java new file mode 100644 index 0000000..695426e --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java @@ -0,0 +1,130 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import java.util.Map; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 服务层 + * + * @author ruoyi + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + public boolean createTable(String sql); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + * @param operName 操作人员 + */ + public void importGenTable(List tableList, String operName); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java new file mode 100644 index 0000000..e7ebc20 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java @@ -0,0 +1,257 @@ +package com.ruoyi.generator.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 代码生成器 工具类 + * + * @author ruoyi + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..9f69403 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.ruoyi.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.ruoyi.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author ruoyi + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java new file mode 100644 index 0000000..1a14681 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java @@ -0,0 +1,408 @@ +package com.ruoyi.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * @param tplCategory 生成的模板 + * @param tplWebType 前端类型 + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory, String tplWebType) + { + String useWebType = "vm/vue"; + if ("element-plus".equals(tplWebType)) + { + useWebType = "vm/vue/v3"; + } + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add(useWebType + "/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/ruoyi-generator/src/main/resources/generator.yml b/ruoyi-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..d1cbf48 --- /dev/null +++ b/ruoyi-generator/src/main/resources/generator.yml @@ -0,0 +1,12 @@ +# 代码生成 +gen: + # 作者 + author: ruoyi + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.ruoyi.system + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ + # 是否允许生成文件覆盖到本地(自定义路径),默认不允许 + allowOverwrite: false \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..52857e8 --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..d1110f7 --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + ${sql} + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..bf88988 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,115 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.ruoyi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..b236ef0 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,105 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.domain.BaseEntity; +#elseif($table.tree) +import com.ruoyi.common.core.domain.TreeEntity; +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } + +#end +#end +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ruoyi-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..264882b --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..14746e1 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,169 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.ruoyi.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..a3f53eb --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/js/api.js.vm b/ruoyi-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/ruoyi-generator/src/main/resources/vm/sql/sql.vm b/ruoyi-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..4e35fc9 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,505 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..04ebe16 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,602 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..765a5e3 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..936b465 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..456755b --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,140 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + +#if($table.sub) + + +#end + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/config/GenConfig.class b/ruoyi-generator/target/classes/com/ruoyi/generator/config/GenConfig.class new file mode 100644 index 0000000..de5ae58 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/config/GenConfig.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/controller/GenController.class b/ruoyi-generator/target/classes/com/ruoyi/generator/controller/GenController.class new file mode 100644 index 0000000..594c914 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/controller/GenController.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTable.class b/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTable.class new file mode 100644 index 0000000..e9e7986 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTable.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTableColumn.class b/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTableColumn.class new file mode 100644 index 0000000..ba27b30 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/domain/GenTableColumn.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableColumnMapper.class b/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableColumnMapper.class new file mode 100644 index 0000000..c4b4135 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableColumnMapper.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableMapper.class b/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableMapper.class new file mode 100644 index 0000000..2b1a6df Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/mapper/GenTableMapper.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableColumnServiceImpl.class b/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableColumnServiceImpl.class new file mode 100644 index 0000000..dd6e1c4 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableColumnServiceImpl.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableServiceImpl.class b/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableServiceImpl.class new file mode 100644 index 0000000..51f6c1a Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/service/GenTableServiceImpl.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableColumnService.class b/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableColumnService.class new file mode 100644 index 0000000..a94d7db Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableColumnService.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableService.class b/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableService.class new file mode 100644 index 0000000..efdcf68 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/service/IGenTableService.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/util/GenUtils.class b/ruoyi-generator/target/classes/com/ruoyi/generator/util/GenUtils.class new file mode 100644 index 0000000..5a881bc Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/util/GenUtils.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityInitializer.class b/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityInitializer.class new file mode 100644 index 0000000..0f8c10c Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityInitializer.class differ diff --git a/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityUtils.class b/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityUtils.class new file mode 100644 index 0000000..c5ad068 Binary files /dev/null and b/ruoyi-generator/target/classes/com/ruoyi/generator/util/VelocityUtils.class differ diff --git a/ruoyi-generator/target/classes/generator.yml b/ruoyi-generator/target/classes/generator.yml new file mode 100644 index 0000000..d1cbf48 --- /dev/null +++ b/ruoyi-generator/target/classes/generator.yml @@ -0,0 +1,12 @@ +# 代码生成 +gen: + # 作者 + author: ruoyi + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.ruoyi.system + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ + # 是否允许生成文件覆盖到本地(自定义路径),默认不允许 + allowOverwrite: false \ No newline at end of file diff --git a/ruoyi-generator/target/classes/mapper/generator/GenTableColumnMapper.xml b/ruoyi-generator/target/classes/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..52857e8 --- /dev/null +++ b/ruoyi-generator/target/classes/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/target/classes/mapper/generator/GenTableMapper.xml b/ruoyi-generator/target/classes/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..d1110f7 --- /dev/null +++ b/ruoyi-generator/target/classes/mapper/generator/GenTableMapper.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + ${sql} + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/target/classes/vm/java/controller.java.vm b/ruoyi-generator/target/classes/vm/java/controller.java.vm new file mode 100644 index 0000000..bf88988 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/controller.java.vm @@ -0,0 +1,115 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.ruoyi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/ruoyi-generator/target/classes/vm/java/domain.java.vm b/ruoyi-generator/target/classes/vm/java/domain.java.vm new file mode 100644 index 0000000..b236ef0 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/domain.java.vm @@ -0,0 +1,105 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.domain.BaseEntity; +#elseif($table.tree) +import com.ruoyi.common.core.domain.TreeEntity; +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } + +#end +#end +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/target/classes/vm/java/mapper.java.vm b/ruoyi-generator/target/classes/vm/java/mapper.java.vm new file mode 100644 index 0000000..7e7d7c2 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/ruoyi-generator/target/classes/vm/java/service.java.vm b/ruoyi-generator/target/classes/vm/java/service.java.vm new file mode 100644 index 0000000..264882b --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/service.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/ruoyi-generator/target/classes/vm/java/serviceImpl.java.vm b/ruoyi-generator/target/classes/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..14746e1 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/serviceImpl.java.vm @@ -0,0 +1,169 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.ruoyi.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/ruoyi-generator/target/classes/vm/java/sub-domain.java.vm b/ruoyi-generator/target/classes/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..a3f53eb --- /dev/null +++ b/ruoyi-generator/target/classes/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/target/classes/vm/js/api.js.vm b/ruoyi-generator/target/classes/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/ruoyi-generator/target/classes/vm/sql/sql.vm b/ruoyi-generator/target/classes/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/ruoyi-generator/target/classes/vm/vue/index-tree.vue.vm b/ruoyi-generator/target/classes/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..4e35fc9 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/vue/index-tree.vue.vm @@ -0,0 +1,505 @@ + + + diff --git a/ruoyi-generator/target/classes/vm/vue/index.vue.vm b/ruoyi-generator/target/classes/vm/vue/index.vue.vm new file mode 100644 index 0000000..04ebe16 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/vue/index.vue.vm @@ -0,0 +1,602 @@ + + + diff --git a/ruoyi-generator/target/classes/vm/vue/v3/index-tree.vue.vm b/ruoyi-generator/target/classes/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..765a5e3 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/ruoyi-generator/target/classes/vm/vue/v3/index.vue.vm b/ruoyi-generator/target/classes/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..936b465 --- /dev/null +++ b/ruoyi-generator/target/classes/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/ruoyi-generator/target/classes/vm/xml/mapper.xml.vm b/ruoyi-generator/target/classes/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..456755b --- /dev/null +++ b/ruoyi-generator/target/classes/vm/xml/mapper.xml.vm @@ -0,0 +1,140 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + +#if($table.sub) + + +#end + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9db6ad6 --- /dev/null +++ b/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,13 @@ +com\ruoyi\generator\service\IGenTableColumnService.class +com\ruoyi\generator\controller\GenController.class +com\ruoyi\generator\domain\GenTable.class +com\ruoyi\generator\service\GenTableServiceImpl.class +com\ruoyi\generator\util\VelocityUtils.class +com\ruoyi\generator\mapper\GenTableMapper.class +com\ruoyi\generator\mapper\GenTableColumnMapper.class +com\ruoyi\generator\service\GenTableColumnServiceImpl.class +com\ruoyi\generator\util\GenUtils.class +com\ruoyi\generator\util\VelocityInitializer.class +com\ruoyi\generator\domain\GenTableColumn.class +com\ruoyi\generator\service\IGenTableService.class +com\ruoyi\generator\config\GenConfig.class diff --git a/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..eeaddda --- /dev/null +++ b/ruoyi-generator/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,13 @@ +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\util\VelocityInitializer.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\service\IGenTableService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\config\GenConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\controller\GenController.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\domain\GenTableColumn.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\mapper\GenTableMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\util\GenUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\service\IGenTableColumnService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\util\VelocityUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\domain\GenTable.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\service\GenTableColumnServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\service\GenTableServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-generator\src\main\java\com\ruoyi\generator\mapper\GenTableColumnMapper.java diff --git a/ruoyi-quartz/pom.xml b/ruoyi-quartz/pom.xml new file mode 100644 index 0000000..68e3e1d --- /dev/null +++ b/ruoyi-quartz/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-quartz + + + quartz定时任务 + + + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..d4e065a --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.ruoyi.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author ruoyi +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java new file mode 100644 index 0000000..f11aebf --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java @@ -0,0 +1,185 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 调度任务信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException + { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..62ecbab --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java @@ -0,0 +1,92 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 调度日志操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java new file mode 100644 index 0000000..1f49695 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java @@ -0,0 +1,171 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.quartz.util.CronUtils; + +/** + * 定时任务调度表 sys_job + * + * @author ruoyi + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式 */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** cron计划策略 */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..121c035 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java @@ -0,0 +1,155 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author ruoyi + */ +public class SysJobLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间 */ + private Date startTime; + + /** 停止时间 */ + private Date stopTime; + + public Long getJobLogId() + { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() + { + return jobMessage; + } + + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getExceptionInfo() + { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getStopTime() + { + return stopTime; + } + + public void setStopTime(Date stopTime) + { + this.stopTime = stopTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..727d916 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author ruoyi + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..20f45db --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author ruoyi + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..8546792 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java new file mode 100644 index 0000000..437ade8 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..812eed7 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,87 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.mapper.SysJobLogMapper; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 定时任务调度日志信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..77fdbb5 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,261 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.mapper.SysJobMapper; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 定时任务调度信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobServiceImpl implements ISysJobService +{ + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) + { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) + { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) + { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + return CronUtils.isValid(cronExpression); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java new file mode 100644 index 0000000..853243b --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java @@ -0,0 +1,28 @@ +package com.ruoyi.quartz.task; + +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * 定时任务调度测试 + * + * @author ruoyi + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..eec1faf --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,106 @@ +package com.ruoyi.quartz.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 抽象quartz调用 + * + * @author ruoyi + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) + { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + before(context, sysJob); + if (sysJob != null) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java new file mode 100644 index 0000000..dd53839 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.ruoyi.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author ruoyi + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..dea8cde --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java @@ -0,0 +1,182 @@ +package com.ruoyi.quartz.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 任务执行工具 + * + * @author ruoyi + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StringUtils.substringBetweenLast(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..5e13558 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,21 @@ +package com.ruoyi.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author ruoyi + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..e975326 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java @@ -0,0 +1,19 @@ +package com.ruoyi.quartz.util; + +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author ruoyi + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..5a83f50 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java @@ -0,0 +1,141 @@ +package com.ruoyi.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.exception.job.TaskException.Code; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author ruoyi + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) + { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) + { + return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR); + } +} diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..ba1b683 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..5605c44 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobController.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobController.class new file mode 100644 index 0000000..e2fcb34 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobController.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobLogController.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobLogController.class new file mode 100644 index 0000000..93a0ad8 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/controller/SysJobLogController.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJob.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJob.class new file mode 100644 index 0000000..d87819b Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJob.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJobLog.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJobLog.class new file mode 100644 index 0000000..463927f Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/domain/SysJobLog.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobLogMapper.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobLogMapper.class new file mode 100644 index 0000000..ef18199 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobLogMapper.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobMapper.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobMapper.class new file mode 100644 index 0000000..ff202e4 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/mapper/SysJobMapper.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobLogService.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobLogService.class new file mode 100644 index 0000000..e0ed661 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobLogService.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobService.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobService.class new file mode 100644 index 0000000..2521f30 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/ISysJobService.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.class new file mode 100644 index 0000000..c02b249 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobServiceImpl.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobServiceImpl.class new file mode 100644 index 0000000..a3796e7 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/service/impl/SysJobServiceImpl.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/task/RyTask.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/task/RyTask.class new file mode 100644 index 0000000..e401d2c Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/task/RyTask.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/AbstractQuartzJob.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/AbstractQuartzJob.class new file mode 100644 index 0000000..f2d46e3 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/AbstractQuartzJob.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/CronUtils.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/CronUtils.class new file mode 100644 index 0000000..49d24ca Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/CronUtils.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/JobInvokeUtil.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/JobInvokeUtil.class new file mode 100644 index 0000000..f0a367c Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/JobInvokeUtil.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.class new file mode 100644 index 0000000..779599c Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzJobExecution.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzJobExecution.class new file mode 100644 index 0000000..fe1d101 Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/QuartzJobExecution.class differ diff --git a/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/ScheduleUtils.class b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/ScheduleUtils.class new file mode 100644 index 0000000..a849b0e Binary files /dev/null and b/ruoyi-quartz/target/classes/com/ruoyi/quartz/util/ScheduleUtils.class differ diff --git a/ruoyi-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml b/ruoyi-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..ba1b683 --- /dev/null +++ b/ruoyi-quartz/target/classes/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/target/classes/mapper/quartz/SysJobMapper.xml b/ruoyi-quartz/target/classes/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..5605c44 --- /dev/null +++ b/ruoyi-quartz/target/classes/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9fb6b30 --- /dev/null +++ b/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,17 @@ +com\ruoyi\quartz\util\QuartzDisallowConcurrentExecution.class +com\ruoyi\quartz\util\CronUtils.class +com\ruoyi\quartz\service\impl\SysJobLogServiceImpl.class +com\ruoyi\quartz\controller\SysJobController.class +com\ruoyi\quartz\util\ScheduleUtils.class +com\ruoyi\quartz\service\ISysJobLogService.class +com\ruoyi\quartz\mapper\SysJobMapper.class +com\ruoyi\quartz\mapper\SysJobLogMapper.class +com\ruoyi\quartz\domain\SysJob.class +com\ruoyi\quartz\domain\SysJobLog.class +com\ruoyi\quartz\controller\SysJobLogController.class +com\ruoyi\quartz\task\RyTask.class +com\ruoyi\quartz\util\AbstractQuartzJob.class +com\ruoyi\quartz\util\JobInvokeUtil.class +com\ruoyi\quartz\service\impl\SysJobServiceImpl.class +com\ruoyi\quartz\service\ISysJobService.class +com\ruoyi\quartz\util\QuartzJobExecution.class diff --git a/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..8b57d4a --- /dev/null +++ b/ruoyi-quartz/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,18 @@ +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\QuartzJobExecution.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\JobInvokeUtil.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\config\ScheduleConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\ScheduleUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\mapper\SysJobMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\task\RyTask.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\CronUtils.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\domain\SysJobLog.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\mapper\SysJobLogMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\domain\SysJob.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\service\ISysJobLogService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\QuartzDisallowConcurrentExecution.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobLogController.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\service\impl\SysJobLogServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\service\ISysJobService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\AbstractQuartzJob.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\service\impl\SysJobServiceImpl.java diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml new file mode 100644 index 0000000..f514d3d --- /dev/null +++ b/ruoyi-system/pom.xml @@ -0,0 +1,28 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-system + + + system系统模块 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccount.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccount.java new file mode 100644 index 0000000..309d999 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccount.java @@ -0,0 +1,136 @@ +package com.ruoyi.system.domain; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +/** + * 客户端账号对象 client_account + * + * @author ruoyi + * @date 2023-09-15 + */ +public class ClientAccount extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 账号名称 */ + @Excel(name = "账号名称") + private String accountName; + + /** 用户名 */ + @Excel(name = "用户名") + private String username; + + /** 密码(加密) */ + private String password; + + + /** 账号状态(0正常 1停用) */ + @Excel(name = "账号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 过期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date expireTime; + + /** 允许的IP地址范围 */ + private String allowedIpRange; + + /** 备注信息 */ + @Excel(name = "备注信息") + private String remark; + + /** 功能权限配置(JSON格式) */ + private String permissions; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setAccountName(String accountName) + { + this.accountName = accountName; + } + + public String getAccountName() + { + return accountName; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getUsername() + { + return username; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getPassword() + { + return password; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setExpireTime(Date expireTime) + { + this.expireTime = expireTime; + } + + public Date getExpireTime() + { + return expireTime; + } + public void setAllowedIpRange(String allowedIpRange) + { + this.allowedIpRange = allowedIpRange; + } + + public String getAllowedIpRange() + { + return allowedIpRange; + } + @Override + public void setRemark(String remark) + { + this.remark = remark; + } + + @Override + public String getRemark() + { + return remark; + } + + public void setPermissions(String permissions) + { + this.permissions = permissions; + } + + public String getPermissions() + { + return permissions; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthInfo.java new file mode 100644 index 0000000..e79c7b1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthInfo.java @@ -0,0 +1,200 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 客户端认证信息对象 client_auth_info + * + * @author ruoyi + * @date 2023-09-15 + */ +public class ClientAuthInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 客户端ID */ + @Excel(name = "客户端ID") + private String clientId; + + /** 用户名 */ + @Excel(name = "用户名") + private String username; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String osName; + + /** 操作系统版本 */ + @Excel(name = "操作系统版本") + private String osVersion; + + /** Java版本 */ + @Excel(name = "Java版本") + private String javaVersion; + + /** 应用版本 */ + @Excel(name = "应用版本") + private String appVersion; + + /** IP地址 */ + @Excel(name = "IP地址") + private String ipAddress; + + /** 主机名 */ + @Excel(name = "主机名") + private String hostname; + + /** 认证状态(0成功 1失败) */ + @Excel(name = "认证状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 认证时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "认证时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date authTime; + + /** 上次活跃时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "上次活跃时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date lastActiveTime; + + /** 是否在线 */ + @Excel(name = "在线状态", readConverterExp = "0=离线,1=在线") + private String online; + + /** 会话令牌 */ + private String accessToken; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + public void setUsername(String username) + { + this.username = username; + } + + public String getUsername() + { + return username; + } + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsName() + { + return osName; + } + public void setOsVersion(String osVersion) + { + this.osVersion = osVersion; + } + + public String getOsVersion() + { + return osVersion; + } + public void setJavaVersion(String javaVersion) + { + this.javaVersion = javaVersion; + } + + public String getJavaVersion() + { + return javaVersion; + } + public void setAppVersion(String appVersion) + { + this.appVersion = appVersion; + } + + public String getAppVersion() + { + return appVersion; + } + public void setIpAddress(String ipAddress) + { + this.ipAddress = ipAddress; + } + + public String getIpAddress() + { + return ipAddress; + } + public void setHostname(String hostname) + { + this.hostname = hostname; + } + + public String getHostname() + { + return hostname; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setAuthTime(Date authTime) + { + this.authTime = authTime; + } + + public Date getAuthTime() + { + return authTime; + } + public void setLastActiveTime(Date lastActiveTime) + { + this.lastActiveTime = lastActiveTime; + } + + public Date getLastActiveTime() + { + return lastActiveTime; + } + public void setOnline(String online) + { + this.online = online; + } + + public String getOnline() + { + return online; + } + public void setAccessToken(String accessToken) + { + this.accessToken = accessToken; + } + + public String getAccessToken() + { + return accessToken; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthKey.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthKey.java new file mode 100644 index 0000000..01eb56f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAuthKey.java @@ -0,0 +1,160 @@ +package com.ruoyi.system.domain; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +/** + * 客户端认证密钥对象 client_auth_key + * + * @author ruoyi + * @date 2023-09-15 + */ +public class ClientAuthKey extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 密钥名称 */ + @Excel(name = "密钥名称") + private String keyName; + + /** 认证密钥 */ + @Excel(name = "认证密钥") + private String authKey; + + /** 适用客户端类型 */ + @Excel(name = "适用客户端类型", readConverterExp = "0=全部,1=采集客户端,2=管理客户端") + private String clientType; + + /** 密钥状态(0正常 1停用) */ + @Excel(name = "密钥状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 使用次数上限 */ + @Excel(name = "使用次数上限") + private Integer usageLimit; + + /** 已使用次数 */ + @Excel(name = "已使用次数") + private Integer usedCount; + + /** 过期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date expireTime; + + /** 允许的IP地址范围 */ + private String allowedIpRange; + + /** 备注信息 */ + @Excel(name = "备注信息") + private String remark; + + /** 功能权限配置(JSON格式) */ + private String permissions; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setKeyName(String keyName) + { + this.keyName = keyName; + } + + public String getKeyName() + { + return keyName; + } + public void setAuthKey(String authKey) + { + this.authKey = authKey; + } + + public String getAuthKey() + { + return authKey; + } + public void setClientType(String clientType) + { + this.clientType = clientType; + } + + public String getClientType() + { + return clientType; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setUsageLimit(Integer usageLimit) + { + this.usageLimit = usageLimit; + } + + public Integer getUsageLimit() + { + return usageLimit; + } + public void setUsedCount(Integer usedCount) + { + this.usedCount = usedCount; + } + + public Integer getUsedCount() + { + return usedCount; + } + public void setExpireTime(Date expireTime) + { + this.expireTime = expireTime; + } + + public Date getExpireTime() + { + return expireTime; + } + public void setAllowedIpRange(String allowedIpRange) + { + this.allowedIpRange = allowedIpRange; + } + + public String getAllowedIpRange() + { + return allowedIpRange; + } + @Override + public void setRemark(String remark) + { + this.remark = remark; + } + + @Override + public String getRemark() + { + return remark; + } + + public void setPermissions(String permissions) + { + this.permissions = permissions; + } + + public String getPermissions() + { + return permissions; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDataReport.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDataReport.java new file mode 100644 index 0000000..9b1dca5 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDataReport.java @@ -0,0 +1,112 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 客户端数据采集报告对象 client_data_report + * + * @author ruoyi + * @date 2023-09-15 + */ +public class ClientDataReport extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 客户端ID */ + @Excel(name = "客户端ID") + private String clientId; + + + /** 数据类型 */ + @Excel(name = "数据类型", readConverterExp = "ORDER=订单数据,RAKUTEN=乐天商品,AMAZON=亚马逊商品") + private String dataType; + + /** 数量 */ + @Excel(name = "数量") + private Integer dataCount; + + /** 采集时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "采集时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date collectTime; + + /** 状态(0成功 1失败) */ + @Excel(name = "状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 备注信息 */ + @Excel(name = "备注信息") + private String remark; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + public void setDataType(String dataType) + { + this.dataType = dataType; + } + + public String getDataType() + { + return dataType; + } + public void setDataCount(Integer dataCount) + { + this.dataCount = dataCount; + } + + public Integer getDataCount() + { + return dataCount; + } + public void setCollectTime(Date collectTime) + { + this.collectTime = collectTime; + } + + public Date getCollectTime() + { + return collectTime; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + @Override + public void setRemark(String remark) + { + this.remark = remark; + } + + @Override + public String getRemark() + { + return remark; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java new file mode 100644 index 0000000..fb0bd54 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientDevice.java @@ -0,0 +1,54 @@ +package com.ruoyi.system.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 客户端设备对象 client_device + */ +public class ClientDevice extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + @Excel(name = "用户名") + private String username; + @Excel(name = "设备ID") + private String deviceId; + @Excel(name = "设备名") + private String name; + @Excel(name = "操作系统") + private String os; + @Excel(name = "状态") + private String status; // online/offline + @Excel(name = "IP") + private String ip; + @Excel(name = "地区") + private String location; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "最近在线", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date lastActiveAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getDeviceId() { return deviceId; } + public void setDeviceId(String deviceId) { this.deviceId = deviceId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getOs() { return os; } + public void setOs(String os) { this.os = os; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getIp() { return ip; } + public void setIp(String ip) { this.ip = ip; } + public String getLocation() { return location; } + public void setLocation(String location) { this.location = location; } + public Date getLastActiveAt() { return lastActiveAt; } + public void setLastActiveAt(Date lastActiveAt) { this.lastActiveAt = lastActiveAt; } +} + + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientErrorReport.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientErrorReport.java new file mode 100644 index 0000000..8334bad --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientErrorReport.java @@ -0,0 +1,152 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 客户端错误报告对象 client_error_report + * + * @author ruoyi + * @date 2023-09-15 + */ +public class ClientErrorReport extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 客户端ID */ + @Excel(name = "客户端ID") + private String clientId; + + /** 用户名 */ + @Excel(name = "用户名") + private String username; + + /** 错误类型 */ + @Excel(name = "错误类型") + private String errorType; + + /** 错误信息 */ + @Excel(name = "错误信息") + private String errorMessage; + + /** 错误时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "错误时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date errorTime; + + /** 错误堆栈信息 */ + @Excel(name = "错误堆栈信息") + private String stackTrace; + + /** 操作系统名称 */ + @Excel(name = "操作系统名称") + private String osName; + + /** 操作系统版本 */ + @Excel(name = "操作系统版本") + private String osVersion; + + /** 应用版本 */ + @Excel(name = "应用版本") + private String appVersion; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + public void setUsername(String username) + { + this.username = username; + } + + public String getUsername() + { + return username; + } + public void setErrorType(String errorType) + { + this.errorType = errorType; + } + + public String getErrorType() + { + return errorType; + } + public void setErrorMessage(String errorMessage) + { + this.errorMessage = errorMessage; + } + + public String getErrorMessage() + { + return errorMessage; + } + public void setErrorTime(Date errorTime) + { + this.errorTime = errorTime; + } + + public Date getErrorTime() + { + return errorTime; + } + + public void setStackTrace(String stackTrace) + { + this.stackTrace = stackTrace; + } + + public String getStackTrace() + { + return stackTrace; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsName() + { + return osName; + } + + public void setOsVersion(String osVersion) + { + this.osVersion = osVersion; + } + + public String getOsVersion() + { + return osVersion; + } + + public void setAppVersion(String appVersion) + { + this.appVersion = appVersion; + } + + public String getAppVersion() + { + return appVersion; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientEventLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientEventLog.java new file mode 100644 index 0000000..063d577 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientEventLog.java @@ -0,0 +1,115 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 客户端事件日志对象 client_event_log + * + * @author ruoyi + * @date 2025-08-18 + */ +public class ClientEventLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 客户端ID */ + @Excel(name = "客户端ID") + private String clientId; + + /** 事件类型 */ + @Excel(name = "事件类型") + private String eventType; + + /** 事件内容 */ + @Excel(name = "事件内容") + private String eventContent; + + /** 事件时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "事件时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date eventTime; + + /** 事件级别 */ + @Excel(name = "事件级别", readConverterExp = "INFO=信息,WARN=警告,ERROR=错误") + private String eventLevel; + + /** 事件状态(0成功 1失败) */ + @Excel(name = "事件状态", readConverterExp = "0=成功,1=失败") + private String status; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + + public void setEventType(String eventType) + { + this.eventType = eventType; + } + + public String getEventType() + { + return eventType; + } + + public void setEventContent(String eventContent) + { + this.eventContent = eventContent; + } + + public String getEventContent() + { + return eventContent; + } + + public void setEventTime(Date eventTime) + { + this.eventTime = eventTime; + } + + public Date getEventTime() + { + return eventTime; + } + + public void setEventLevel(String eventLevel) + { + this.eventLevel = eventLevel; + } + + public String getEventLevel() + { + return eventLevel; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientInfo.java new file mode 100644 index 0000000..4102f62 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientInfo.java @@ -0,0 +1,284 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 客户端信息对象 client_info + * + * @author ruoyi + * @date 2025-08-18 + */ +public class ClientInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 客户端ID */ + @Excel(name = "客户端ID") + private String clientId; + + /** 用户名 */ + @Excel(name = "用户名") + private String username; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String osName; + + /** 应用版本 */ + @Excel(name = "应用版本") + private String appVersion; + + /** IP地址 */ + @Excel(name = "IP地址") + private String ipAddress; + + /** 最后活跃时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "最后活跃时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date lastActiveTime; + + /** 在线状态(0离线 1在线) */ + @Excel(name = "在线状态", readConverterExp = "0=离线,1=在线") + private String online; + + /** 主机名 */ + @Excel(name = "主机名") + private String hostname; + + /** 操作系统版本 */ + @Excel(name = "操作系统版本") + private String osVersion; + + /** Java版本 */ + @Excel(name = "Java版本") + private String javaVersion; + + /** 认证状态(0成功 1失败) */ + @Excel(name = "认证状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 认证时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "认证时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date authTime; + + /** 会话令牌 */ + private String accessToken; + + /** CPU使用率 */ + @Excel(name = "CPU使用率") + private Double cpuUsage; + + /** 内存使用率 */ + @Excel(name = "内存使用率") + private Double memoryUsage; + + /** 磁盘使用率 */ + @Excel(name = "磁盘使用率") + private Double diskUsage; + + /** 网络状态 */ + @Excel(name = "网络状态") + private String networkStatus; + + /** 心跳时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "心跳时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date heartbeatTime; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getUsername() + { + return username; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsName() + { + return osName; + } + + public void setAppVersion(String appVersion) + { + this.appVersion = appVersion; + } + + public String getAppVersion() + { + return appVersion; + } + + public void setIpAddress(String ipAddress) + { + this.ipAddress = ipAddress; + } + + public String getIpAddress() + { + return ipAddress; + } + + public void setLastActiveTime(Date lastActiveTime) + { + this.lastActiveTime = lastActiveTime; + } + + public Date getLastActiveTime() + { + return lastActiveTime; + } + + public void setOnline(String online) + { + this.online = online; + } + + public String getOnline() + { + return online; + } + + public void setHostname(String hostname) + { + this.hostname = hostname; + } + + public String getHostname() + { + return hostname; + } + + public void setOsVersion(String osVersion) + { + this.osVersion = osVersion; + } + + public String getOsVersion() + { + return osVersion; + } + + public void setJavaVersion(String javaVersion) + { + this.javaVersion = javaVersion; + } + + public String getJavaVersion() + { + return javaVersion; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + public void setAuthTime(Date authTime) + { + this.authTime = authTime; + } + + public Date getAuthTime() + { + return authTime; + } + + public void setAccessToken(String accessToken) + { + this.accessToken = accessToken; + } + + public String getAccessToken() + { + return accessToken; + } + + public void setCpuUsage(Double cpuUsage) + { + this.cpuUsage = cpuUsage; + } + + public Double getCpuUsage() + { + return cpuUsage; + } + + public void setMemoryUsage(Double memoryUsage) + { + this.memoryUsage = memoryUsage; + } + + public Double getMemoryUsage() + { + return memoryUsage; + } + + public void setDiskUsage(Double diskUsage) + { + this.diskUsage = diskUsage; + } + + public Double getDiskUsage() + { + return diskUsage; + } + + public void setNetworkStatus(String networkStatus) + { + this.networkStatus = networkStatus; + } + + public String getNetworkStatus() + { + return networkStatus; + } + + public void setHeartbeatTime(Date heartbeatTime) + { + this.heartbeatTime = heartbeatTime; + } + + public Date getHeartbeatTime() + { + return heartbeatTime; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RefreshToken.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RefreshToken.java new file mode 100644 index 0000000..e2b02a4 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RefreshToken.java @@ -0,0 +1,79 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 刷新令牌对象 refresh_token + */ +public class RefreshToken extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + + /** 账号ID */ + private Long accountId; + + /** 设备ID */ + private String deviceId; + + /** 刷新令牌 */ + private String token; + + /** 到期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date expireTime; + + /** 是否已撤销 */ + private String revoked; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Date getExpireTime() { + return expireTime; + } + + public void setExpireTime(Date expireTime) { + this.expireTime = expireTime; + } + + public String getRevoked() { + return revoked; + } + + public void setRevoked(String revoked) { + this.revoked = revoked; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java new file mode 100644 index 0000000..83f0703 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java @@ -0,0 +1,81 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 缓存信息 + * + * @author ruoyi + */ +public class SysCache +{ + /** 缓存名称 */ + private String cacheName = ""; + + /** 缓存键名 */ + private String cacheKey = ""; + + /** 缓存内容 */ + private String cacheValue = ""; + + /** 备注 */ + private String remark = ""; + + public SysCache() + { + + } + + public SysCache(String cacheName, String remark) + { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) + { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() + { + return cacheName; + } + + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + public String getCacheKey() + { + return cacheKey; + } + + public void setCacheKey(String cacheKey) + { + this.cacheKey = cacheKey; + } + + public String getCacheValue() + { + return cacheValue; + } + + public void setCacheValue(String cacheValue) + { + this.cacheValue = cacheValue; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java new file mode 100644 index 0000000..c54678c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author ruoyi + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** 参数名称 */ + @Excel(name = "参数名称") + private String configName; + + /** 参数键名 */ + @Excel(name = "参数键名") + private String configKey; + + /** 参数键值 */ + @Excel(name = "参数键值") + private String configValue; + + /** 系统内置(Y是 N否) */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java new file mode 100644 index 0000000..7fdea30 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 系统访问记录表 sys_logininfor + * + * @author ruoyi + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** 用户账号 */ + @Excel(name = "用户账号") + private String userName; + + /** 登录状态 0成功 1失败 */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 登录IP地址 */ + @Excel(name = "登录地址") + private String ipaddr; + + /** 登录地点 */ + @Excel(name = "登录地点") + private String loginLocation; + + /** 浏览器类型 */ + @Excel(name = "浏览器") + private String browser; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String os; + + /** 提示消息 */ + @Excel(name = "提示消息") + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Date loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java new file mode 100644 index 0000000..8c07a54 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java @@ -0,0 +1,102 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author ruoyi + */ +public class SysNotice extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 公告ID */ + private Long noticeId; + + /** 公告标题 */ + private String noticeTitle; + + /** 公告类型(1通知 2公告) */ + private String noticeType; + + /** 公告内容 */ + private String noticeContent; + + /** 公告状态(0正常 1关闭) */ + private String status; + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) + { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() + { + return noticeTitle; + } + + public void setNoticeType(String noticeType) + { + this.noticeType = noticeType; + } + + public String getNoticeType() + { + return noticeType; + } + + public void setNoticeContent(String noticeContent) + { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() + { + return noticeContent; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java new file mode 100644 index 0000000..f6761df --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java @@ -0,0 +1,269 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author ruoyi + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** 操作模块 */ + @Excel(name = "操作模块") + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + @Excel(name = "请求方法") + private String method; + + /** 请求方式 */ + @Excel(name = "请求方式") + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** 操作人员 */ + @Excel(name = "操作人员") + private String operName; + + /** 部门名称 */ + @Excel(name = "部门名称") + private String deptName; + + /** 请求url */ + @Excel(name = "请求地址") + private String operUrl; + + /** 操作地址 */ + @Excel(name = "操作地址") + private String operIp; + + /** 操作地点 */ + @Excel(name = "操作地点") + private String operLocation; + + /** 请求参数 */ + @Excel(name = "请求参数") + private String operParam; + + /** 返回参数 */ + @Excel(name = "返回参数") + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** 错误消息 */ + @Excel(name = "错误消息") + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + @Excel(name = "消耗时间", suffix = "毫秒") + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperLocation() + { + return operLocation; + } + + public void setOperLocation(String operLocation) + { + this.operLocation = operLocation; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java new file mode 100644 index 0000000..820a13b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 岗位表 sys_post + * + * @author ruoyi + */ +public class SysPost extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** 岗位编码 */ + @Excel(name = "岗位编码") + private String postCode; + + /** 岗位名称 */ + @Excel(name = "岗位名称") + private String postName; + + /** 岗位排序 */ + @Excel(name = "岗位排序") + private Integer postSort; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() + { + return postCode; + } + + public void setPostCode(String postCode) + { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() + { + return postName; + } + + public void setPostName(String postName) + { + this.postName = postName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getPostSort() + { + return postSort; + } + + public void setPostSort(Integer postSort) + { + this.postSort = postSort; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java new file mode 100644 index 0000000..47b21bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author ruoyi + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..de10a74 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author ruoyi + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java new file mode 100644 index 0000000..2bbd318 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java @@ -0,0 +1,113 @@ +package com.ruoyi.system.domain; + +/** + * 当前在线会话 + * + * @author ruoyi + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 部门名称 */ + private String deptName; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java new file mode 100644 index 0000000..6e8c416 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author ruoyi + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java new file mode 100644 index 0000000..4d15810 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author ruoyi + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..a5d5fdc --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java @@ -0,0 +1,106 @@ +package com.ruoyi.system.domain.vo; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author ruoyi + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..afff8c9 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java @@ -0,0 +1,148 @@ +package com.ruoyi.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountMapper.java new file mode 100644 index 0000000..7d7c644 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientAccountMapper.java @@ -0,0 +1,47 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.ClientAccount; + +/** + * 客户端账号Mapper接口 + * + * @author ruoyi + */ +public interface ClientAccountMapper +{ + /** + * 查询客户端账号 + */ + public ClientAccount selectClientAccountById(Long id); + + /** + * 通过用户名查询客户端账号 + */ + public ClientAccount selectClientAccountByUsername(String username); + + /** + * 查询客户端账号列表 + */ + public List selectClientAccountList(ClientAccount clientAccount); + + /** + * 新增客户端账号 + */ + public int insertClientAccount(ClientAccount clientAccount); + + /** + * 修改客户端账号 + */ + public int updateClientAccount(ClientAccount clientAccount); + + /** + * 删除客户端账号 + */ + public int deleteClientAccountById(Long id); + + /** + * 批量删除客户端账号 + */ + public int deleteClientAccountByIds(Long[] ids); +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java new file mode 100644 index 0000000..35d1201 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientDeviceMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.ClientDevice; +import java.util.List; + +public interface ClientDeviceMapper { + ClientDevice selectByDeviceId(String deviceId); + List selectByUsername(String username); + int insert(ClientDevice device); + int updateByDeviceId(ClientDevice device); + int deleteByDeviceId(String deviceId); + int countByUsername(String username); +} + + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientEventLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientEventLogMapper.java new file mode 100644 index 0000000..e0d11e1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientEventLogMapper.java @@ -0,0 +1,69 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.ClientEventLog; + +/** + * 客户端事件日志Mapper接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface ClientEventLogMapper +{ + /** + * 查询客户端事件日志 + * + * @param id 客户端事件日志主键 + * @return 客户端事件日志 + */ + public ClientEventLog selectClientEventLogById(Long id); + + /** + * 查询客户端事件日志列表 + * + * @param clientEventLog 客户端事件日志 + * @return 客户端事件日志集合 + */ + public List selectClientEventLogList(ClientEventLog clientEventLog); + + /** + * 新增客户端事件日志 + * + * @param clientEventLog 客户端事件日志 + * @return 结果 + */ + public int insertClientEventLog(ClientEventLog clientEventLog); + + /** + * 修改客户端事件日志 + * + * @param clientEventLog 客户端事件日志 + * @return 结果 + */ + public int updateClientEventLog(ClientEventLog clientEventLog); + + /** + * 删除客户端事件日志 + * + * @param id 客户端事件日志主键 + * @return 结果 + */ + public int deleteClientEventLogById(Long id); + + /** + * 批量删除客户端事件日志 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteClientEventLogByIds(Long[] ids); + + /** + * 根据客户端ID查询事件日志 + * + * @param clientId 客户端ID + * @return 客户端事件日志集合 + */ + public List selectClientEventLogByClientId(String clientId); +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientInfoMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientInfoMapper.java new file mode 100644 index 0000000..c010ab4 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientInfoMapper.java @@ -0,0 +1,93 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.ClientInfo; + +/** + * 客户端信息Mapper接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface ClientInfoMapper +{ + /** + * 查询客户端信息 + * + * @param id 客户端信息主键 + * @return 客户端信息 + */ + public ClientInfo selectClientInfoById(Long id); + + /** + * 查询客户端信息列表 + * + * @param clientInfo 客户端信息 + * @return 客户端信息集合 + */ + public List selectClientInfoList(ClientInfo clientInfo); + + /** + * 根据客户端ID查询客户端信息 + * + * @param clientId 客户端ID + * @return 客户端信息 + */ + public ClientInfo selectClientInfoByClientId(String clientId); + + /** + * 新增客户端信息 + * + * @param clientInfo 客户端信息 + * @return 结果 + */ + public int insertClientInfo(ClientInfo clientInfo); + + /** + * 修改客户端信息 + * + * @param clientInfo 客户端信息 + * @return 结果 + */ + public int updateClientInfo(ClientInfo clientInfo); + + /** + * 删除客户端信息 + * + * @param id 客户端信息主键 + * @return 结果 + */ + public int deleteClientInfoById(Long id); + + /** + * 批量删除客户端信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteClientInfoByIds(Long[] ids); + + /** + * 更新客户端在线状态 + * + * @param clientId 客户端ID + * @param online 在线状态 + * @return 结果 + */ + public int updateClientOnlineStatus(String clientId, String online); + + /** + * 更新客户端心跳信息 + * + * @param clientInfo 客户端信息 + * @return 结果 + */ + public int updateClientHeartbeat(ClientInfo clientInfo); + + /** + * 统计在线客户端数量 + * + * @return 在线客户端数量 + */ + public int selectOnlineClientCount(); +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientMonitorMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientMonitorMapper.java new file mode 100644 index 0000000..c28b6c9 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ClientMonitorMapper.java @@ -0,0 +1,264 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import com.ruoyi.system.domain.ClientInfo; +import com.ruoyi.system.domain.ClientErrorReport; +import com.ruoyi.system.domain.ClientDataReport; +import com.ruoyi.system.domain.ClientEventLog; + +/** + * 客户端监控数据层 + * + * @author ruoyi + */ +public interface ClientMonitorMapper { + + /** + * 插入客户端信息 + * + * @param clientInfo 客户端信息 + * @return 结果 + */ + public int insertClientInfo(ClientInfo clientInfo); + + /** + * 更新客户端信息 + * + * @param clientInfo 客户端信息 + * @return 结果 + */ + public int updateClientInfo(ClientInfo clientInfo); + + /** + * 直接执行SQL更新语句 + * + * @param sql SQL更新语句 + * @return 影响的行数 + */ + @Update("${sql}") + public int updateBySql(@Param("sql") String sql); + + /** + * 插入客户端事件日志 + * + * @param clientEventLog 事件日志 + * @return 结果 + */ + public int insertClientEventLog(ClientEventLog clientEventLog); + + /** + * 插入错误报告 + * + * @param clientErrorReport 错误报告 + * @return 结果 + */ + public int insertClientError(ClientErrorReport clientErrorReport); + + /** + * 插入数据采集报告 + * + * @param clientDataReport 数据采集报告 + * @return 结果 + */ + public int insertDataReport(ClientDataReport clientDataReport); + + /** + * 查询客户端信息 + * + * @param clientId 客户端ID + * @return 客户端信息 + */ + public ClientInfo selectClientInfoByClientId(String clientId); + + /** + * 查询客户端信息列表 + * + * @param clientInfo 客户端信息 + * @return 客户端信息列表 + */ + public List selectClientInfoList(ClientInfo clientInfo); + + /** + * 查询客户端错误报告列表 + * + * @param clientErrorReport 错误报告 + * @return 错误报告列表(包含用户名) + */ + public List> selectClientErrorList(ClientErrorReport clientErrorReport); + + /** + * 查询客户端事件日志列表 + * + * @param clientEventLog 事件日志 + * @return 事件日志列表 + */ + public List selectClientEventLogList(ClientEventLog clientEventLog); + + /** + * 查询客户端数据采集报告列表 + * + * @param clientDataReport 数据采集报告 + * @return 数据采集报告列表 + */ + public List selectClientDataReportList(ClientDataReport clientDataReport); + + /** + * 查询在线客户端数量 + * + * @return 在线客户端数量 + */ + public int selectOnlineClientCount(); + + /** + * 查询客户端总数 + * + * @return 客户端总数 + */ + public int selectTotalClientCount(); + + /** + * 查询今日错误数量 + * + * @return 今日错误数量 + */ + public int selectTodayErrorCount(); + + /** + * 查询今日数据采集数量 + * + * @return 今日数据采集数量 + */ + public int selectTodayDataCount(); + + /** + * 更新客户端在线状态 + * + * @param clientId 客户端ID + * @param online 在线状态 0离线 1在线 + * @return 结果 + */ + public int updateClientOnlineStatus(String clientId, String online); + + /** + * 直接执行SQL更新客户端状态 + * + * @param sql SQL语句 + * @return 结果 + */ + @Update("${sql}") + public int updateClientStatus(@Param("sql") String sql); + + /** + * 查询数据采集类型分布统计 + * + * @return 数据采集类型分布列表 + */ + public List> selectDataTypeDistribution(); + + /** + * 查询最近7天的客户端活跃趋势 + * + * @return 每天活跃客户端数据 + */ + public List> selectClientActiveTrend(); + + /** + * 查询客户端详细信息 + * + * @param clientId 客户端ID + * @return 客户端详细信息 + */ + public Map selectClientDetail(String clientId); + + /** + * 查询客户端版本分布 + * + * @return 版本分布数据 + */ + public List> selectVersionDistribution(); + + /** + * 查询API请求统计信息 + * + * @return API请求统计数据 + */ + public Map selectApiRequestStats(); + + /** + * 插入1688爬取风控监控数据 + * + * @param monitorData 1688风控监控数据 + * @return 结果 + */ + public int insertAlibaba1688MonitorData(Map monitorData); + + /** + * 查询1688爬取风控监控数据列表 + * + * @param params 查询参数 + * @return 1688风控监控数据列表 + */ + public List> selectAlibaba1688MonitorList(Map params); + + /** + * 查询1688爬取风控监控统计信息 + * + * @return 1688风控监控统计数据 + */ + public Map selectAlibaba1688Statistics(); + + /** + * 更新客户端心跳信息 + * + * @param clientInfo 客户端信息(包含心跳数据) + * @return 结果 + */ + public int updateClientHeartbeat(ClientInfo clientInfo); + + /** + * 将过期客户端设置为离线状态 + * + * @return 影响行数 + */ + public int updateExpiredClientsOffline(); + + /** + * 删除过期的错误报告 + * + * @return 删除行数 + */ + public int deleteExpiredErrorReports(); + + + + /** + * 删除过期的事件日志 + * + * @return 删除行数 + */ + public int deleteExpiredEventLogs(); + + /** + * 查找当天相同条件的数据报告 + * + * @param clientId 客户端ID + * @param dataType 数据类型 + * @param status 状态 + * @return 数据报告 + */ + public ClientDataReport findRecentDataReport(@Param("clientId") String clientId, @Param("dataType") String dataType, @Param("status") String status); + + /** + * 更新数据报告的数量 + * + * @param id 记录ID + * @param newCount 新的数量 + * @return 影响行数 + */ + public int updateDataReportCount(@Param("id") Long id, @Param("newCount") int newCount); +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RefreshTokenMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RefreshTokenMapper.java new file mode 100644 index 0000000..b77b890 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RefreshTokenMapper.java @@ -0,0 +1,40 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.RefreshToken; +import java.util.List; + +/** + * 刷新令牌数据层 + */ +public interface RefreshTokenMapper { + + /** + * 保存刷新令牌 + */ + int insertRefreshToken(RefreshToken refreshToken); + + /** + * 根据令牌查找 + */ + RefreshToken selectByToken(String token); + + /** + * 撤销账号的所有令牌 + */ + int revokeByAccountId(Long accountId); + + /** + * 撤销设备的所有令牌 + */ + int revokeByDeviceId(String deviceId); + + /** + * 删除过期令牌 + */ + int deleteExpiredTokens(); + + /** + * 更新令牌状态 + */ + int updateRefreshToken(RefreshToken refreshToken); +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..13d49d6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author ruoyi + */ +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..384a9b6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java @@ -0,0 +1,118 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 数据层 + * + * @author ruoyi + */ +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..a341f1e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -0,0 +1,95 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictDataMapper +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..5fb48fb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictTypeMapper +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..629866f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author ruoyi + */ +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..99c0c50 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java @@ -0,0 +1,125 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysMenu; + +/** + * 菜单表 数据层 + * + * @author ruoyi + */ +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..c34f0a2 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author ruoyi + */ +public interface SysNoticeMapper +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..2ae6457 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author ruoyi + */ +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..19be227 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 数据层 + * + * @author ruoyi + */ +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..f9d3a2f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..cf2bd8c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysRole; + +/** + * 角色表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..6602bee --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..0976eb0 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -0,0 +1,127 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户表 数据层 + * + * @author ruoyi + */ +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userId") Long userId, @Param("avatar") String avatar); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userId") Long userId, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..2a6a720 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户岗位列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..3143ec8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java new file mode 100644 index 0000000..b307776 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java @@ -0,0 +1,89 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 服务层 + * + * @author ruoyi + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java new file mode 100644 index 0000000..f228208 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 服务层 + * + * @author ruoyi + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java new file mode 100644 index 0000000..9bc4f13 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictDataService +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..01c1c1d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java @@ -0,0 +1,98 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictTypeService +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public boolean checkDictTypeUnique(SysDictType dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java new file mode 100644 index 0000000..ce3151d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author ruoyi + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java new file mode 100644 index 0000000..7d60696 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.system.domain.vo.RouterVo; + +/** + * 菜单 业务层 + * + * @author ruoyi + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java new file mode 100644 index 0000000..47ce1b7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 公告 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeService +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java new file mode 100644 index 0000000..4fd8e5a --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author ruoyi + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java new file mode 100644 index 0000000..84779bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 服务层 + * + * @author ruoyi + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java new file mode 100644 index 0000000..9185cce --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 角色业务层 + * + * @author ruoyi + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + public void checkRoleDataScope(Long... roleIds); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..8eb5448 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author ruoyi + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java new file mode 100644 index 0000000..7a5b679 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -0,0 +1,206 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户 业务层 + * + * @author ruoyi + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(Long userId, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(Long userId, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..4d29b22 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,232 @@ +package com.ruoyi.system.service.impl; + +import java.util.Collection; +import java.util.List; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() + { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) + { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisCache.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..82a22d8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,359 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.mapper.SysDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + if (depts == null || depts.isEmpty()) { + return new ArrayList<>(); + } + + // 使用Map优化查找性能,避免重复遍历 + Map deptMap = new HashMap<>(); + Map> childrenMap = new HashMap<>(); + + // 构建映射关系 + for (SysDept dept : depts) { + deptMap.put(dept.getDeptId(), dept); + childrenMap.computeIfAbsent(dept.getParentId(), k -> new ArrayList<>()).add(dept); + } + + List returnList = new ArrayList<>(); + + // 找到根节点并构建树 + for (SysDept dept : depts) { + if (!deptMap.containsKey(dept.getParentId())) { + buildTreeRecursive(dept, childrenMap); + returnList.add(dept); + } + } + + return returnList.isEmpty() ? depts : returnList; + } + + /** + * 递归构建树结构(优化版本) + */ + private void buildTreeRecursive(SysDept parent, Map> childrenMap) { + List children = childrenMap.get(parent.getDeptId()); + if (children != null && !children.isEmpty()) { + parent.setChildren(children); + for (SysDept child : children) { + buildTreeRecursive(child, childrenMap); + } + } + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + @Cacheable(value = "dept", key = "#deptId", unless = "#result == null") + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId)) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表(保留原方法以兼容性) + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表(优化版本,使用stream减少循环) + */ + private List getChildList(List list, SysDept t) + { + return list.stream() + .filter(dept -> StringUtils.isNotNull(dept.getParentId()) + && dept.getParentId().longValue() == t.getDeptId().longValue()) + .collect(Collectors.toList()); + } + + /** + * 判断是否有子节点(优化版本) + */ + private boolean hasChild(List list, SysDept t) + { + return list.stream() + .anyMatch(dept -> StringUtils.isNotNull(dept.getParentId()) + && dept.getParentId().longValue() == t.getDeptId().longValue()); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..fced569 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysDictDataService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService +{ + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) + { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) + { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) + { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) + { + for (Long dictCode : dictCodes) + { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) + { + int row = dictDataMapper.insertDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) + { + int row = dictDataMapper.updateDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..7fd9654 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,223 @@ +package com.ruoyi.system.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.mapper.SysDictTypeMapper; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictTypeServiceImpl implements ISysDictTypeService +{ + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() + { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) + { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() + { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) + { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) + { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) + { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) + { + for (Long dictId : dictIds) + { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() + { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() + { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() + { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) + { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) + { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) + { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public boolean checkDictTypeUnique(SysDictType dict) + { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..216aecb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.mapper.SysLogininforMapper; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) + { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..aa0929e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,560 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.vo.MetaVo; +import com.ruoyi.system.domain.vo.RouterVo; +import com.ruoyi.system.mapper.SysMenuMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + @Cacheable(value = "userPerms", key = "#userId", unless = "#result == null || #result.isEmpty()") + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + return perms.stream() + .filter(StringUtils::isNotEmpty) + .flatMap(perm -> Arrays.stream(perm.trim().split(","))) + .collect(Collectors.toSet()); + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + @Cacheable(value = "rolePerms", key = "#roleId", unless = "#result == null || #result.isEmpty()") + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + return perms.stream() + .filter(StringUtils::isNotEmpty) + .flatMap(perm -> Arrays.stream(perm.trim().split(","))) + .collect(Collectors.toSet()); + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + // @Cacheable(value = "userMenuTree", key = "#userId", unless = "#result == null || #result.isEmpty()") + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(getRouteName(menu.getRouteName(), menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(getRouteName(menu.getRouteName(), routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + if (menus == null || menus.isEmpty()) { + return new ArrayList<>(); + } + + // 使用Map优化查找性能 + Map menuMap = new HashMap<>(); + Map> childrenMap = new HashMap<>(); + + // 构建映射关系 + for (SysMenu menu : menus) { + menuMap.put(menu.getMenuId(), menu); + childrenMap.computeIfAbsent(menu.getParentId(), k -> new ArrayList<>()).add(menu); + } + + List returnList = new ArrayList<>(); + + // 找到根节点并构建树 + for (SysMenu menu : menus) { + if (!menuMap.containsKey(menu.getParentId())) { + buildMenuTreeRecursive(menu, childrenMap); + returnList.add(menu); + } + } + + return returnList.isEmpty() ? menus : returnList; + } + + /** + * 递归构建菜单树结构(优化版本) + */ + private void buildMenuTreeRecursive(SysMenu parent, Map> childrenMap) { + List children = childrenMap.get(parent.getMenuId()); + if (children != null && !children.isEmpty()) { + parent.setChildren(children); + for (SysMenu child : children) { + buildMenuTreeRecursive(child, childrenMap); + } + } + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + return StringUtils.EMPTY; + } + return getRouteName(menu.getRouteName(), menu.getPath()); + } + + /** + * 获取路由名称,如没有配置路由名称则取路由地址 + * + * @param name 路由名称 + * @param path 路由地址 + * @return 路由名称(驼峰格式) + */ + public String getRouteName(String name, String path) + { + String routerName = StringUtils.isNotEmpty(name) ? name : path; + return StringUtils.capitalize(routerName); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" }, + new String[] { "", "", "", "/", "/" }); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..765438b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,92 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.mapper.SysNoticeMapper; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeServiceImpl implements ISysNoticeService +{ + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) + { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) + { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) + { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) + { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) + { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) + { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..b35a0d7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,134 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.mapper.SysOperLogMapper; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + private static final Logger log = LoggerFactory.getLogger(SysOperLogServiceImpl.class); + + @Autowired + private SysOperLogMapper operLogMapper; + + private ThreadPoolExecutor logExecutor; + + @PostConstruct + public void init() { + // 创建有界线程池用于处理日志,避免无限制创建线程 + logExecutor = new ThreadPoolExecutor( + 2, // 核心线程数 + 5, // 最大线程数 + 60L, // 空闲线程存活时间 + TimeUnit.SECONDS, // 时间单位 + new LinkedBlockingQueue<>(100), // 有界队列,避免OOM + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行 + ); + } + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) + { + // 异步写入日志,避免阻塞主业务流程 + CompletableFuture.runAsync(() -> { + try { + operLogMapper.insertOperlog(operLog); + } catch (Exception e) { + log.error("异步保存操作日志失败", e); + } + }, logExecutor); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } + + /** + * 优雅关闭线程池 + */ + @PreDestroy + public void shutdown() { + if (logExecutor != null && !logExecutor.isShutdown()) { + logExecutor.shutdown(); + try { + // 等待30秒让任务完成 + if (!logExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + logExecutor.shutdownNow(); + // 再等待10秒 + if (!logExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + log.warn("线程池未能在指定时间内关闭"); + } + } + } catch (InterruptedException e) { + logExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..5e5fe06 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..e432bb1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,427 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysRoleDept; +import com.ruoyi.system.domain.SysRoleMenu; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysRoleDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + @Override + public void checkRoleDataScope(Long... roleIds) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + for (Long roleId : roleIds) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..f80a877 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.system.service.impl; + +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) + { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..9daf5b3 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,550 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanValidators; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.domain.SysUserPost; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(Long userId, String avatar) + { + return userMapper.updateUserAvatar(userId, avatar) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(Long userId, String password) + { + return userMapper.resetUserPwd(userId, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + deptService.checkDeptDataScope(user.getDeptId()); + String password = configService.selectConfigByKey("sys.user.initPassword"); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + user.setUserId(u.getUserId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientAccountMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientAccountMapper.xml new file mode 100644 index 0000000..ab05b3d --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientAccountMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + select id, account_name, username, password, status, expire_time, + allowed_ip_range, remark, permissions, create_by, create_time, update_by, update_time + from client_account + + + + + + + + + + insert into client_account + + account_name, + username, + password, + status, + expire_time, + allowed_ip_range, + remark, + permissions, + create_by, + create_time + + + #{accountName}, + #{username}, + #{password}, + #{status}, + #{expireTime}, + #{allowedIpRange}, + #{remark}, + #{permissions}, + #{createBy}, + sysdate() + + + + + update client_account + + account_name = #{accountName}, + username = #{username}, + password = #{password}, + status = #{status}, + expire_time = #{expireTime}, + allowed_ip_range = #{allowedIpRange}, + remark = #{remark}, + permissions = #{permissions}, + update_by = #{updateBy}, + update_time = sysdate() + + where id = #{id} + + + + delete from client_account where id = #{id} + + + + delete from client_account where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml new file mode 100644 index 0000000..e10f740 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientDeviceMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, create_time, update_time) + values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, now(), now()) + + + + update client_device + set username = #{username}, + name = #{name}, + os = #{os}, + status = #{status}, + ip = #{ip}, + location = #{location}, + last_active_at = #{lastActiveAt}, + update_time = now() + where device_id = #{deviceId} + + + + delete from client_device where device_id = #{deviceId} + + + + + + diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientEventLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientEventLogMapper.xml new file mode 100644 index 0000000..0a95455 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientEventLogMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + select id, client_id, event_type, event_content, event_time, event_level, status, create_time, update_time, remark + from client_event_log + + + + + + + + + + insert into client_event_log + + client_id, + event_type, + event_content, + event_time, + event_level, + status, + create_time, + update_time, + remark, + + + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status}, + #{createTime}, + #{updateTime}, + #{remark}, + + + + + update client_event_log + + client_id = #{clientId}, + event_type = #{eventType}, + event_content = #{eventContent}, + event_time = #{eventTime}, + event_level = #{eventLevel}, + status = #{status}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + delete from client_event_log where id = #{id} + + + + delete from client_event_log where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientInfoMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientInfoMapper.xml new file mode 100644 index 0000000..73696d7 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientInfoMapper.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, client_id, username, os_name, app_version, ip_address, last_active_time, online, + hostname, os_version, java_version, status, auth_time, access_token, cpu_usage, + memory_usage, disk_usage, network_status, heartbeat_time, create_time, update_time, remark + from client_info + + + + + + + + + + + + insert into client_info + + client_id, + username, + os_name, + app_version, + ip_address, + last_active_time, + online, + hostname, + os_version, + java_version, + status, + auth_time, + access_token, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + create_time, + update_time, + remark, + + + #{clientId}, + #{username}, + #{osName}, + #{appVersion}, + #{ipAddress}, + #{lastActiveTime}, + #{online}, + #{hostname}, + #{osVersion}, + #{javaVersion}, + #{status}, + #{authTime}, + #{accessToken}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + #{createTime}, + #{updateTime}, + #{remark}, + + + + + update client_info + + client_id = #{clientId}, + username = #{username}, + os_name = #{osName}, + app_version = #{appVersion}, + ip_address = #{ipAddress}, + last_active_time = #{lastActiveTime}, + online = #{online}, + hostname = #{hostname}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + status = #{status}, + auth_time = #{authTime}, + access_token = #{accessToken}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + update client_info set online = #{online}, last_active_time = now() where client_id = #{clientId} + + + + update client_info + + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + + where client_id = #{clientId} + + + + delete from client_info where id = #{id} + + + + delete from client_info where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml new file mode 100644 index 0000000..75cddcc --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml @@ -0,0 +1,554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into client_info + + client_id, + username, + access_token, + os_name, + os_version, + java_version, + app_version, + hostname, + ip_address, + status, + auth_time, + last_active_time, + online, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + + + #{clientId}, + #{username}, + #{accessToken}, + #{osName}, + #{osVersion}, + #{javaVersion}, + #{appVersion}, + #{hostname}, + #{ipAddress}, + #{status}, + #{authTime}, + #{lastActiveTime}, + #{online}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + + + + + + update client_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = NOW() + + where client_id = #{clientId} + + + + + insert into client_event_log( + client_id, + event_type, + event_content, + event_time, + event_level, + status + )values( + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status} + ) + + + + + + + insert into client_data_report + + client_id, + data_type, + data_count, + collect_time, + status, + + + #{clientId}, + #{dataType}, + #{dataCount}, + #{collectTime}, + #{status}, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO client_error_report + + client_id, + username, + error_type, + error_message, + stack_trace, + error_time, + os_name, + os_version, + app_version, + + + #{clientId}, + #{username}, + #{errorType}, + #{errorMessage}, + #{stackTrace}, + #{errorTime}, + #{osName}, + #{osVersion}, + #{appVersion}, + + + + + + update client_info set online = #{param2} where client_id = #{param1} + + + + + update client_info + + last_active_time = #{lastActiveTime}, + heartbeat_time = #{heartbeatTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus} + + where client_id = #{clientId} + + + + + update client_auth_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online} + + where client_id = #{clientId} + + + + + select id, client_id, username, access_token, os_name, os_version, java_version, + app_version, hostname, ip_address, status, auth_time, last_active_time, online + from client_auth_info + + + + + select id, client_id, username, error_type, error_message, stack_trace, + error_time, os_name, os_version, app_version + from client_error_report + + + + + select id, client_id, username, heartbeat_time, cpu_usage, memory_usage, + disk_usage, network_status + from client_heartbeat + + + + + select id, client_id, username, data_type, data_count, collect_time, status + from client_data_report + + + + + + + + + + + + + + + + + INSERT INTO client_alibaba1688_monitor + ( + client_id, + ip_address, + event_type, + event_time, + duration, + create_time + ) + VALUES + ( + #{clientId}, + #{ipAddress}, + #{eventType}, + #{eventTime}, + #{duration}, + #{createTime} + ) + + + + + + + + + + + UPDATE client_info + SET online = '0' + WHERE online = '1' + AND (heartbeat_time IS NULL OR heartbeat_time < DATE_SUB(NOW(), INTERVAL 7 DAY)) + + + + + DELETE FROM client_error_report + WHERE error_time < DATE_SUB(NOW(), INTERVAL 30 DAY) + + + + + + DELETE FROM client_event_log + WHERE event_time < DATE_SUB(NOW(), INTERVAL 7 DAY) + + + + + + + + UPDATE client_data_report + SET data_count = #{newCount}, + collect_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml.bak b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml.bak new file mode 100644 index 0000000..b0acaa1 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ClientMonitorMapper.xml.bak @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into client_info + + client_id, + username, + access_token, + os_name, + os_version, + java_version, + app_version, + hostname, + ip_address, + status, + auth_time, + last_active_time, + online, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + + + #{clientId}, + #{username}, + #{accessToken}, + #{osName}, + #{osVersion}, + #{javaVersion}, + #{appVersion}, + #{hostname}, + #{ipAddress}, + #{status}, + #{authTime}, + #{lastActiveTime}, + #{online}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + + + + + + update client_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = NOW() + + where client_id = #{clientId} + + + + + insert into client_event_log( + client_id, + event_type, + event_content, + event_time, + event_level, + status + )values( + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status} + ) + + + + + + + insert into client_data_report + + client_id, + data_type, + data_count, + collect_time, + status, + + + #{clientId}, + #{dataType}, + #{dataCount}, + #{collectTime}, + #{status}, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO client_error_report + + client_id, + error_type, + error_message, + error_time + + + #{clientId}, + #{errorType}, + #{errorMessage}, + #{errorTime} + + + + + + update client_info set online = #{param2} where client_id = #{param1} + + + + + update client_info + + last_active_time = #{lastActiveTime}, + heartbeat_time = #{heartbeatTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus} + + where client_id = #{clientId} + + + + + update client_auth_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online} + + where client_id = #{clientId} + + + + + select id, client_id, username, access_token, os_name, os_version, java_version, + app_version, hostname, ip_address, status, auth_time, last_active_time, online + from client_auth_info + + + + + select id, client_id, username, error_type, error_message, stack_trace, + error_time, os_name, os_version, app_version + from client_error_report + + + + + select id, client_id, username, heartbeat_time, cpu_usage, memory_usage, + disk_usage, network_status + from client_heartbeat + + + + + select id, client_id, username, data_type, data_count, collect_time, status + from client_data_report + + + + + + + + + + + + + + + + + INSERT INTO client_alibaba1688_monitor + ( + client_id, + ip_address, + event_type, + event_time, + duration, + create_time + ) + VALUES + ( + #{clientId}, + #{ipAddress}, + #{eventType}, + #{eventTime}, + #{duration}, + #{createTime}, + ) + + + + + + + + + + + UPDATE client_info + SET online = '0' + WHERE online = '1' + AND (heartbeat_time IS NULL OR heartbeat_time < DATE_SUB(NOW(), INTERVAL 7 DAY)) + + + + + DELETE FROM client_error_report + WHERE error_time < DATE_SUB(NOW(), INTERVAL 30 DAY) + + + + + DELETE FROM client_data_report + WHERE collect_time < DATE_SUB(NOW(), INTERVAL 90 DAY) + AND data_type NOT IN ('API_CALL') + + + + + DELETE FROM client_event_log + WHERE event_time < DATE_SUB(NOW(), INTERVAL 7 DAY) + + + + + + + + UPDATE client_data_report + SET data_count = #{newCount}, + collect_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/RefreshTokenMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RefreshTokenMapper.xml new file mode 100644 index 0000000..cfb951a --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/RefreshTokenMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + insert into refresh_token + + account_id, + device_id, + token, + expire_time, + revoked, + create_time + + + #{accountId}, + #{deviceId}, + #{token}, + #{expireTime}, + #{revoked}, + sysdate() + + + + + + + update refresh_token set revoked = '1', update_time = sysdate() + where account_id = #{accountId} and revoked = '0' + + + + update refresh_token set revoked = '1', update_time = sysdate() + where device_id = #{deviceId} and revoked = '0' + + + + delete from refresh_token + where expire_time < sysdate() or revoked = '1' + + + + update refresh_token + + revoked = #{revoked}, + update_time = sysdate() + + where id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..a5ff114 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..cf439f6 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..3b94b7f --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..438d484 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..822d665 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..84e87c9 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..65d3079 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..201db07 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..227c459 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..7c4139b --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..955d4ee --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..cb60a85 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..29c7ad5 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password} where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..2b90bc4 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..dd72689 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAccount.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAccount.class new file mode 100644 index 0000000..022173e Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAccount.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthInfo.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthInfo.class new file mode 100644 index 0000000..f191f3f Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthInfo.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthKey.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthKey.class new file mode 100644 index 0000000..19a2f43 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientAuthKey.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDataReport.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDataReport.class new file mode 100644 index 0000000..7bf2d93 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDataReport.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDevice.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDevice.class new file mode 100644 index 0000000..71e8bea Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientDevice.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientErrorReport.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientErrorReport.class new file mode 100644 index 0000000..1797322 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientErrorReport.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientEventLog.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientEventLog.class new file mode 100644 index 0000000..c420b70 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientEventLog.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientInfo.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientInfo.class new file mode 100644 index 0000000..038219f Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/ClientInfo.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/RefreshToken.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/RefreshToken.class new file mode 100644 index 0000000..9364408 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/RefreshToken.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysCache.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysCache.class new file mode 100644 index 0000000..53b8072 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysCache.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysConfig.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysConfig.class new file mode 100644 index 0000000..1d76bfd Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysConfig.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysLogininfor.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysLogininfor.class new file mode 100644 index 0000000..7e5236e Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysLogininfor.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysNotice.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysNotice.class new file mode 100644 index 0000000..bbc0dd5 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysNotice.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysOperLog.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysOperLog.class new file mode 100644 index 0000000..bcd30fe Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysOperLog.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysPost.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysPost.class new file mode 100644 index 0000000..29dc09d Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysPost.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleDept.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleDept.class new file mode 100644 index 0000000..b2dc305 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleDept.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleMenu.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleMenu.class new file mode 100644 index 0000000..929f772 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysRoleMenu.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserOnline.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserOnline.class new file mode 100644 index 0000000..9df86d4 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserOnline.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserPost.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserPost.class new file mode 100644 index 0000000..a5bf5c7 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserPost.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserRole.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserRole.class new file mode 100644 index 0000000..4c0152c Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/SysUserRole.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/MetaVo.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/MetaVo.class new file mode 100644 index 0000000..f5f993c Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/MetaVo.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/RouterVo.class b/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/RouterVo.class new file mode 100644 index 0000000..0c75adf Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/domain/vo/RouterVo.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientAccountMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientAccountMapper.class new file mode 100644 index 0000000..ae7b952 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientAccountMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientDeviceMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientDeviceMapper.class new file mode 100644 index 0000000..cda0c05 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientDeviceMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientEventLogMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientEventLogMapper.class new file mode 100644 index 0000000..44c5523 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientEventLogMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientInfoMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientInfoMapper.class new file mode 100644 index 0000000..35c72be Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientInfoMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientMonitorMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientMonitorMapper.class new file mode 100644 index 0000000..a0a6f06 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/ClientMonitorMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/RefreshTokenMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/RefreshTokenMapper.class new file mode 100644 index 0000000..a318176 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/RefreshTokenMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysConfigMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysConfigMapper.class new file mode 100644 index 0000000..7d241a8 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysConfigMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDeptMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDeptMapper.class new file mode 100644 index 0000000..2337343 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDeptMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictDataMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictDataMapper.class new file mode 100644 index 0000000..a6c74f2 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictDataMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictTypeMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictTypeMapper.class new file mode 100644 index 0000000..f3ae6dc Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysDictTypeMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysLogininforMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysLogininforMapper.class new file mode 100644 index 0000000..dc8a193 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysLogininforMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysMenuMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysMenuMapper.class new file mode 100644 index 0000000..b8c12b3 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysMenuMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysNoticeMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysNoticeMapper.class new file mode 100644 index 0000000..9a18a1e Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysNoticeMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysOperLogMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysOperLogMapper.class new file mode 100644 index 0000000..aa261d1 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysOperLogMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysPostMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysPostMapper.class new file mode 100644 index 0000000..b35a1d7 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysPostMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleDeptMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleDeptMapper.class new file mode 100644 index 0000000..631d4cb Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleDeptMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMapper.class new file mode 100644 index 0000000..c61c600 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMenuMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMenuMapper.class new file mode 100644 index 0000000..4506b7a Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysRoleMenuMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserMapper.class new file mode 100644 index 0000000..49e0dcf Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserPostMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserPostMapper.class new file mode 100644 index 0000000..7de9959 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserPostMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserRoleMapper.class b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserRoleMapper.class new file mode 100644 index 0000000..3e023bb Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/mapper/SysUserRoleMapper.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysConfigService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysConfigService.class new file mode 100644 index 0000000..6283134 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysConfigService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDeptService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDeptService.class new file mode 100644 index 0000000..0f1933e Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDeptService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictDataService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictDataService.class new file mode 100644 index 0000000..87359af Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictDataService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictTypeService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictTypeService.class new file mode 100644 index 0000000..0de418f Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysDictTypeService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysLogininforService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysLogininforService.class new file mode 100644 index 0000000..a4a43ba Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysLogininforService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysMenuService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysMenuService.class new file mode 100644 index 0000000..c28fb13 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysMenuService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysNoticeService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysNoticeService.class new file mode 100644 index 0000000..e768daf Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysNoticeService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysOperLogService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysOperLogService.class new file mode 100644 index 0000000..7e9101e Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysOperLogService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysPostService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysPostService.class new file mode 100644 index 0000000..9b3d526 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysPostService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysRoleService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysRoleService.class new file mode 100644 index 0000000..be262ec Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysRoleService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserOnlineService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserOnlineService.class new file mode 100644 index 0000000..223e79b Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserOnlineService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserService.class b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserService.class new file mode 100644 index 0000000..1ccf78f Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/ISysUserService.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysConfigServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysConfigServiceImpl.class new file mode 100644 index 0000000..2bd5150 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysConfigServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDeptServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDeptServiceImpl.class new file mode 100644 index 0000000..af8254d Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDeptServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictDataServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictDataServiceImpl.class new file mode 100644 index 0000000..34e08b7 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictDataServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.class new file mode 100644 index 0000000..2cc7ebb Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysLogininforServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysLogininforServiceImpl.class new file mode 100644 index 0000000..6ca79eb Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysLogininforServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysMenuServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysMenuServiceImpl.class new file mode 100644 index 0000000..a94f64c Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysMenuServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysNoticeServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysNoticeServiceImpl.class new file mode 100644 index 0000000..2bb1201 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysNoticeServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysOperLogServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysOperLogServiceImpl.class new file mode 100644 index 0000000..dbb2ab9 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysOperLogServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysPostServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysPostServiceImpl.class new file mode 100644 index 0000000..79218f4 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysPostServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysRoleServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysRoleServiceImpl.class new file mode 100644 index 0000000..380d85a Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysRoleServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.class new file mode 100644 index 0000000..40ffab9 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.class differ diff --git a/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserServiceImpl.class b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserServiceImpl.class new file mode 100644 index 0000000..6201109 Binary files /dev/null and b/ruoyi-system/target/classes/com/ruoyi/system/service/impl/SysUserServiceImpl.class differ diff --git a/ruoyi-system/target/classes/mapper/system/ClientAccountMapper.xml b/ruoyi-system/target/classes/mapper/system/ClientAccountMapper.xml new file mode 100644 index 0000000..ab05b3d --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientAccountMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + select id, account_name, username, password, status, expire_time, + allowed_ip_range, remark, permissions, create_by, create_time, update_by, update_time + from client_account + + + + + + + + + + insert into client_account + + account_name, + username, + password, + status, + expire_time, + allowed_ip_range, + remark, + permissions, + create_by, + create_time + + + #{accountName}, + #{username}, + #{password}, + #{status}, + #{expireTime}, + #{allowedIpRange}, + #{remark}, + #{permissions}, + #{createBy}, + sysdate() + + + + + update client_account + + account_name = #{accountName}, + username = #{username}, + password = #{password}, + status = #{status}, + expire_time = #{expireTime}, + allowed_ip_range = #{allowedIpRange}, + remark = #{remark}, + permissions = #{permissions}, + update_by = #{updateBy}, + update_time = sysdate() + + where id = #{id} + + + + delete from client_account where id = #{id} + + + + delete from client_account where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/ClientDeviceMapper.xml b/ruoyi-system/target/classes/mapper/system/ClientDeviceMapper.xml new file mode 100644 index 0000000..e10f740 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientDeviceMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, create_time, update_time) + values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, now(), now()) + + + + update client_device + set username = #{username}, + name = #{name}, + os = #{os}, + status = #{status}, + ip = #{ip}, + location = #{location}, + last_active_at = #{lastActiveAt}, + update_time = now() + where device_id = #{deviceId} + + + + delete from client_device where device_id = #{deviceId} + + + + + + diff --git a/ruoyi-system/target/classes/mapper/system/ClientEventLogMapper.xml b/ruoyi-system/target/classes/mapper/system/ClientEventLogMapper.xml new file mode 100644 index 0000000..0a95455 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientEventLogMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + select id, client_id, event_type, event_content, event_time, event_level, status, create_time, update_time, remark + from client_event_log + + + + + + + + + + insert into client_event_log + + client_id, + event_type, + event_content, + event_time, + event_level, + status, + create_time, + update_time, + remark, + + + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status}, + #{createTime}, + #{updateTime}, + #{remark}, + + + + + update client_event_log + + client_id = #{clientId}, + event_type = #{eventType}, + event_content = #{eventContent}, + event_time = #{eventTime}, + event_level = #{eventLevel}, + status = #{status}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + delete from client_event_log where id = #{id} + + + + delete from client_event_log where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/ClientInfoMapper.xml b/ruoyi-system/target/classes/mapper/system/ClientInfoMapper.xml new file mode 100644 index 0000000..73696d7 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientInfoMapper.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, client_id, username, os_name, app_version, ip_address, last_active_time, online, + hostname, os_version, java_version, status, auth_time, access_token, cpu_usage, + memory_usage, disk_usage, network_status, heartbeat_time, create_time, update_time, remark + from client_info + + + + + + + + + + + + insert into client_info + + client_id, + username, + os_name, + app_version, + ip_address, + last_active_time, + online, + hostname, + os_version, + java_version, + status, + auth_time, + access_token, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + create_time, + update_time, + remark, + + + #{clientId}, + #{username}, + #{osName}, + #{appVersion}, + #{ipAddress}, + #{lastActiveTime}, + #{online}, + #{hostname}, + #{osVersion}, + #{javaVersion}, + #{status}, + #{authTime}, + #{accessToken}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + #{createTime}, + #{updateTime}, + #{remark}, + + + + + update client_info + + client_id = #{clientId}, + username = #{username}, + os_name = #{osName}, + app_version = #{appVersion}, + ip_address = #{ipAddress}, + last_active_time = #{lastActiveTime}, + online = #{online}, + hostname = #{hostname}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + status = #{status}, + auth_time = #{authTime}, + access_token = #{accessToken}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + update client_info set online = #{online}, last_active_time = now() where client_id = #{clientId} + + + + update client_info + + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + + where client_id = #{clientId} + + + + delete from client_info where id = #{id} + + + + delete from client_info where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml b/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml new file mode 100644 index 0000000..75cddcc --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml @@ -0,0 +1,554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into client_info + + client_id, + username, + access_token, + os_name, + os_version, + java_version, + app_version, + hostname, + ip_address, + status, + auth_time, + last_active_time, + online, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + + + #{clientId}, + #{username}, + #{accessToken}, + #{osName}, + #{osVersion}, + #{javaVersion}, + #{appVersion}, + #{hostname}, + #{ipAddress}, + #{status}, + #{authTime}, + #{lastActiveTime}, + #{online}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + + + + + + update client_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = NOW() + + where client_id = #{clientId} + + + + + insert into client_event_log( + client_id, + event_type, + event_content, + event_time, + event_level, + status + )values( + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status} + ) + + + + + + + insert into client_data_report + + client_id, + data_type, + data_count, + collect_time, + status, + + + #{clientId}, + #{dataType}, + #{dataCount}, + #{collectTime}, + #{status}, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO client_error_report + + client_id, + username, + error_type, + error_message, + stack_trace, + error_time, + os_name, + os_version, + app_version, + + + #{clientId}, + #{username}, + #{errorType}, + #{errorMessage}, + #{stackTrace}, + #{errorTime}, + #{osName}, + #{osVersion}, + #{appVersion}, + + + + + + update client_info set online = #{param2} where client_id = #{param1} + + + + + update client_info + + last_active_time = #{lastActiveTime}, + heartbeat_time = #{heartbeatTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus} + + where client_id = #{clientId} + + + + + update client_auth_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online} + + where client_id = #{clientId} + + + + + select id, client_id, username, access_token, os_name, os_version, java_version, + app_version, hostname, ip_address, status, auth_time, last_active_time, online + from client_auth_info + + + + + select id, client_id, username, error_type, error_message, stack_trace, + error_time, os_name, os_version, app_version + from client_error_report + + + + + select id, client_id, username, heartbeat_time, cpu_usage, memory_usage, + disk_usage, network_status + from client_heartbeat + + + + + select id, client_id, username, data_type, data_count, collect_time, status + from client_data_report + + + + + + + + + + + + + + + + + INSERT INTO client_alibaba1688_monitor + ( + client_id, + ip_address, + event_type, + event_time, + duration, + create_time + ) + VALUES + ( + #{clientId}, + #{ipAddress}, + #{eventType}, + #{eventTime}, + #{duration}, + #{createTime} + ) + + + + + + + + + + + UPDATE client_info + SET online = '0' + WHERE online = '1' + AND (heartbeat_time IS NULL OR heartbeat_time < DATE_SUB(NOW(), INTERVAL 7 DAY)) + + + + + DELETE FROM client_error_report + WHERE error_time < DATE_SUB(NOW(), INTERVAL 30 DAY) + + + + + + DELETE FROM client_event_log + WHERE event_time < DATE_SUB(NOW(), INTERVAL 7 DAY) + + + + + + + + UPDATE client_data_report + SET data_count = #{newCount}, + collect_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml.bak b/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml.bak new file mode 100644 index 0000000..b0acaa1 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/ClientMonitorMapper.xml.bak @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into client_info + + client_id, + username, + access_token, + os_name, + os_version, + java_version, + app_version, + hostname, + ip_address, + status, + auth_time, + last_active_time, + online, + cpu_usage, + memory_usage, + disk_usage, + network_status, + heartbeat_time, + + + #{clientId}, + #{username}, + #{accessToken}, + #{osName}, + #{osVersion}, + #{javaVersion}, + #{appVersion}, + #{hostname}, + #{ipAddress}, + #{status}, + #{authTime}, + #{lastActiveTime}, + #{online}, + #{cpuUsage}, + #{memoryUsage}, + #{diskUsage}, + #{networkStatus}, + #{heartbeatTime}, + + + + + + update client_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus}, + heartbeat_time = #{heartbeatTime}, + update_time = NOW() + + where client_id = #{clientId} + + + + + insert into client_event_log( + client_id, + event_type, + event_content, + event_time, + event_level, + status + )values( + #{clientId}, + #{eventType}, + #{eventContent}, + #{eventTime}, + #{eventLevel}, + #{status} + ) + + + + + + + insert into client_data_report + + client_id, + data_type, + data_count, + collect_time, + status, + + + #{clientId}, + #{dataType}, + #{dataCount}, + #{collectTime}, + #{status}, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO client_error_report + + client_id, + error_type, + error_message, + error_time + + + #{clientId}, + #{errorType}, + #{errorMessage}, + #{errorTime} + + + + + + update client_info set online = #{param2} where client_id = #{param1} + + + + + update client_info + + last_active_time = #{lastActiveTime}, + heartbeat_time = #{heartbeatTime}, + online = #{online}, + cpu_usage = #{cpuUsage}, + memory_usage = #{memoryUsage}, + disk_usage = #{diskUsage}, + network_status = #{networkStatus} + + where client_id = #{clientId} + + + + + update client_auth_info + + username = #{username}, + access_token = #{accessToken}, + os_name = #{osName}, + os_version = #{osVersion}, + java_version = #{javaVersion}, + app_version = #{appVersion}, + hostname = #{hostname}, + ip_address = #{ipAddress}, + status = #{status}, + last_active_time = #{lastActiveTime}, + online = #{online} + + where client_id = #{clientId} + + + + + select id, client_id, username, access_token, os_name, os_version, java_version, + app_version, hostname, ip_address, status, auth_time, last_active_time, online + from client_auth_info + + + + + select id, client_id, username, error_type, error_message, stack_trace, + error_time, os_name, os_version, app_version + from client_error_report + + + + + select id, client_id, username, heartbeat_time, cpu_usage, memory_usage, + disk_usage, network_status + from client_heartbeat + + + + + select id, client_id, username, data_type, data_count, collect_time, status + from client_data_report + + + + + + + + + + + + + + + + + INSERT INTO client_alibaba1688_monitor + ( + client_id, + ip_address, + event_type, + event_time, + duration, + create_time + ) + VALUES + ( + #{clientId}, + #{ipAddress}, + #{eventType}, + #{eventTime}, + #{duration}, + #{createTime}, + ) + + + + + + + + + + + UPDATE client_info + SET online = '0' + WHERE online = '1' + AND (heartbeat_time IS NULL OR heartbeat_time < DATE_SUB(NOW(), INTERVAL 7 DAY)) + + + + + DELETE FROM client_error_report + WHERE error_time < DATE_SUB(NOW(), INTERVAL 30 DAY) + + + + + DELETE FROM client_data_report + WHERE collect_time < DATE_SUB(NOW(), INTERVAL 90 DAY) + AND data_type NOT IN ('API_CALL') + + + + + DELETE FROM client_event_log + WHERE event_time < DATE_SUB(NOW(), INTERVAL 7 DAY) + + + + + + + + UPDATE client_data_report + SET data_count = #{newCount}, + collect_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/RefreshTokenMapper.xml b/ruoyi-system/target/classes/mapper/system/RefreshTokenMapper.xml new file mode 100644 index 0000000..cfb951a --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/RefreshTokenMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + insert into refresh_token + + account_id, + device_id, + token, + expire_time, + revoked, + create_time + + + #{accountId}, + #{deviceId}, + #{token}, + #{expireTime}, + #{revoked}, + sysdate() + + + + + + + update refresh_token set revoked = '1', update_time = sysdate() + where account_id = #{accountId} and revoked = '0' + + + + update refresh_token set revoked = '1', update_time = sysdate() + where device_id = #{deviceId} and revoked = '0' + + + + delete from refresh_token + where expire_time < sysdate() or revoked = '1' + + + + update refresh_token + + revoked = #{revoked}, + update_time = sysdate() + + where id = #{id} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysConfigMapper.xml b/ruoyi-system/target/classes/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..a5ff114 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysDeptMapper.xml b/ruoyi-system/target/classes/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..cf439f6 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysDeptMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysDictDataMapper.xml b/ruoyi-system/target/classes/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..3b94b7f --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/target/classes/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..438d484 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysLogininforMapper.xml b/ruoyi-system/target/classes/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..822d665 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysMenuMapper.xml b/ruoyi-system/target/classes/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..84e87c9 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysMenuMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysNoticeMapper.xml b/ruoyi-system/target/classes/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..65d3079 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysOperLogMapper.xml b/ruoyi-system/target/classes/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..201db07 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysPostMapper.xml b/ruoyi-system/target/classes/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..227c459 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysRoleDeptMapper.xml b/ruoyi-system/target/classes/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..7c4139b --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysRoleMapper.xml b/ruoyi-system/target/classes/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..955d4ee --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysRoleMenuMapper.xml b/ruoyi-system/target/classes/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..cb60a85 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysUserMapper.xml b/ruoyi-system/target/classes/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..29c7ad5 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysUserMapper.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password} where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysUserPostMapper.xml b/ruoyi-system/target/classes/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..2b90bc4 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/target/classes/mapper/system/SysUserRoleMapper.xml b/ruoyi-system/target/classes/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..dd72689 --- /dev/null +++ b/ruoyi-system/target/classes/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..4ea82f6 --- /dev/null +++ b/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,67 @@ +com\ruoyi\system\domain\ClientAccount.class +com\ruoyi\system\mapper\ClientInfoMapper.class +com\ruoyi\system\mapper\SysOperLogMapper.class +com\ruoyi\system\domain\SysUserRole.class +com\ruoyi\system\mapper\SysRoleMapper.class +com\ruoyi\system\service\ISysDeptService.class +com\ruoyi\system\service\ISysDictTypeService.class +com\ruoyi\system\domain\SysOperLog.class +com\ruoyi\system\domain\SysRoleDept.class +com\ruoyi\system\domain\ClientAuthInfo.class +com\ruoyi\system\mapper\SysConfigMapper.class +com\ruoyi\system\domain\SysUserPost.class +com\ruoyi\system\service\impl\SysDictDataServiceImpl.class +com\ruoyi\system\domain\SysRoleMenu.class +com\ruoyi\system\domain\ClientEventLog.class +com\ruoyi\system\domain\ClientInfo.class +com\ruoyi\system\service\ISysConfigService.class +com\ruoyi\system\service\ISysUserService.class +com\ruoyi\system\domain\vo\MetaVo.class +com\ruoyi\system\mapper\SysNoticeMapper.class +com\ruoyi\system\domain\SysConfig.class +com\ruoyi\system\domain\ClientDataReport.class +com\ruoyi\system\service\ISysUserOnlineService.class +com\ruoyi\system\service\ISysPostService.class +com\ruoyi\system\domain\SysCache.class +com\ruoyi\system\mapper\SysPostMapper.class +com\ruoyi\system\service\impl\SysMenuServiceImpl.class +com\ruoyi\system\service\ISysMenuService.class +com\ruoyi\system\domain\SysPost.class +com\ruoyi\system\mapper\ClientDeviceMapper.class +com\ruoyi\system\service\impl\SysUserServiceImpl.class +com\ruoyi\system\domain\ClientAuthKey.class +com\ruoyi\system\mapper\SysDictTypeMapper.class +com\ruoyi\system\service\ISysLogininforService.class +com\ruoyi\system\domain\SysLogininfor.class +com\ruoyi\system\mapper\ClientAccountMapper.class +com\ruoyi\system\service\impl\SysConfigServiceImpl.class +com\ruoyi\system\service\impl\SysLogininforServiceImpl.class +com\ruoyi\system\mapper\SysDeptMapper.class +com\ruoyi\system\mapper\ClientMonitorMapper.class +com\ruoyi\system\domain\ClientErrorReport.class +com\ruoyi\system\mapper\ClientEventLogMapper.class +com\ruoyi\system\domain\RefreshToken.class +com\ruoyi\system\mapper\SysUserPostMapper.class +com\ruoyi\system\domain\SysUserOnline.class +com\ruoyi\system\domain\ClientDevice.class +com\ruoyi\system\mapper\SysUserRoleMapper.class +com\ruoyi\system\service\impl\SysRoleServiceImpl.class +com\ruoyi\system\service\impl\SysDictTypeServiceImpl.class +com\ruoyi\system\domain\vo\RouterVo.class +com\ruoyi\system\service\ISysNoticeService.class +com\ruoyi\system\mapper\SysUserMapper.class +com\ruoyi\system\mapper\SysLogininforMapper.class +com\ruoyi\system\service\impl\SysOperLogServiceImpl.class +com\ruoyi\system\service\ISysDictDataService.class +com\ruoyi\system\mapper\SysMenuMapper.class +com\ruoyi\system\service\ISysRoleService.class +com\ruoyi\system\service\impl\SysNoticeServiceImpl.class +com\ruoyi\system\mapper\SysDictDataMapper.class +com\ruoyi\system\service\ISysOperLogService.class +com\ruoyi\system\mapper\RefreshTokenMapper.class +com\ruoyi\system\service\impl\SysUserOnlineServiceImpl.class +com\ruoyi\system\service\impl\SysDeptServiceImpl.class +com\ruoyi\system\service\impl\SysPostServiceImpl.class +com\ruoyi\system\mapper\SysRoleMenuMapper.class +com\ruoyi\system\domain\SysNotice.class +com\ruoyi\system\mapper\SysRoleDeptMapper.class diff --git a/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..b7890b1 --- /dev/null +++ b/ruoyi-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,67 @@ +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysLogininforMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientInfo.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysRoleDept.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\vo\RouterVo.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysPostMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysMenuMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysUserRole.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysConfigMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysOperLogService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysOperLog.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysLogininfor.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysUserOnline.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysDeptService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientAuthInfo.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysConfig.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\ClientInfoMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysLogininforService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysOperLogMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysRoleMenuMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysNoticeMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysMenuServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysOperLogServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\ClientMonitorMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDictTypeServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysCache.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysUserPost.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserOnlineServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysConfigService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysPostServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\RefreshToken.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysConfigServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysDictTypeMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysPostService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysDictDataService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysDeptMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientDevice.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysLogininforServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\vo\MetaVo.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysUserMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysRoleServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysNotice.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysNoticeService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysRoleMenu.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDictDataServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysUserRoleMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysMenuService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientAccount.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysRoleDeptMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysNoticeServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientDataReport.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysUserPostMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\RefreshTokenMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientEventLog.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientAuthKey.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\ClientErrorReport.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysDictDataMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysUserOnlineService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysDictTypeService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysUserService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\ClientAccountMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\ClientEventLogMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\ISysRoleService.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\domain\SysPost.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\ClientDeviceMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysRoleMapper.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserServiceImpl.java +C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDeptServiceImpl.java diff --git a/ruoyi-ui/.editorconfig b/ruoyi-ui/.editorconfig new file mode 100644 index 0000000..7034f9b --- /dev/null +++ b/ruoyi-ui/.editorconfig @@ -0,0 +1,22 @@ +# 告诉EditorConfig插件,这是根文件,不用继续往上查找 +root = true + +# 匹配全部文件 +[*] +# 设置字符集 +charset = utf-8 +# 缩进风格,可选space、tab +indent_style = space +# 缩进的空格数 +indent_size = 2 +# 结尾换行符,可选lf、cr、crlf +end_of_line = lf +# 在文件结尾插入新行 +insert_final_newline = true +# 删除一行中的前后空格 +trim_trailing_whitespace = true + +# 匹配md结尾的文件 +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development new file mode 100644 index 0000000..5882300 --- /dev/null +++ b/ruoyi-ui/.env.development @@ -0,0 +1,11 @@ +# 页面标题 +VUE_APP_TITLE =ERP管理系统 + +# 开发环境配置 +ENV = 'development' + +# ERP管理系统/开发环境 +VUE_APP_BASE_API = 'http://localhost:8080' + +# 路由懒加载 +VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production new file mode 100644 index 0000000..583111e --- /dev/null +++ b/ruoyi-ui/.env.production @@ -0,0 +1,8 @@ +# 页面标题 +VUE_APP_TITLE = ERP管理系统 + +# 生产环境配置 +ENV = 'production' + +# ERP管理系统/生产环境 +VUE_APP_BASE_API = 'http://8.138.23.49:8080' diff --git a/ruoyi-ui/.env.staging b/ruoyi-ui/.env.staging new file mode 100644 index 0000000..68ace26 --- /dev/null +++ b/ruoyi-ui/.env.staging @@ -0,0 +1,12 @@ +# 页面标题 +VUE_APP_TITLE = ERP管理系统 + +BABEL_ENV = production + +NODE_ENV = production + +# 测试环境配置 +ENV = 'staging' + +# ERP管理系统/测试环境 +VUE_APP_BASE_API = '/stage-api' diff --git a/ruoyi-ui/.gitignore b/ruoyi-ui/.gitignore new file mode 100644 index 0000000..78a752d --- /dev/null +++ b/ruoyi-ui/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/ruoyi-ui/README.md b/ruoyi-ui/README.md new file mode 100644 index 0000000..00c0ab8 --- /dev/null +++ b/ruoyi-ui/README.md @@ -0,0 +1,30 @@ +## 开发 + +```bash +# 克隆项目 +git clone https://gitee.com/y_project/RuoYi-Vue + +# 进入项目目录 +cd ruoyi-ui + +# 安装依赖 +npm install + +# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 +npm install --registry=https://registry.npmmirror.com + +# 启动服务 +npm run dev +``` + +浏览器访问 http://localhost:80 + +## 发布 + +```bash +# 构建测试环境 +npm run build:stage + +# 构建生产环境 +npm run build:prod +``` \ No newline at end of file diff --git a/ruoyi-ui/babel.config.js b/ruoyi-ui/babel.config.js new file mode 100644 index 0000000..c8267b2 --- /dev/null +++ b/ruoyi-ui/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + 'plugins': ['dynamic-import-node'] + } + } +} \ No newline at end of file diff --git a/ruoyi-ui/bin/build.bat b/ruoyi-ui/bin/build.bat new file mode 100644 index 0000000..dda590d --- /dev/null +++ b/ruoyi-ui/bin/build.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅdistļ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run build:prod + +pause \ No newline at end of file diff --git a/ruoyi-ui/bin/package.bat b/ruoyi-ui/bin/package.bat new file mode 100644 index 0000000..0e5bc0f --- /dev/null +++ b/ruoyi-ui/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] װWeḅnode_modulesļ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm install --registry=https://registry.npmmirror.com + +pause \ No newline at end of file diff --git a/ruoyi-ui/bin/run-web.bat b/ruoyi-ui/bin/run-web.bat new file mode 100644 index 0000000..d30deae --- /dev/null +++ b/ruoyi-ui/bin/run-web.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] ʹ Vue CLI Web ̡ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run dev + +pause \ No newline at end of file diff --git a/ruoyi-ui/build/index.js b/ruoyi-ui/build/index.js new file mode 100644 index 0000000..0c57de2 --- /dev/null +++ b/ruoyi-ui/build/index.js @@ -0,0 +1,35 @@ +const { run } = require('runjs') +const chalk = require('chalk') +const config = require('../vue.config.js') +const rawArgv = process.argv.slice(2) +const args = rawArgv.join(' ') + +if (process.env.npm_config_preview || rawArgv.includes('--preview')) { + const report = rawArgv.includes('--report') + + run(`vue-cli-service build ${args}`) + + const port = 9526 + const publicPath = config.publicPath + + var connect = require('connect') + var serveStatic = require('serve-static') + const app = connect() + + app.use( + publicPath, + serveStatic('./dist', { + index: ['index.html', '/'] + }) + ) + + app.listen(port, function () { + console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) + if (report) { + console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) + } + + }) +} else { + run(`vue-cli-service build ${args}`) +} diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json new file mode 100644 index 0000000..a9abaf6 --- /dev/null +++ b/ruoyi-ui/package.json @@ -0,0 +1,74 @@ +{ + "name": "ruoyi", + "version": "3.9.0", + "description": "ERP管理系统", + "author": "若依", + "license": "MIT", + "scripts": { + "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" + }, + "keywords": [ + "vue", + "admin", + "dashboard", + "element-ui", + "boilerplate", + "admin-template", + "management-system" + ], + "repository": { + "type": "git", + "url": "https://gitee.com/y_project/RuoYi-Vue.git" + }, + "dependencies": { + "@riophae/vue-treeselect": "0.4.0", + "axios": "0.28.1", + "clipboard": "2.0.8", + "core-js": "^3.43.0", + "echarts": "5.4.0", + "element-ui": "2.15.14", + "exceljs": "^4.4.0", + "file-saver": "2.0.5", + "fuse.js": "6.4.3", + "highlight.js": "9.18.5", + "js-beautify": "1.13.0", + "js-cookie": "3.0.1", + "jsencrypt": "3.0.0-rc.1", + "nprogress": "0.2.0", + "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", + "vue-router": "3.4.9", + "vuedraggable": "2.24.3", + "vuex": "3.6.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.4.6", + "@vue/cli-service": "4.4.6", + "babel-plugin-dynamic-import-node": "2.3.3", + "chalk": "4.1.0", + "compression-webpack-plugin": "6.1.2", + "connect": "3.6.6", + "sass": "1.32.13", + "sass-loader": "10.1.1", + "script-ext-html-webpack-plugin": "2.1.5", + "svg-sprite-loader": "5.1.1", + "vue-template-compiler": "2.6.12" + }, + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/ruoyi-ui/public/favicon.ico b/ruoyi-ui/public/favicon.ico new file mode 100644 index 0000000..e263760 Binary files /dev/null and b/ruoyi-ui/public/favicon.ico differ diff --git a/ruoyi-ui/public/index.html b/ruoyi-ui/public/index.html new file mode 100644 index 0000000..925455c --- /dev/null +++ b/ruoyi-ui/public/index.html @@ -0,0 +1,208 @@ + + + + + + + + + <%= webpackConfig.name %> + + + + +
+
+
+
+
+
正在加载系统资源,请耐心等待
+
+
+ + diff --git a/ruoyi-ui/public/robots.txt b/ruoyi-ui/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/ruoyi-ui/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/ruoyi-ui/public/styles/theme-chalk/index.css b/ruoyi-ui/public/styles/theme-chalk/index.css new file mode 100644 index 0000000..b78d5a9 --- /dev/null +++ b/ruoyi-ui/public/styles/theme-chalk/index.css @@ -0,0 +1 @@ +@charset "UTF-8";[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\e6a0"}.el-icon-ice-cream-square:before{content:"\e6a3"}.el-icon-lollipop:before{content:"\e6a4"}.el-icon-potato-strips:before{content:"\e6a5"}.el-icon-milk-tea:before{content:"\e6a6"}.el-icon-ice-drink:before{content:"\e6a7"}.el-icon-ice-tea:before{content:"\e6a9"}.el-icon-coffee:before{content:"\e6aa"}.el-icon-orange:before{content:"\e6ab"}.el-icon-pear:before{content:"\e6ac"}.el-icon-apple:before{content:"\e6ad"}.el-icon-cherry:before{content:"\e6ae"}.el-icon-watermelon:before{content:"\e6af"}.el-icon-grape:before{content:"\e6b0"}.el-icon-refrigerator:before{content:"\e6b1"}.el-icon-goblet-square-full:before{content:"\e6b2"}.el-icon-goblet-square:before{content:"\e6b3"}.el-icon-goblet-full:before{content:"\e6b4"}.el-icon-goblet:before{content:"\e6b5"}.el-icon-cold-drink:before{content:"\e6b6"}.el-icon-coffee-cup:before{content:"\e6b8"}.el-icon-water-cup:before{content:"\e6b9"}.el-icon-hot-water:before{content:"\e6ba"}.el-icon-ice-cream:before{content:"\e6bb"}.el-icon-dessert:before{content:"\e6bc"}.el-icon-sugar:before{content:"\e6bd"}.el-icon-tableware:before{content:"\e6be"}.el-icon-burger:before{content:"\e6bf"}.el-icon-knife-fork:before{content:"\e6c1"}.el-icon-fork-spoon:before{content:"\e6c2"}.el-icon-chicken:before{content:"\e6c3"}.el-icon-food:before{content:"\e6c4"}.el-icon-dish-1:before{content:"\e6c5"}.el-icon-dish:before{content:"\e6c6"}.el-icon-moon-night:before{content:"\e6ee"}.el-icon-moon:before{content:"\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\e6f1"}.el-icon-partly-cloudy:before{content:"\e6f2"}.el-icon-cloudy:before{content:"\e6f3"}.el-icon-sunny:before{content:"\e6f6"}.el-icon-sunset:before{content:"\e6f7"}.el-icon-sunrise-1:before{content:"\e6f8"}.el-icon-sunrise:before{content:"\e6f9"}.el-icon-heavy-rain:before{content:"\e6fa"}.el-icon-lightning:before{content:"\e6fb"}.el-icon-light-rain:before{content:"\e6fc"}.el-icon-wind-power:before{content:"\e6fd"}.el-icon-baseball:before{content:"\e712"}.el-icon-soccer:before{content:"\e713"}.el-icon-football:before{content:"\e715"}.el-icon-basketball:before{content:"\e716"}.el-icon-ship:before{content:"\e73f"}.el-icon-truck:before{content:"\e740"}.el-icon-bicycle:before{content:"\e741"}.el-icon-mobile-phone:before{content:"\e6d3"}.el-icon-service:before{content:"\e6d4"}.el-icon-key:before{content:"\e6e2"}.el-icon-unlock:before{content:"\e6e4"}.el-icon-lock:before{content:"\e6e5"}.el-icon-watch:before{content:"\e6fe"}.el-icon-watch-1:before{content:"\e6ff"}.el-icon-timer:before{content:"\e702"}.el-icon-alarm-clock:before{content:"\e703"}.el-icon-map-location:before{content:"\e704"}.el-icon-delete-location:before{content:"\e705"}.el-icon-add-location:before{content:"\e706"}.el-icon-location-information:before{content:"\e707"}.el-icon-location-outline:before{content:"\e708"}.el-icon-location:before{content:"\e79e"}.el-icon-place:before{content:"\e709"}.el-icon-discover:before{content:"\e70a"}.el-icon-first-aid-kit:before{content:"\e70b"}.el-icon-trophy-1:before{content:"\e70c"}.el-icon-trophy:before{content:"\e70d"}.el-icon-medal:before{content:"\e70e"}.el-icon-medal-1:before{content:"\e70f"}.el-icon-stopwatch:before{content:"\e710"}.el-icon-mic:before{content:"\e711"}.el-icon-copy-document:before{content:"\e718"}.el-icon-full-screen:before{content:"\e719"}.el-icon-switch-button:before{content:"\e71b"}.el-icon-aim:before{content:"\e71c"}.el-icon-crop:before{content:"\e71d"}.el-icon-odometer:before{content:"\e71e"}.el-icon-time:before{content:"\e71f"}.el-icon-bangzhu:before{content:"\e724"}.el-icon-close-notification:before{content:"\e726"}.el-icon-microphone:before{content:"\e727"}.el-icon-turn-off-microphone:before{content:"\e728"}.el-icon-position:before{content:"\e729"}.el-icon-postcard:before{content:"\e72a"}.el-icon-message:before{content:"\e72b"}.el-icon-chat-line-square:before{content:"\e72d"}.el-icon-chat-dot-square:before{content:"\e72e"}.el-icon-chat-dot-round:before{content:"\e72f"}.el-icon-chat-square:before{content:"\e730"}.el-icon-chat-line-round:before{content:"\e731"}.el-icon-chat-round:before{content:"\e732"}.el-icon-set-up:before{content:"\e733"}.el-icon-turn-off:before{content:"\e734"}.el-icon-open:before{content:"\e735"}.el-icon-connection:before{content:"\e736"}.el-icon-link:before{content:"\e737"}.el-icon-cpu:before{content:"\e738"}.el-icon-thumb:before{content:"\e739"}.el-icon-female:before{content:"\e73a"}.el-icon-male:before{content:"\e73b"}.el-icon-guide:before{content:"\e73c"}.el-icon-news:before{content:"\e73e"}.el-icon-price-tag:before{content:"\e744"}.el-icon-discount:before{content:"\e745"}.el-icon-wallet:before{content:"\e747"}.el-icon-coin:before{content:"\e748"}.el-icon-money:before{content:"\e749"}.el-icon-bank-card:before{content:"\e74a"}.el-icon-box:before{content:"\e74b"}.el-icon-present:before{content:"\e74c"}.el-icon-sell:before{content:"\e6d5"}.el-icon-sold-out:before{content:"\e6d6"}.el-icon-shopping-bag-2:before{content:"\e74d"}.el-icon-shopping-bag-1:before{content:"\e74e"}.el-icon-shopping-cart-2:before{content:"\e74f"}.el-icon-shopping-cart-1:before{content:"\e750"}.el-icon-shopping-cart-full:before{content:"\e751"}.el-icon-smoking:before{content:"\e752"}.el-icon-no-smoking:before{content:"\e753"}.el-icon-house:before{content:"\e754"}.el-icon-table-lamp:before{content:"\e755"}.el-icon-school:before{content:"\e756"}.el-icon-office-building:before{content:"\e757"}.el-icon-toilet-paper:before{content:"\e758"}.el-icon-notebook-2:before{content:"\e759"}.el-icon-notebook-1:before{content:"\e75a"}.el-icon-files:before{content:"\e75b"}.el-icon-collection:before{content:"\e75c"}.el-icon-receiving:before{content:"\e75d"}.el-icon-suitcase-1:before{content:"\e760"}.el-icon-suitcase:before{content:"\e761"}.el-icon-film:before{content:"\e763"}.el-icon-collection-tag:before{content:"\e765"}.el-icon-data-analysis:before{content:"\e766"}.el-icon-pie-chart:before{content:"\e767"}.el-icon-data-board:before{content:"\e768"}.el-icon-data-line:before{content:"\e76d"}.el-icon-reading:before{content:"\e769"}.el-icon-magic-stick:before{content:"\e76a"}.el-icon-coordinate:before{content:"\e76b"}.el-icon-mouse:before{content:"\e76c"}.el-icon-brush:before{content:"\e76e"}.el-icon-headset:before{content:"\e76f"}.el-icon-umbrella:before{content:"\e770"}.el-icon-scissors:before{content:"\e771"}.el-icon-mobile:before{content:"\e773"}.el-icon-attract:before{content:"\e774"}.el-icon-monitor:before{content:"\e775"}.el-icon-search:before{content:"\e778"}.el-icon-takeaway-box:before{content:"\e77a"}.el-icon-paperclip:before{content:"\e77d"}.el-icon-printer:before{content:"\e77e"}.el-icon-document-add:before{content:"\e782"}.el-icon-document:before{content:"\e785"}.el-icon-document-checked:before{content:"\e786"}.el-icon-document-copy:before{content:"\e787"}.el-icon-document-delete:before{content:"\e788"}.el-icon-document-remove:before{content:"\e789"}.el-icon-tickets:before{content:"\e78b"}.el-icon-folder-checked:before{content:"\e77f"}.el-icon-folder-delete:before{content:"\e780"}.el-icon-folder-remove:before{content:"\e781"}.el-icon-folder-add:before{content:"\e783"}.el-icon-folder-opened:before{content:"\e784"}.el-icon-folder:before{content:"\e78a"}.el-icon-edit-outline:before{content:"\e764"}.el-icon-edit:before{content:"\e78c"}.el-icon-date:before{content:"\e78e"}.el-icon-c-scale-to-original:before{content:"\e7c6"}.el-icon-view:before{content:"\e6ce"}.el-icon-loading:before{content:"\e6cf"}.el-icon-rank:before{content:"\e6d1"}.el-icon-sort-down:before{content:"\e7c4"}.el-icon-sort-up:before{content:"\e7c5"}.el-icon-sort:before{content:"\e6d2"}.el-icon-finished:before{content:"\e6cd"}.el-icon-refresh-left:before{content:"\e6c7"}.el-icon-refresh-right:before{content:"\e6c8"}.el-icon-refresh:before{content:"\e6d0"}.el-icon-video-play:before{content:"\e7c0"}.el-icon-video-pause:before{content:"\e7c1"}.el-icon-d-arrow-right:before{content:"\e6dc"}.el-icon-d-arrow-left:before{content:"\e6dd"}.el-icon-arrow-up:before{content:"\e6e1"}.el-icon-arrow-down:before{content:"\e6df"}.el-icon-arrow-right:before{content:"\e6e0"}.el-icon-arrow-left:before{content:"\e6de"}.el-icon-top-right:before{content:"\e6e7"}.el-icon-top-left:before{content:"\e6e8"}.el-icon-top:before{content:"\e6e6"}.el-icon-bottom:before{content:"\e6eb"}.el-icon-right:before{content:"\e6e9"}.el-icon-back:before{content:"\e6ea"}.el-icon-bottom-right:before{content:"\e6ec"}.el-icon-bottom-left:before{content:"\e6ed"}.el-icon-caret-top:before{content:"\e78f"}.el-icon-caret-bottom:before{content:"\e790"}.el-icon-caret-right:before{content:"\e791"}.el-icon-caret-left:before{content:"\e792"}.el-icon-d-caret:before{content:"\e79a"}.el-icon-share:before{content:"\e793"}.el-icon-menu:before{content:"\e798"}.el-icon-s-grid:before{content:"\e7a6"}.el-icon-s-check:before{content:"\e7a7"}.el-icon-s-data:before{content:"\e7a8"}.el-icon-s-opportunity:before{content:"\e7aa"}.el-icon-s-custom:before{content:"\e7ab"}.el-icon-s-claim:before{content:"\e7ad"}.el-icon-s-finance:before{content:"\e7ae"}.el-icon-s-comment:before{content:"\e7af"}.el-icon-s-flag:before{content:"\e7b0"}.el-icon-s-marketing:before{content:"\e7b1"}.el-icon-s-shop:before{content:"\e7b4"}.el-icon-s-open:before{content:"\e7b5"}.el-icon-s-management:before{content:"\e7b6"}.el-icon-s-ticket:before{content:"\e7b7"}.el-icon-s-release:before{content:"\e7b8"}.el-icon-s-home:before{content:"\e7b9"}.el-icon-s-promotion:before{content:"\e7ba"}.el-icon-s-operation:before{content:"\e7bb"}.el-icon-s-unfold:before{content:"\e7bc"}.el-icon-s-fold:before{content:"\e7a9"}.el-icon-s-platform:before{content:"\e7bd"}.el-icon-s-order:before{content:"\e7be"}.el-icon-s-cooperation:before{content:"\e7bf"}.el-icon-bell:before{content:"\e725"}.el-icon-message-solid:before{content:"\e799"}.el-icon-video-camera:before{content:"\e772"}.el-icon-video-camera-solid:before{content:"\e796"}.el-icon-camera:before{content:"\e779"}.el-icon-camera-solid:before{content:"\e79b"}.el-icon-download:before{content:"\e77c"}.el-icon-upload2:before{content:"\e77b"}.el-icon-upload:before{content:"\e7c3"}.el-icon-picture-outline-round:before{content:"\e75f"}.el-icon-picture-outline:before{content:"\e75e"}.el-icon-picture:before{content:"\e79f"}.el-icon-close:before{content:"\e6db"}.el-icon-check:before{content:"\e6da"}.el-icon-plus:before{content:"\e6d9"}.el-icon-minus:before{content:"\e6d8"}.el-icon-help:before{content:"\e73d"}.el-icon-s-help:before{content:"\e7b3"}.el-icon-circle-close:before{content:"\e78d"}.el-icon-circle-check:before{content:"\e720"}.el-icon-circle-plus-outline:before{content:"\e723"}.el-icon-remove-outline:before{content:"\e722"}.el-icon-zoom-out:before{content:"\e776"}.el-icon-zoom-in:before{content:"\e777"}.el-icon-error:before{content:"\e79d"}.el-icon-success:before{content:"\e79c"}.el-icon-circle-plus:before{content:"\e7a0"}.el-icon-remove:before{content:"\e7a2"}.el-icon-info:before{content:"\e7a1"}.el-icon-question:before{content:"\e7a4"}.el-icon-warning-outline:before{content:"\e6c9"}.el-icon-warning:before{content:"\e7a3"}.el-icon-goods:before{content:"\e7c2"}.el-icon-s-goods:before{content:"\e7b2"}.el-icon-star-off:before{content:"\e717"}.el-icon-star-on:before{content:"\e797"}.el-icon-more-outline:before{content:"\e6cc"}.el-icon-more:before{content:"\e794"}.el-icon-phone-outline:before{content:"\e6cb"}.el-icon-phone:before{content:"\e795"}.el-icon-user:before{content:"\e6e3"}.el-icon-user-solid:before{content:"\e7a5"}.el-icon-setting:before{content:"\e6ca"}.el-icon-s-tools:before{content:"\e7ac"}.el-icon-delete:before{content:"\e6d7"}.el-icon-delete-solid:before{content:"\e7c9"}.el-icon-eleme:before{content:"\e7c7"}.el-icon-platform-eleme:before{content:"\e7ca"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}.el-pagination{white-space:nowrap;padding:2px 5px;color:#303133;font-weight:700}.el-pagination::after,.el-pagination::before{display:table;content:""}.el-pagination::after{clear:both}.el-pagination button,.el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;-webkit-box-sizing:border-box;box-sizing:border-box}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield;line-height:normal}.el-pagination .el-input__suffix{right:0;-webkit-transform:scale(.8);transform:scale(.8)}.el-pagination .el-select .el-input{width:100px;margin:0 5px}.el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px}.el-pagination button{border:none;padding:0 6px;background:0 0}.el-pagination button:focus{outline:0}.el-pagination button:hover{color:#409EFF}.el-pagination button:disabled{color:#C0C4CC;background-color:#FFF;cursor:not-allowed}.el-pagination .btn-next,.el-pagination .btn-prev{background:center center no-repeat #FFF;background-size:16px;cursor:pointer;margin:0;color:#303133}.el-pagination .btn-next .el-icon,.el-pagination .btn-prev .el-icon{display:block;font-size:12px;font-weight:700}.el-pagination .btn-prev{padding-right:12px}.el-pagination .btn-next{padding-left:12px}.el-pagination .el-pager li.disabled{color:#C0C4CC;cursor:not-allowed}.el-pager li,.el-pager li.btn-quicknext:hover,.el-pager li.btn-quickprev:hover{cursor:pointer}.el-pagination--small .btn-next,.el-pagination--small .btn-prev,.el-pagination--small .el-pager li,.el-pagination--small .el-pager li.btn-quicknext,.el-pagination--small .el-pager li.btn-quickprev,.el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.el-pagination--small .arrow.disabled{visibility:hidden}.el-pagination--small .more::before,.el-pagination--small li.more::before{line-height:24px}.el-pagination--small button,.el-pagination--small span:not([class*=suffix]){height:22px;line-height:22px}.el-pagination--small .el-pagination__editor,.el-pagination--small .el-pagination__editor.el-input .el-input__inner{height:22px}.el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#606266}.el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.el-pagination__sizes .el-input .el-input__inner:hover{border-color:#409EFF}.el-pagination__total{margin-right:10px;font-weight:400;color:#606266}.el-pagination__jump{margin-left:24px;font-weight:400;color:#606266}.el-pagination__jump .el-input__inner{padding:0 3px}.el-pagination__rightwrapper{float:right}.el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:3px}.el-pager,.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev{padding:0}.el-dialog,.el-pager li{-webkit-box-sizing:border-box}.el-pagination__editor.el-input{width:50px}.el-pagination__editor.el-input .el-input__inner{height:28px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev,.el-pagination.is-background .el-pager li{margin:0 5px;background-color:#f4f4f5;color:#606266;min-width:30px;border-radius:2px}.el-pagination.is-background .btn-next.disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-prev.disabled,.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .el-pager li.disabled{color:#C0C4CC}.el-pagination.is-background .el-pager li:not(.disabled):hover{color:#409EFF}.el-pagination.is-background .el-pager li:not(.disabled).active{background-color:#409EFF;color:#FFF}.el-pagination.is-background.el-pagination--small .btn-next,.el-pagination.is-background.el-pagination--small .btn-prev,.el-pagination.is-background.el-pagination--small .el-pager li{margin:0 3px;min-width:22px}.el-pager,.el-pager li{vertical-align:top;margin:0;display:inline-block}.el-pager{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;list-style:none;font-size:0}.el-pager .more::before{line-height:30px}.el-pager li{padding:0 4px;background:#FFF;font-size:13px;min-width:35.5px;height:28px;line-height:28px;box-sizing:border-box;text-align:center}.el-pager li.btn-quicknext,.el-pager li.btn-quickprev{line-height:28px;color:#303133}.el-pager li.btn-quicknext.disabled,.el-pager li.btn-quickprev.disabled{color:#C0C4CC}.el-pager li.active+li{border-left:0}.el-pager li:hover{color:#409EFF}.el-pager li.active{color:#409EFF;cursor:default}@-webkit-keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}.el-dialog{position:relative;margin:0 auto 50px;background:#FFF;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#409EFF}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px;word-break:break-all}.el-dialog__footer{padding:10px 20px 20px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-autocomplete{position:relative;display:inline-block}.el-autocomplete-suggestion{margin:5px 0;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;border:1px solid #E4E7ED;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#FFF}.el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#606266;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-autocomplete-suggestion li.highlighted,.el-autocomplete-suggestion li:hover{background-color:#F5F7FA}.el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.el-autocomplete-suggestion.is-loading li::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-autocomplete-suggestion.is-loading li:hover{background-color:#FFF}.el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.el-dropdown{display:inline-block;position:relative;color:#606266;font-size:14px}.el-dropdown .el-button-group{display:block}.el-dropdown .el-button-group .el-button{float:none}.el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.el-dropdown .el-dropdown__caret-button::before{content:'';position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:rgba(255,255,255,.5)}.el-dropdown .el-dropdown__caret-button.el-button--default::before{background:rgba(220,223,230,.5)}.el-dropdown .el-dropdown__caret-button:hover:not(.is-disabled)::before{top:0;bottom:0}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.el-dropdown__icon{font-size:12px;margin:0 3px}.el-dropdown .el-dropdown-selfdefine:focus:active,.el-dropdown .el-dropdown-selfdefine:focus:not(.focusing){outline-width:0}.el-dropdown [disabled]{cursor:not-allowed;color:#bbb}.el-dropdown-menu{position:absolute;top:0;left:0;z-index:10;padding:10px 0;margin:5px 0;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-dropdown-menu__item,.el-menu-item{font-size:14px;padding:0 20px;cursor:pointer}.el-dropdown-menu__item{list-style:none;line-height:36px;margin:0;color:#606266;outline:0}.el-dropdown-menu__item:focus,.el-dropdown-menu__item:not(.is-disabled):hover{background-color:#ecf5ff;color:#66b1ff}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #EBEEF5}.el-dropdown-menu__item--divided:before{content:'';height:6px;display:block;margin:0 -20px;background-color:#FFF}.el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.el-dropdown-menu--medium{padding:6px 0}.el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.el-dropdown-menu--small{padding:6px 0}.el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.el-dropdown-menu--mini{padding:3px 0}.el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#FFF}.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu::after,.el-menu::before{display:table;content:""}.el-breadcrumb__item:last-child .el-breadcrumb__separator,.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu::after{clear:both}.el-menu.el-menu--horizontal{border-bottom:solid 1px #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409EFF;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--collapse .el-submenu,.el-menu-item{position:relative}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#FFF;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409EFF;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;list-style:none}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-menu .el-submenu{min-width:200px}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #E4E7ED;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:none;transform:none}.el-menu--popup{z-index:100;min-width:200px;border:none;padding:5px 0;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{color:#303133;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.el-radio-button__inner,.el-submenu__title{-webkit-box-sizing:border-box;position:relative;white-space:nowrap}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409EFF}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409EFF}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.el-radio-button__inner,.el-radio-group{display:inline-block;line-height:1;vertical-align:middle}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{-webkit-transition:.2s;transition:.2s;opacity:0}.el-radio-group{font-size:0}.el-radio-button{position:relative;display:inline-block;outline:0}.el-radio-button__inner{background:#FFF;border:1px solid #DCDFE6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#409EFF}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #409EFF;box-shadow:-1px 0 0 0 #409EFF}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#F2F6FC}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-picker-panel,.el-popover,.el-select-dropdown,.el-table-filter,.el-time-panel{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-switch{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{-webkit-transition:.2s;transition:.2s;height:20px;font-size:14px;font-weight:500;vertical-align:middle;color:#303133}.el-switch__label.is-active{color:#409EFF}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #DCDFE6;outline:0;border-radius:10px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#DCDFE6;-webkit-transition:border-color .3s,background-color .3s;transition:border-color .3s,background-color .3s;vertical-align:middle}.el-input__prefix,.el-input__suffix{-webkit-transition:all .3s;color:#C0C4CC}.el-switch__core:after{content:"";position:absolute;top:1px;left:1px;border-radius:100%;-webkit-transition:all .3s;transition:all .3s;width:16px;height:16px;background-color:#FFF}.el-switch.is-checked .el-switch__core{border-color:#409EFF;background-color:#409EFF}.el-switch.is-checked .el-switch__core::after{left:100%;margin-left:-17px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter,.el-switch .label-fade-leave-active{opacity:0}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #E4E7ED;border-radius:4px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item{padding-right:40px}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#409EFF;background-color:#FFF}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#F5F7FA}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\e6da";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#FFF}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#F5F7FA}.el-select-dropdown__item.selected{color:#409EFF;font-weight:700}.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#E4E7ED}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{display:inline-block;position:relative}.el-select .el-select__tags>span{display:contents}.el-select:hover .el-input__inner{border-color:#C0C4CC}.el-select .el-input__inner{cursor:pointer;padding-right:35px}.el-select .el-input__inner:focus{border-color:#409EFF}.el-select .el-input .el-select__caret{color:#C0C4CC;font-size:14px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);cursor:pointer}.el-select .el-input .el-select__caret.is-reverse{-webkit-transform:rotateZ(0);transform:rotateZ(0)}.el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);border-radius:100%;color:#C0C4CC;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-select .el-input .el-select__caret.is-show-close:hover{color:#909399}.el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.el-select .el-input.is-disabled .el-input__inner:hover{border-color:#E4E7ED}.el-range-editor.is-active,.el-range-editor.is-active:hover,.el-select .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-select>.el-input{display:block}.el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.el-select__input.is-mini{height:14px}.el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#C0C4CC;line-height:18px;font-size:14px}.el-select__close:hover{color:#909399}.el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-select__tags-text{overflow:hidden;text-overflow:ellipsis}.el-select .el-tag{-webkit-box-sizing:border-box;box-sizing:border-box;border-color:transparent;margin:2px 0 2px 6px;background-color:#f0f2f5;display:-webkit-box;display:-ms-flexbox;display:flex;max-width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-select .el-tag__close.el-icon-close{background-color:#C0C4CC;top:0;color:#FFF;-ms-flex-negative:0;flex-shrink:0}.el-select .el-tag__close.el-icon-close:hover{background-color:#909399}.el-table,.el-table__expanded-cell{background-color:#FFF}.el-select .el-tag__close.el-icon-close::before{display:block;-webkit-transform:translate(0,.5px);transform:translate(0,.5px)}.el-table{position:relative;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-flex:1;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;font-size:12px;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit .el-table__cell.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th.el-table__cell{background:#F5F7FA}.el-table .el-table__cell{padding:12px 0;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table .el-table__cell.is-center{text-align:center}.el-table .el-table__cell.is-right{text-align:right}.el-table .el-table__cell.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table .el-table__cell.is-hidden>*{visibility:hidden}.el-table--medium .el-table__cell{padding:10px 0}.el-table--small{font-size:12px}.el-table--small .el-table__cell{padding:8px 0}.el-table--mini{font-size:12px}.el-table--mini .el-table__cell{padding:6px 0}.el-table tr{background-color:#FFF}.el-table tr input[type=checkbox]{margin:0}.el-table td.el-table__cell,.el-table th.el-table__cell.is-leaf{border-bottom:1px solid #EBEEF5}.el-table th.el-table__cell.is-sortable{cursor:pointer}.el-table th.el-table__cell{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#FFF}.el-table th.el-table__cell>.cell{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th.el-table__cell>.cell.highlight{color:#409EFF}.el-table th.el-table__cell.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td.el-table__cell div{-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td,.el-table .cell,.el-table-filter{-webkit-box-sizing:border-box}.el-table td.el-table__cell.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-left:10px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #EBEEF5}.el-table--border::after,.el-table--group::after,.el-table::before{content:'';position:absolute;background-color:#EBEEF5;z-index:1}.el-table--border::after,.el-table--group::after{top:0;right:0;width:1px;height:100%}.el-table::before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border .el-table__cell,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #EBEEF5}.el-table--border .el-table__cell:first-child .cell{padding-left:10px}.el-table--border th.el-table__cell.gutter:last-of-type{border-bottom:1px solid #EBEEF5;border-bottom-width:1px}.el-table--border th.el-table__cell,.el-table__fixed-right-patch{border-bottom:1px solid #EBEEF5}.el-table--hidden{visibility:hidden}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;-webkit-box-shadow:0 0 10px rgba(0,0,0,.12);box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right::before,.el-table__fixed::before{content:'';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#EBEEF5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#FFF}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td.el-table__cell{border-top:1px solid #EBEEF5;background-color:#F5F7FA;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td.el-table__cell{border-top:1px solid #EBEEF5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td.el-table__cell,.el-table__header-wrapper tbody td.el-table__cell{background-color:#F5F7FA;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #EBEEF5}.el-table .caret-wrapper{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#C0C4CC;top:5px}.el-table .sort-caret.descending{border-top-color:#C0C4CC;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409EFF}.el-table .descending .sort-caret.descending{border-top-color:#409EFF}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{background:#FAFAFA}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td.el-table__cell,.el-table--striped .el-table__body tr.el-table__row--striped.selection-row td.el-table__cell{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.selection-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped>td.el-table__cell,.el-table__body tr.hover-row.selection-row>td.el-table__cell,.el-table__body tr.hover-row>td.el-table__cell{background-color:#F5F7FA}.el-table__body tr.current-row>td.el-table__cell,.el-table__body tr.selection-row>td.el-table__cell{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #EBEEF5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;-webkit-transform:scale(.75);transform:scale(.75)}.el-table--enable-row-transition .el-table__body td.el-table__cell{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell{background-color:#F5F7FA}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #EBEEF5;border-radius:2px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409EFF;color:#FFF}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #EBEEF5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-date-table td.in-range div,.el-date-table td.in-range div:hover,.el-date-table.is-week-mode .el-date-table__row.current div,.el-date-table.is-week-mode .el-date-table__row:hover div{background-color:#F2F6FC}.el-table-filter__bottom button:hover{color:#409EFF}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-date-table{font-size:12px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#606266}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td{width:32px;height:30px;padding:4px 0;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.el-date-table td div{height:30px;padding:3px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);border-radius:50%}.el-date-table td.next-month,.el-date-table td.prev-month{color:#C0C4CC}.el-date-table td.today{position:relative}.el-date-table td.today span{color:#409EFF;font-weight:700}.el-date-table td.today.end-date span,.el-date-table td.today.start-date span{color:#FFF}.el-date-table td.available:hover{color:#409EFF}.el-date-table td.current:not(.disabled) span{color:#FFF;background-color:#409EFF}.el-date-table td.end-date div,.el-date-table td.start-date div{color:#FFF}.el-date-table td.end-date span,.el-date-table td.start-date span{background-color:#409EFF}.el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td.disabled div{background-color:#F5F7FA;opacity:1;cursor:not-allowed;color:#C0C4CC}.el-date-table td.selected div{margin-left:5px;margin-right:5px;background-color:#F2F6FC;border-radius:15px}.el-date-table td.selected div:hover{background-color:#F2F6FC}.el-date-table td.selected span{background-color:#409EFF;color:#FFF;border-radius:15px}.el-date-table td.week{font-size:80%;color:#606266}.el-month-table,.el-year-table{font-size:12px;border-collapse:collapse}.el-date-table th{padding:5px;color:#606266;font-weight:400;border-bottom:solid 1px #EBEEF5}.el-month-table{margin:-1px}.el-month-table td{text-align:center;padding:8px 0;cursor:pointer}.el-month-table td div{height:48px;padding:6px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-month-table td.today .cell{color:#409EFF;font-weight:700}.el-month-table td.today.end-date .cell,.el-month-table td.today.start-date .cell{color:#FFF}.el-month-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-month-table td.disabled .cell:hover{color:#C0C4CC}.el-month-table td .cell{width:60px;height:36px;display:block;line-height:36px;color:#606266;margin:0 auto;border-radius:18px}.el-month-table td .cell:hover{color:#409EFF}.el-month-table td.in-range div,.el-month-table td.in-range div:hover{background-color:#F2F6FC}.el-month-table td.end-date div,.el-month-table td.start-date div{color:#FFF}.el-month-table td.end-date .cell,.el-month-table td.start-date .cell{color:#FFF;background-color:#409EFF}.el-month-table td.start-date div{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-month-table td.end-date div{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-month-table td.current:not(.disabled) .cell{color:#409EFF}.el-year-table{margin:-1px}.el-year-table .el-icon{color:#303133}.el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.el-year-table td.today .cell{color:#409EFF;font-weight:700}.el-year-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-year-table td.disabled .cell:hover{color:#C0C4CC}.el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#606266;margin:0 auto}.el-year-table td .cell:hover,.el-year-table td.current:not(.disabled) .cell{color:#409EFF}.el-date-range-picker{width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker__header{position:relative;text-align:center;height:28px}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{font-size:16px;font-weight:500;margin-right:50px}.el-date-range-picker__content{float:left;width:50%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:16px}.el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{-webkit-box-sizing:border-box;box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#303133}.el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#FFF}.el-date-picker{width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-picker__header{margin:12px;text-align:center}.el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #EBEEF5}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#606266}.el-date-picker__header-label.active,.el-date-picker__header-label:hover{color:#409EFF}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{padding:10px;text-align:center}.el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.time-select{margin:5px 0;min-width:0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.selected:not(.disabled){color:#409EFF;font-weight:700}.time-select-item.disabled{color:#E4E7ED;cursor:not-allowed}.time-select-item:hover{background-color:#F5F7FA;font-weight:700;cursor:pointer}.el-date-editor{position:relative;display:inline-block;text-align:left}.el-date-editor.el-input,.el-date-editor.el-input__inner{width:220px}.el-date-editor--monthrange.el-input,.el-date-editor--monthrange.el-input__inner{width:300px}.el-date-editor--daterange.el-input,.el-date-editor--daterange.el-input__inner,.el-date-editor--timerange.el-input,.el-date-editor--timerange.el-input__inner{width:350px}.el-date-editor--datetimerange.el-input,.el-date-editor--datetimerange.el-input__inner{width:400px}.el-date-editor--dates .el-input__inner{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .el-icon-circle-close{cursor:pointer}.el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#C0C4CC;float:left;line-height:32px}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#606266}.el-date-editor .el-range-input::-webkit-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input:-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::placeholder{color:#C0C4CC}.el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#303133}.el-date-editor .el-range__close-icon{font-size:14px;color:#C0C4CC;width:25px;display:inline-block;float:right;line-height:32px}.el-range-editor.el-input__inner{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:3px 10px}.el-range-editor .el-range-input{line-height:1}.el-range-editor--medium.el-input__inner{height:36px}.el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.el-range-editor--medium .el-range-input{font-size:14px}.el-range-editor--medium .el-range__close-icon,.el-range-editor--medium .el-range__icon{line-height:28px}.el-range-editor--small.el-input__inner{height:32px}.el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.el-range-editor--small .el-range-input{font-size:13px}.el-range-editor--small .el-range__close-icon,.el-range-editor--small .el-range__icon{line-height:24px}.el-range-editor--mini.el-input__inner{height:28px}.el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.el-range-editor--mini .el-range-input{font-size:12px}.el-range-editor--mini .el-range__close-icon,.el-range-editor--mini .el-range__icon{line-height:20px}.el-range-editor.is-disabled{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled:focus,.el-range-editor.is-disabled:hover{border-color:#E4E7ED}.el-range-editor.is-disabled input{background-color:#F5F7FA;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled input::-webkit-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input:-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::placeholder{color:#C0C4CC}.el-range-editor.is-disabled .el-range-separator{color:#C0C4CC}.el-picker-panel{color:#606266;border:1px solid #E4E7ED;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#FFF;border-radius:4px;line-height:30px;margin:5px 0}.el-picker-panel__body-wrapper::after,.el-picker-panel__body::after{content:"";display:table;clear:both}.el-picker-panel__content{position:relative;margin:15px}.el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#FFF;position:relative;font-size:0}.el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#606266;padding-left:12px;text-align:left;outline:0;cursor:pointer}.el-picker-panel__shortcut:hover{color:#409EFF}.el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#409EFF}.el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.el-picker-panel__icon-btn{font-size:12px;color:#303133;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.el-picker-panel__icon-btn:hover{color:#409EFF}.el-picker-panel__icon-btn.is-disabled{color:#bbb}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;background-color:#FFF;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__input.el-input .el-input__inner,.el-time-spinner__list{padding:0;text-align:center}.el-time-spinner__wrapper.is-arrow{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{-webkit-transform:translateY(-32px);transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#FFF;cursor:default}.el-time-spinner__arrow{font-size:12px;color:#909399;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.el-time-spinner__arrow:hover{color:#409EFF}.el-time-spinner__arrow.el-icon-arrow-up{top:10px}.el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__list{margin:0;list-style:none}.el-time-spinner__list::after,.el-time-spinner__list::before{content:'';display:block;width:100%;height:80px}.el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#606266}.el-time-spinner__item:hover:not(.disabled):not(.active){background:#F5F7FA;cursor:pointer}.el-time-spinner__item.active:not(.disabled){color:#303133;font-weight:700}.el-time-spinner__item.disabled{color:#C0C4CC;cursor:not-allowed}.el-time-panel{margin:5px 0;border:1px solid #E4E7ED;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:content-box;box-sizing:content-box}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content::after,.el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #E4E7ED;border-bottom:1px solid #E4E7ED}.el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#303133}.el-time-panel__btn.confirm{font-weight:800;color:#409EFF}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{position:relative;text-align:center;padding:10px}.el-time-range-picker__cell{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.el-time-range-picker__body{border-radius:2px;border:1px solid #E4E7ED}.el-popover{position:absolute;background:#FFF;min-width:150px;border-radius:4px;border:1px solid #EBEEF5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover,.el-cascader__dropdown,.el-color-picker__panel,.el-message-box,.el-notification{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#FFF;border-radius:4px;border:1px solid #EBEEF5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper::after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus,.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#F56C6C}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409EFF}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__status{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);font-size:24px!important}.el-message-box__status::before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67C23A}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#E6A23C}.el-message-box__status.el-icon-error{color:#F56C6C}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#F56C6C;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#C0C4CC}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#409EFF;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item::after{clear:both}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__content::after{clear:both}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#F56C6C;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:'*';color:#F56C6C;margin-right:4px}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#F56C6C}.el-form-item--feedback .el-input__validateIcon{display:inline-block}.el-tabs__header{padding:0;position:relative;margin:0 0 15px}.el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#409EFF;z-index:1;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;-webkit-transition:all .15s;transition:all .15s}.el-tabs__new-tab .el-icon-plus{-webkit-transform:scale(.8,.8);transform:scale(.8,.8)}.el-tabs__new-tab:hover{color:#409EFF}.el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.el-tabs__nav-wrap::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#E4E7ED;z-index:1}.el-tabs__nav-wrap.is-scrollable{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#909399}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;position:relative;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:left;z-index:2}.el-tabs__nav.is-stretch{min-width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.el-tabs__nav.is-stretch>*{-webkit-box-flex:1;-ms-flex:1;flex:1;text-align:center}.el-tabs__item{padding:0 20px;height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#303133;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:0}.el-tabs__item:focus.is-active.is-focus:not(:active){-webkit-box-shadow:0 0 2px 2px #409EFF inset;box-shadow:0 0 2px 2px #409EFF inset;border-radius:3px}.el-tabs__item .el-icon-close{border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.el-tabs__item .el-icon-close:before{-webkit-transform:scale(.9);transform:scale(.9);display:inline-block}.el-tabs--card>.el-tabs__header .el-tabs__active-bar,.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left,.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs__item .el-icon-close:hover{background-color:#C0C4CC;color:#FFF}.el-tabs__item.is-active{color:#409EFF}.el-tabs__item:hover{color:#409EFF;cursor:pointer}.el-tabs__item.is-disabled{color:#C0C4CC;cursor:default}.el-tabs__content{overflow:hidden;position:relative}.el-tabs--card>.el-tabs__header{border-bottom:1px solid #E4E7ED}.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid #E4E7ED;border-bottom:none;border-radius:4px 4px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #E4E7ED;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .el-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#FFF}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .el-icon-close{width:14px}.el-tabs--border-card{background:#FFF;border:1px solid #DCDFE6;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04);box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:#F5F7FA;border-bottom:1px solid #E4E7ED;margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.el-tabs--border-card>.el-tabs__header .el-tabs__item{-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin-top:-1px;color:#909399}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-col-offset-0,.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#409EFF;background-color:#FFF;border-right-color:#DCDFE6;border-left-color:#DCDFE6}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:#409EFF}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:#C0C4CC}.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),.el-tabs--top .el-tabs__item.is-top:nth-child(2){padding-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:last-child,.el-tabs--bottom .el-tabs__item.is-top:last-child,.el-tabs--top .el-tabs__item.is-bottom:last-child,.el-tabs--top .el-tabs__item.is-top:last-child{padding-right:0}.el-cascader-menu:last-child .el-cascader-node,.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid #DCDFE6}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid transparent}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{top:0;bottom:auto;width:2px;height:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{-webkit-transform:rotateZ(90deg);transform:rotateZ(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{left:auto;top:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{right:auto;bottom:0}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left::after{right:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left::after,.el-tabs--left .el-tabs__nav-wrap.is-right::after,.el-tabs--right .el-tabs__nav-wrap.is-left::after,.el-tabs--right .el-tabs__nav-wrap.is-right::after{height:100%;width:2px;bottom:auto;top:0}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{float:none}.el-tabs--left .el-tabs__item.is-left,.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-right{display:block}.el-tabs--left .el-tabs__header.is-left{float:left;margin-bottom:0;margin-right:10px}.el-button-group>.el-button:not(:last-child),.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid #E4E7ED;border-bottom:none;border-top:1px solid #E4E7ED;text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid #E4E7ED;border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid #E4E7ED;border-right-color:#fff;border-left:none;border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-radius:4px 0 0 4px;border-bottom:1px solid #E4E7ED;border-right:none}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid #dfe4ed}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 transparent}.el-tabs--right .el-tabs__header.is-right{float:right;margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right::after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid #E4E7ED}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid #E4E7ED;border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid #E4E7ED;border-left-color:#fff;border-right:none;border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-radius:0 4px 4px 0;border-bottom:1px solid #E4E7ED;border-left:none}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid #dfe4ed}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 transparent}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{-webkit-animation:slideInRight-enter .3s;animation:slideInRight-enter .3s}.slideInRight-leave{position:absolute;left:0;right:0;-webkit-animation:slideInRight-leave .3s;animation:slideInRight-leave .3s}.slideInLeft-enter{-webkit-animation:slideInLeft-enter .3s;animation:slideInLeft-enter .3s}.slideInLeft-leave{position:absolute;left:0;right:0;-webkit-animation:slideInLeft-leave .3s;animation:slideInLeft-leave .3s}@-webkit-keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@-webkit-keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}.el-tree{position:relative;cursor:default;background:#FFF;color:#606266}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#909399;font-size:14px}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:#409EFF}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node:focus>.el-tree-node__content{background-color:#F5F7FA}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:#409EFF;color:#fff}.el-tree-node__content:hover,.el-upload-list__item:hover{background-color:#F5F7FA}.el-tree-node__content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:#C0C4CC;font-size:12px;-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#C0C4CC}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#f0f7ff}.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#FFF;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#C0C4CC}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#FFF}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67C23A}.el-alert--success.is-light .el-alert__description{color:#67C23A}.el-alert--success.is-dark{background-color:#67C23A;color:#FFF}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#FFF}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#E6A23C}.el-alert--warning.is-light .el-alert__description{color:#E6A23C}.el-alert--warning.is-dark{background-color:#E6A23C;color:#FFF}.el-alert--error.is-light{background-color:#fef0f0;color:#F56C6C}.el-alert--error.is-light .el-alert__description{color:#F56C6C}.el-alert--error.is-dark{background-color:#F56C6C;color:#FFF}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert-fade-enter,.el-alert-fade-leave-active,.el-loading-fade-enter,.el-loading-fade-leave-active,.el-notification-fade-leave-active,.el-upload iframe{opacity:0}.el-carousel__arrow--right,.el-notification.right{right:16px}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #EBEEF5;position:fixed;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67C23A}.el-notification .el-icon-error{color:#F56C6C}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#E6A23C}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.el-input-number .el-input{display:block}.el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.el-input-number__decrease,.el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#F5F7FA;color:#606266;cursor:pointer;font-size:13px}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:#409EFF}.el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#409EFF}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #DCDFE6}.el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #DCDFE6}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:#E4E7ED;color:#E4E7ED}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:#E4E7ED;cursor:not-allowed}.el-input-number--medium{width:200px;line-height:34px}.el-input-number--medium .el-input-number__decrease,.el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.el-input-number--small{width:130px;line-height:30px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.9);transform:scale(.9)}.el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.el-input-number--mini{width:130px;line-height:26px}.el-input-number--mini .el-input-number__decrease,.el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.el-input-number--mini .el-input-number__decrease [class*=el-icon],.el-input-number--mini .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #DCDFE6}.el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #DCDFE6;border-radius:0 0 4px}.el-input-number.is-controls-right[class*=medium] [class*=decrease],.el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.el-input-number.is-controls-right[class*=mini] [class*=decrease],.el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-button-group::after,.el-button-group::before,.el-color-dropdown__main-wrapper::after,.el-link.is-underline:hover:after,.el-page-header__left::after,.el-progress-bar__inner::after,.el-row::after,.el-row::before,.el-slider::after,.el-slider::before,.el-slider__button-wrapper::after,.el-transfer-panel .el-transfer-panel__footer::after,.el-upload-cover::after,.el-upload-list--picture-card .el-upload-list__item-actions::after{content:""}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#FFF}.el-tooltip__popper.is-light{background:#FFF;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#FFF}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#FFF}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#FFF}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#FFF}.el-slider::after,.el-slider::before{display:table}.el-slider__button-wrapper .el-tooltip,.el-slider__button-wrapper::after{display:inline-block;vertical-align:middle}.el-slider::after{clear:both}.el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#E4E7ED;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.el-slider__runway.show-input{margin-right:160px;width:auto}.el-slider__runway.disabled{cursor:default}.el-slider__runway.disabled .el-slider__bar{background-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button{border-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button-wrapper.dragging,.el-slider__runway.disabled .el-slider__button-wrapper.hover,.el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.el-slider__runway.disabled .el-slider__button.dragging,.el-slider__runway.disabled .el-slider__button.hover,.el-slider__runway.disabled .el-slider__button:hover{-webkit-transform:scale(1);transform:scale(1);cursor:not-allowed}.el-slider__button-wrapper,.el-slider__stop{-webkit-transform:translateX(-50%);position:absolute}.el-slider__input{float:right;margin-top:3px;width:130px}.el-slider__input.el-input-number--mini{margin-top:5px}.el-slider__input.el-input-number--medium{margin-top:0}.el-slider__input.el-input-number--large{margin-top:-2px}.el-slider__bar{height:6px;background-color:#409EFF;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.el-slider__button-wrapper{height:36px;width:36px;z-index:1001;top:-15px;transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal}.el-image-viewer__btn,.el-slider__button,.el-step__icon-inner{-moz-user-select:none;-ms-user-select:none}.el-slider__button-wrapper::after{height:100%}.el-slider__button-wrapper.hover,.el-slider__button-wrapper:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button-wrapper.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__button{width:16px;height:16px;border:2px solid #409EFF;background-color:#FFF;border-radius:50%;-webkit-transition:.2s;transition:.2s;-webkit-user-select:none;user-select:none}.el-slider__button.dragging,.el-slider__button.hover,.el-slider__button:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.el-slider__button.hover,.el-slider__button:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__stop{height:6px;width:6px;border-radius:100%;background-color:#FFF;transform:translateX(-50%)}.el-slider__marks{top:0;left:12px;width:18px;height:100%}.el-slider__marks-text{position:absolute;-webkit-transform:translateX(-50%);transform:translateX(-50%);font-size:14px;color:#909399;margin-top:15px}.el-slider.is-vertical{position:relative}.el-slider.is-vertical .el-slider__runway{width:6px;height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:6px;height:auto;border-radius:0 0 3px 3px}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #DCDFE6;line-height:20px;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#C0C4CC}.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#409EFF}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-col-pull-0,.el-col-pull-1,.el-col-pull-10,.el-col-pull-11,.el-col-pull-12,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-2,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-push-0,.el-col-push-1,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-2,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-row,.el-upload-dragger,.el-upload-list__item{position:relative}.el-loading-spinner .el-loading-text{color:#409EFF;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409EFF;stroke-linecap:round}.el-loading-spinner i{color:#409EFF}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-row{-webkit-box-sizing:border-box;box-sizing:border-box}.el-row::after,.el-row::before{display:table}.el-row::after{clear:both}.el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.el-col-0,.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-top{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}[class*=el-col-]{float:left;-webkit-box-sizing:border-box;box-sizing:border-box}.el-col-0{width:0%}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0%}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0%}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0%}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0%}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0%}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-upload{display:inline-block;text-align:center;cursor:pointer;outline:0}.el-upload__input{display:none}.el-upload__tip{font-size:12px;color:#606266;margin-top:7px}.el-upload iframe{position:absolute;z-index:-1;top:0;left:0;filter:alpha(opacity=0)}.el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;cursor:pointer;line-height:146px;vertical-align:top}.el-upload--picture-card i{font-size:28px;color:#8c939d}.el-upload--picture-card:hover,.el-upload:focus{border-color:#409EFF;color:#409EFF}.el-upload:focus .el-upload-dragger{border-color:#409EFF}.el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:360px;height:180px;text-align:center;cursor:pointer;overflow:hidden}.el-upload-dragger .el-icon-upload{font-size:67px;color:#C0C4CC;margin:40px 0 16px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:1px solid #DCDFE6;margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:#606266;font-size:14px;text-align:center}.el-upload-dragger .el-upload__text em{color:#409EFF;font-style:normal}.el-upload-dragger:hover{border-color:#409EFF}.el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #409EFF}.el-upload-list{margin:0;padding:0;list-style:none}.el-upload-list__item{-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#606266;line-height:1.8;margin-top:5px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;width:100%}.el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item:first-child{margin-top:10px}.el-upload-list__item .el-icon-upload-success{color:#67C23A}.el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#606266}.el-upload-list__item .el-icon-close:hover{opacity:1}.el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:5px;font-size:12px;cursor:pointer;opacity:1;color:#409EFF}.el-upload-list__item:hover .el-icon-close{display:inline-block}.el-upload-list__item:hover .el-progress__text{display:none}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.el-upload-list__item.is-success .el-upload-list__item-name:focus,.el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#409EFF;cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon-close-tip{display:inline-block}.el-upload-list__item.is-success:active,.el-upload-list__item.is-success:not(.focusing):focus{outline-width:0}.el-upload-list__item.is-success:active .el-icon-close-tip,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:not(.focusing):focus .el-icon-close-tip{display:none}.el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.el-upload-list__item-name{color:#606266;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;-webkit-transition:color .3s;transition:color .3s;white-space:nowrap}.el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#909399;line-height:inherit}.el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#606266;display:none}.el-upload-list__item-delete:hover{color:#409EFF}.el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.el-upload-list--picture-card .el-upload-list__item .el-icon-check,.el-upload-list--picture-card .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture-card .el-upload-list__item .el-icon-close,.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);-webkit-transition:opacity .3s;transition:opacity .3s}.el-upload-list--picture-card .el-upload-list__item-actions::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-block}.el-upload-list--picture-card .el-progress{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);bottom:auto;width:126px}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{overflow:hidden;z-index:0;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.el-upload-list--picture .el-upload-list__item .el-icon-check,.el-upload-list--picture .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{background:0 0;-webkit-box-shadow:none;box-shadow:none;top:-2px;right:-12px}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name{line-height:70px;margin-top:0}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px;background-color:#FFF}.el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 1px 1px #ccc;box-shadow:0 1px 1px #ccc}.el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.el-upload-cover::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-cover img{display:block;width:100%;height:100%}.el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-cover__label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);color:#fff}.el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.el-upload-cover__interact .btn{display:inline-block;color:#FFF;font-size:14px;cursor:pointer;vertical-align:middle;-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);margin-top:60px}.el-upload-cover__interact .btn span{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{-webkit-transform:translateY(-13px);transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#FFF;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#FFF;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#303133}.el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67C23A}.el-progress.is-success .el-progress__text{color:#67C23A}.el-progress.is-warning .el-progress-bar__inner{background-color:#E6A23C}.el-badge__content,.el-progress.is-exception .el-progress-bar__inner{background-color:#F56C6C}.el-progress.is-warning .el-progress__text{color:#E6A23C}.el-progress.is-exception .el-progress__text{color:#F56C6C}.el-progress-bar{padding-right:50px;display:inline-block;vertical-align:middle;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__header,.el-message,.el-step__icon{-webkit-box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#EBEEF5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#409EFF;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;-webkit-transition:width .6s ease;transition:width .6s ease}.el-progress-bar__inner::after{display:inline-block;height:100%;vertical-align:middle}.el-progress-bar__innerText{display:inline-block;vertical-align:middle;color:#FFF;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-time-spinner{width:100%;white-space:nowrap}.el-spinner{display:inline-block;vertical-align:middle}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.el-message{min-width:380px;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#EBEEF5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,transform .4s,top .4s;transition:opacity .3s,transform .4s,top .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67C23A}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#E6A23C}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#F56C6C}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#C0C4CC;font-size:16px}.el-message__closeBtn:focus{outline-width:0}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67C23A}.el-message .el-icon-error{color:#F56C6C}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#E6A23C}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{border-radius:10px;color:#FFF;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #FFF}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#409EFF}.el-badge__content--success{background-color:#67C23A}.el-badge__content--warning{background-color:#E6A23C}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#F56C6C}.el-card{border-radius:4px;border:1px solid #EBEEF5;background-color:#FFF;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #EBEEF5;box-sizing:border-box}.el-card__body,.el-main{padding:20px}.el-rate{height:20px;line-height:1}.el-rate:active,.el-rate:focus{outline-width:0}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#C0C4CC;-webkit-transition:.3s;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-step.is-vertical,.el-steps{display:-webkit-box;display:-ms-flexbox}.el-rate__text{font-size:14px;vertical-align:middle}.el-steps{display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#F5F7FA}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column}.el-step{position:relative;-ms-flex-negative:1;flex-shrink:1}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{-ms-flex-preferred-size:auto!important;flex-basis:auto!important;-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main{padding-right:0}.el-step__head{position:relative;width:100%}.el-step__head.is-process{color:#303133;border-color:#303133}.el-step__head.is-wait{color:#C0C4CC;border-color:#C0C4CC}.el-step__head.is-success{color:#67C23A;border-color:#67C23A}.el-step__head.is-error{color:#F56C6C;border-color:#F56C6C}.el-step__head.is-finish{color:#409EFF;border-color:#409EFF}.el-step__icon{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:24px;height:24px;font-size:14px;box-sizing:border-box;background:#FFF;-webkit-transition:.15s ease-out;transition:.15s ease-out}.el-step.is-horizontal,.el-step__icon-inner{display:inline-block}.el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{-webkit-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{-webkit-transform:translateY(1px);transform:translateY(1px)}.el-step__line{position:absolute;border-color:inherit;background-color:#C0C4CC}.el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;-webkit-transition:.15s ease-out;transition:.15s ease-out;-webkit-box-sizing:border-box;box-sizing:border-box;width:0;height:0}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{font-weight:700;color:#303133}.el-step__title.is-wait{color:#C0C4CC}.el-step__title.is-success{color:#67C23A}.el-step__title.is-error{color:#F56C6C}.el-step__title.is-finish{color:#409EFF}.el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.el-step__description.is-process{color:#303133}.el-step__description.is-wait{color:#C0C4CC}.el-step__description.is-success{color:#67C23A}.el-step__description.is-error{color:#F56C6C}.el-step__description.is-finish{color:#409EFF}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:flex}.el-step.is-vertical .el-step__head{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{padding-left:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{-webkit-transform:scale(.8) translateY(1px);transform:scale(.8) translateY(1px)}.el-step.is-simple .el-step__main{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.el-step.is-simple .el-step__arrow{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-step.is-simple .el-step__arrow::after,.el-step.is-simple .el-step__arrow::before{content:'';display:inline-block;position:absolute;height:15px;width:1px;background:#C0C4CC}.el-step.is-simple .el-step__arrow::before{-webkit-transform:rotate(-45deg) translateY(-4px);transform:rotate(-45deg) translateY(-4px);-webkit-transform-origin:0 0;transform-origin:0 0}.el-step.is-simple .el-step__arrow::after{-webkit-transform:rotate(45deg) translateY(4px);transform:rotate(45deg) translateY(4px);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-carousel{position:relative}.el-carousel--horizontal{overflow-x:hidden}.el-carousel--vertical{overflow-y:hidden}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#FFF;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;margin:0;padding:0;z-index:2}.el-carousel__indicators--horizontal{bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.el-carousel__indicators--vertical{right:0;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#C0C4CC;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{background-color:transparent;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{display:inline-block;padding:12px 4px}.el-carousel__indicator--vertical{padding:4px 12px}.el-carousel__indicator--vertical .el-carousel__button{width:2px;height:15px}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#FFF;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.el-carousel__item,.el-carousel__mask{height:100%;position:absolute;top:0;left:0}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#FFF;opacity:.24;-webkit-transition:.2s;transition:.2s}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-fade-in-enter,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #EBEEF5;border-bottom:1px solid #EBEEF5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:48px;line-height:48px;background-color:#FFF;color:#303133;cursor:pointer;border-bottom:1px solid #EBEEF5;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409EFF}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#FFF;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:1px solid #EBEEF5}.el-cascader__search-input,.el-cascader__tags,.el-tag{-webkit-box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-cascader,.el-tag{display:inline-block}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#EBEEF5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#FFF;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#EBEEF5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#FFF}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#EBEEF5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#FFF;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#EBEEF5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#FFF}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.el-cascader{position:relative;font-size:14px;line-height:40px}.el-cascader:not(.is-disabled):hover .el-input__inner{cursor:pointer;border-color:#C0C4CC}.el-cascader .el-input .el-input__inner:focus,.el-cascader .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-cascader .el-input{cursor:pointer}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-icon-arrow-down{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:14px}.el-cascader .el-input .el-icon-arrow-down.is-reverse{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-cascader .el-input .el-icon-circle-close:hover{color:#909399}.el-cascader--medium{font-size:14px;line-height:36px}.el-cascader--small{font-size:13px;line-height:32px}.el-cascader--mini{font-size:12px;line-height:28px}.el-cascader.is-disabled .el-cascader__label{z-index:2;color:#C0C4CC}.el-cascader__dropdown{margin:5px 0;font-size:14px;background:#FFF;border:1px solid #E4E7ED;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-cascader__tags{position:absolute;left:0;right:30px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:normal;text-align:left;box-sizing:border-box}.el-cascader__tags .el-tag{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;max-width:100%;margin:2px 0 2px 6px;text-overflow:ellipsis;background:#f0f2f5}.el-cascader__tags .el-tag:not(.is-hit){border-color:transparent}.el-cascader__tags .el-tag>span{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis}.el-cascader__tags .el-tag .el-icon-close{-webkit-box-flex:0;-ms-flex:none;flex:none;background-color:#C0C4CC;color:#FFF}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:#909399}.el-cascader__suggestion-panel{border-radius:4px}.el-cascader__suggestion-list{max-height:204px;margin:0;padding:6px 0;font-size:14px;color:#606266;text-align:center}.el-cascader__suggestion-item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;padding:0 15px;text-align:left;outline:0;cursor:pointer}.el-cascader__suggestion-item:focus,.el-cascader__suggestion-item:hover{background:#F5F7FA}.el-cascader__suggestion-item.is-checked{color:#409EFF;font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{margin:10px 0;color:#C0C4CC}.el-cascader__search-input{-webkit-box-flex:1;-ms-flex:1;flex:1;height:24px;min-width:60px;margin:2px 0 2px 15px;padding:0;color:#606266;border:none;outline:0;box-sizing:border-box}.el-cascader__search-input::-webkit-input-placeholder{color:#C0C4CC}.el-cascader__search-input:-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::placeholder{color:#C0C4CC}.el-color-predefine{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:12px;margin-top:8px;width:280px}.el-color-predefine__colors{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-color-predefine__color-selector{margin:0 0 8px 8px;width:20px;height:20px;border-radius:4px;cursor:pointer}.el-color-predefine__color-selector:nth-child(10n+1){margin-left:0}.el-color-predefine__color-selector.selected{-webkit-box-shadow:0 0 3px 2px #409EFF;box-shadow:0 0 3px 2px #409EFF}.el-color-predefine__color-selector>div{display:-webkit-box;display:-ms-flexbox;display:flex;height:100%;border-radius:3px}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.el-color-hue-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);height:100%}.el-color-hue-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to bottom,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-svpanel{position:relative;width:280px;height:180px}.el-color-svpanel__black,.el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.el-color-svpanel__white{background:-webkit-gradient(linear,left top,right top,from(#fff),to(rgba(255,255,255,0)));background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.el-color-svpanel__black{background:-webkit-gradient(linear,left bottom,left top,from(#000),to(rgba(0,0,0,0)));background:linear-gradient(to top,#000,rgba(0,0,0,0))}.el-color-svpanel__cursor{position:absolute}.el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);transform:translate(-2px,-2px)}.el-color-alpha-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-alpha-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);height:100%}.el-color-alpha-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to bottom,rgba(255,255,255,0) 0,#fff 100%)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-dropdown{width:300px}.el-color-dropdown__main-wrapper{margin-bottom:6px}.el-color-dropdown__main-wrapper::after{display:table;clear:both}.el-color-dropdown__btns{margin-top:6px;text-align:right}.el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.el-color-dropdown__btn:hover{color:#409EFF;border-color:#409EFF}.el-color-dropdown__link-btn{cursor:pointer;color:#409EFF;text-decoration:none;padding:15px;font-size:12px}.el-color-dropdown__link-btn:hover{color:tint(#409EFF,20%)}.el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.el-color-picker--medium{height:36px}.el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.el-color-picker--small{height:32px}.el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.el-color-picker--small .el-color-picker__empty,.el-color-picker--small .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker--mini{height:28px}.el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.el-color-picker--mini .el-color-picker__empty,.el-color-picker--mini .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:rgba(255,255,255,.7)}.el-color-picker__trigger{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.el-color-picker__color{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.el-color-picker__icon,.el-input,.el-textarea{display:inline-block;width:100%}.el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.el-color-picker__empty{font-size:12px;color:#999;position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.el-color-picker__icon{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);color:#FFF;text-align:center;font-size:12px}.el-input__prefix,.el-input__suffix{position:absolute;top:0;text-align:center}.el-color-picker__panel{position:absolute;z-index:10;padding:6px;-webkit-box-sizing:content-box;box-sizing:content-box;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-input__inner,.el-textarea__inner,.el-transfer-panel{-webkit-box-sizing:border-box}.el-textarea{position:relative;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#909399;font-size:12px}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input,.el-input__inner{font-size:inherit}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;height:40px;line-height:40px;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__inner::-ms-reveal{display:none}.el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{height:100%;right:5px;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{height:100%;left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-image-viewer__btn,.el-image__preview,.el-link,.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-transfer{font-size:14px}.el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#FFF;background-color:#409EFF;font-size:0}.el-button-group>.el-button+.el-button,.el-transfer-panel__item+.el-transfer-panel__item,.el-transfer__button [class*=el-icon-]+span{margin-left:0}.el-divider__text,.el-image__error,.el-link,.el-timeline,.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer__button.is-with-texts{border-radius:4px}.el-transfer__button.is-disabled,.el-transfer__button.is-disabled:hover{border:1px solid #DCDFE6;background-color:#F5F7FA;color:#C0C4CC}.el-transfer__button:first-child{margin-bottom:10px}.el-transfer__button:nth-child(2){margin:0}.el-transfer-panel{border:1px solid #EBEEF5;border-radius:4px;overflow:hidden;background:#FFF;display:inline-block;vertical-align:middle;width:200px;max-height:100%;box-sizing:border-box;position:relative}.el-transfer-panel__body{height:246px}.el-transfer-panel__body.is-with-footer{padding-bottom:40px}.el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block!important}.el-transfer-panel__item.el-checkbox{color:#606266}.el-transfer-panel__item:hover{color:#409EFF}.el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;padding-left:24px;line-height:30px}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;margin:15px;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;width:auto}.el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.el-transfer-panel__filter .el-input__icon{margin-left:5px}.el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#F5F7FA;margin:0;padding-left:15px;border-bottom:1px solid #EBEEF5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#000}.el-container,.el-header{-webkit-box-sizing:border-box}.el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#303133;font-weight:400}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#909399;font-size:12px;font-weight:400}.el-transfer-panel .el-transfer-panel__footer{height:40px;background:#FFF;margin:0;padding:0;border-top:1px solid #EBEEF5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.el-transfer-panel .el-transfer-panel__footer::after{display:inline-block;height:100%;vertical-align:middle}.el-container,.el-timeline-item__node{display:-webkit-box;display:-ms-flexbox}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#606266}.el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#909399;text-align:center}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.el-transfer-panel .el-checkbox__inner::after{height:6px;width:3px;left:4px}.el-container{display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;box-sizing:border-box;min-width:0}.el-container.is-vertical,.el-drawer,.el-empty,.el-result{-webkit-box-orient:vertical;-webkit-box-direction:normal}.el-container.is-vertical{-ms-flex-direction:column;flex-direction:column}.el-header{padding:0 20px;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-main{display:block;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-timeline{margin:0;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline-item{position:relative;padding-bottom:20px}.el-timeline-item__wrapper{position:relative;padding-left:28px;top:-3px}.el-timeline-item__tail{position:absolute;left:4px;height:100%;border-left:2px solid #E4E7ED}.el-timeline-item__icon{color:#FFF;font-size:13px}.el-timeline-item__node{position:absolute;background-color:#E4E7ED;border-radius:50%;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image__error,.el-timeline-item__dot{display:-webkit-box;display:-ms-flexbox}.el-timeline-item__node--normal{left:-1px;width:12px;height:12px}.el-timeline-item__node--large{left:-2px;width:14px;height:14px}.el-timeline-item__node--primary{background-color:#409EFF}.el-timeline-item__node--success{background-color:#67C23A}.el-timeline-item__node--warning{background-color:#E6A23C}.el-timeline-item__node--danger{background-color:#F56C6C}.el-timeline-item__node--info{background-color:#909399}.el-timeline-item__dot{position:absolute;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-timeline-item__content{color:#303133}.el-timeline-item__timestamp{color:#909399;line-height:1;font-size:13px}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-link{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;vertical-align:middle;position:relative;text-decoration:none;outline:0;padding:0;font-weight:500}.el-link.is-underline:hover:after{position:absolute;left:0;right:0;height:0;bottom:0;border-bottom:1px solid #409EFF}.el-link.el-link--default:after,.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:#409EFF}.el-link.is-disabled{cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default{color:#606266}.el-link.el-link--default:hover{color:#409EFF}.el-link.el-link--default.is-disabled{color:#C0C4CC}.el-link.el-link--primary{color:#409EFF}.el-link.el-link--primary:hover{color:#66b1ff}.el-link.el-link--primary.is-disabled{color:#a0cfff}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:#F56C6C}.el-link.el-link--danger{color:#F56C6C}.el-link.el-link--danger:hover{color:#f78989}.el-link.el-link--danger.is-disabled{color:#fab6b6}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:#67C23A}.el-link.el-link--success{color:#67C23A}.el-link.el-link--success:hover{color:#85ce61}.el-link.el-link--success.is-disabled{color:#b3e19d}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:#E6A23C}.el-link.el-link--warning{color:#E6A23C}.el-link.el-link--warning:hover{color:#ebb563}.el-link.el-link--warning.is-disabled{color:#f3d19e}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:#909399}.el-link.el-link--info{color:#909399}.el-link.el-link--info:hover{color:#a6a9ad}.el-link.el-link--info.is-disabled{color:#c8c9cc}.el-divider{background-color:#DCDFE6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#FFF;padding:0 20px;font-weight:500;color:#303133}.el-image__error,.el-image__placeholder{background:#F5F7FA}.el-divider__text.is-left{left:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-divider__text.is-center{left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-image__error,.el-image__inner,.el-image__placeholder{width:100%;height:100%}.el-image{position:relative;display:inline-block;overflow:hidden}.el-image__inner{vertical-align:top}.el-image__inner--center{position:relative;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:block}.el-image__error{display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#C0C4CC;vertical-align:middle}.el-image-viewer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.el-image-viewer__btn{position:absolute;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:50%;opacity:.8;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;user-select:none}.el-button,.el-checkbox,.el-checkbox-button__inner,.el-empty__image img,.el-radio{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-image-viewer__close{top:40px;right:40px;width:40px;height:40px;font-size:24px;color:#fff;background-color:#606266}.el-image-viewer__canvas{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image-viewer__actions{left:50%;bottom:30px;-webkit-transform:translateX(-50%);transform:translateX(-50%);width:282px;height:44px;padding:0 23px;background-color:#606266;border-color:#fff;border-radius:22px}.el-image-viewer__actions__inner{width:100%;height:100%;text-align:justify;cursor:default;font-size:23px;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.el-image-viewer__next,.el-image-viewer__prev{width:44px;height:44px;font-size:24px;color:#fff;background-color:#606266;border-color:#fff;top:50%}.el-image-viewer__prev{-webkit-transform:translateY(-50%);transform:translateY(-50%);left:40px}.el-image-viewer__next{-webkit-transform:translateY(-50%);transform:translateY(-50%);right:40px;text-indent:2px}.el-image-viewer__mask{position:absolute;width:100%;height:100%;top:0;left:0;opacity:.5;background:#000}.viewer-fade-enter-active{-webkit-animation:viewer-fade-in .3s;animation:viewer-fade-in .3s}.viewer-fade-leave-active{-webkit-animation:viewer-fade-out .3s;animation:viewer-fade-out .3s}@-webkit-keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button,.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini{font-size:12px;border-radius:3px}.el-button--mini.is-circle{padding:7px}.el-button--text{border-color:transparent;color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;border-color:transparent;background-color:transparent}.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover{border-color:transparent}.el-button-group .el-button--danger:last-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:last-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:last-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:last-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:last-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child),.el-button-group>.el-dropdown>.el-button{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:first-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:first-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:first-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:first-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-right-color:rgba(255,255,255,.5)}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button.is-disabled{z-index:1}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button.is-active,.el-button-group>.el-button:not(.is-disabled):active,.el-button-group>.el-button:not(.is-disabled):focus,.el-button-group>.el-button:not(.is-disabled):hover{z-index:1}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0}.el-calendar{background-color:#fff}.el-calendar__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #EBEEF5}.el-backtop,.el-page-header{display:-webkit-box;display:-ms-flexbox}.el-calendar__title{color:#000;-ms-flex-item-align:center;align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{padding:12px 0;color:#606266;font-weight:400}.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{color:#C0C4CC}.el-backtop,.el-calendar-table td.is-today{color:#409EFF}.el-calendar-table td{border-bottom:1px solid #EBEEF5;border-right:1px solid #EBEEF5;vertical-align:top;-webkit-transition:background-color .2s ease;transition:background-color .2s ease}.el-calendar-table td.is-selected{background-color:#F2F8FE}.el-calendar-table tr:first-child td{border-top:1px solid #EBEEF5}.el-calendar-table tr td:first-child{border-left:1px solid #EBEEF5}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{-webkit-box-sizing:border-box;box-sizing:border-box;padding:8px;height:85px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:#F2F8FE}.el-backtop{position:fixed;background-color:#FFF;width:40px;height:40px;border-radius:50%;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;font-size:20px;-webkit-box-shadow:0 0 6px rgba(0,0,0,.12);box-shadow:0 0 6px rgba(0,0,0,.12);cursor:pointer;z-index:5}.el-backtop:hover{background-color:#F2F6FC}.el-page-header{display:flex;line-height:24px}.el-page-header__left{display:-webkit-box;display:-ms-flexbox;display:flex;cursor:pointer;margin-right:40px;position:relative}.el-page-header__left::after{position:absolute;width:1px;height:16px;right:-20px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);background-color:#DCDFE6}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-page-header__left .el-icon-back{font-size:18px;margin-right:6px;-ms-flex-item-align:center;align-self:center}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{font-size:18px;color:#303133}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#FFF;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s ease-in .05s;transition:-webkit-transform .15s ease-in .05s;transition:transform .15s ease-in .05s;transition:transform .15s ease-in .05s,-webkit-transform .15s ease-in .05s;-webkit-transform-origin:center;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{display:inline-block;position:relative}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #8cc5ff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-avatar,.el-cascader-panel,.el-radio,.el-radio--medium.is-bordered .el-radio__label,.el-radio__label{font-size:14px}.el-radio{color:#606266;font-weight:500;line-height:1;cursor:pointer;white-space:nowrap;outline:0;margin-right:30px}.el-cascader-node>.el-radio,.el-radio:last-child{margin-right:0}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px}.el-cascader-menu,.el-cascader-menu__list,.el-radio__inner{-webkit-box-sizing:border-box}.el-radio.is-bordered.is-checked{border-color:#409EFF}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#EBEEF5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#F5F7FA;border-color:#E4E7ED}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio__input{white-space:nowrap;cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#F5F7FA}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#C0C4CC}.el-radio__input.is-disabled+span.el-radio__label{color:#C0C4CC;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#409EFF;background:#409EFF}.el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#409EFF}.el-radio__input.is-focus .el-radio__inner{border-color:#409EFF}.el-radio__inner{border:1px solid #DCDFE6;border-radius:100%;width:14px;height:14px;background-color:#FFF;cursor:pointer;box-sizing:border-box}.el-radio__inner:hover{border-color:#409EFF}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#FFF;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-radio__label{padding-left:10px}.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-cascader-panel{display:-webkit-box;display:-ms-flexbox;display:flex;border-radius:4px}.el-cascader-panel.is-bordered{border:1px solid #E4E7ED;border-radius:4px}.el-cascader-menu{min-width:180px;box-sizing:border-box;color:#606266;border-right:solid 1px #E4E7ED}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu__wrap{height:204px}.el-cascader-menu__list{position:relative;min-height:100%;margin:0;padding:6px 0;list-style:none;box-sizing:border-box}.el-cascader-menu__hover-zone{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.el-cascader-menu__empty-text{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-align:center;color:#C0C4CC}.el-cascader-node{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0 30px 0 20px;height:34px;line-height:34px;outline:0}.el-cascader-node.is-selectable.in-active-path{color:#606266}.el-cascader-node.in-active-path,.el-cascader-node.is-active,.el-cascader-node.is-selectable.in-checked-path{color:#409EFF;font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):focus,.el-cascader-node:not(.is-disabled):hover{background:#F5F7FA}.el-cascader-node.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{-webkit-box-flex:1;-ms-flex:1;flex:1;padding:0 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-avatar{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden;color:#fff;background:#C0C4CC;width:40px;height:40px;line-height:40px}.el-drawer,.el-drawer__body>*{-webkit-box-sizing:border-box}.el-avatar>img{display:block;height:100%;vertical-align:middle}.el-empty__image img,.el-empty__image svg{vertical-align:top;height:100%;width:100%}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:4px}.el-avatar--icon{font-size:18px}.el-avatar--large{width:40px;height:40px;line-height:40px}.el-avatar--medium{width:36px;height:36px;line-height:36px}.el-avatar--small{width:28px;height:28px;line-height:28px}@-webkit-keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@-webkit-keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@-webkit-keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@-webkit-keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}@keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}.el-drawer{position:absolute;box-sizing:border-box;background-color:#FFF;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);overflow:hidden;outline:0}.el-drawer.rtl{-webkit-animation:rtl-drawer-out .3s;animation:rtl-drawer-out .3s;right:0}.el-drawer__open .el-drawer.rtl{-webkit-animation:rtl-drawer-in .3s 1ms;animation:rtl-drawer-in .3s 1ms}.el-drawer.ltr{-webkit-animation:ltr-drawer-out .3s;animation:ltr-drawer-out .3s;left:0}.el-drawer__open .el-drawer.ltr{-webkit-animation:ltr-drawer-in .3s 1ms;animation:ltr-drawer-in .3s 1ms}.el-drawer.ttb{-webkit-animation:ttb-drawer-out .3s;animation:ttb-drawer-out .3s;top:0}.el-drawer__open .el-drawer.ttb{-webkit-animation:ttb-drawer-in .3s 1ms;animation:ttb-drawer-in .3s 1ms}.el-drawer.btt{-webkit-animation:btt-drawer-out .3s;animation:btt-drawer-out .3s;bottom:0}.el-drawer__open .el-drawer.btt{-webkit-animation:btt-drawer-in .3s 1ms;animation:btt-drawer-in .3s 1ms}.el-drawer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;margin:0}.el-drawer__header{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#72767b;display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:32px;padding:20px 20px 0}.el-drawer__header>:first-child{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-drawer__title{margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;line-height:inherit;font-size:1rem}.el-drawer__close-btn{border:none;cursor:pointer;font-size:20px;color:inherit;background-color:transparent}.el-drawer__body{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto}.el-drawer__body>*{box-sizing:border-box}.el-drawer.ltr,.el-drawer.rtl{height:100%;top:0;bottom:0}.el-drawer.btt,.el-drawer.ttb{width:100%;left:0;right:0}.el-drawer__container{position:relative;left:0;right:0;top:0;bottom:0;height:100%;width:100%}.el-drawer-fade-enter-active{-webkit-animation:el-drawer-fade-in .3s;animation:el-drawer-fade-in .3s}.el-drawer-fade-leave-active{animation:el-drawer-fade-in .3s reverse}.el-statistic{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:#000;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum";text-align:center}.el-statistic .head{margin-bottom:4px;color:#606266;font-size:13px}.el-statistic .con{font-family:Sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#303133}.el-statistic .con .number{font-size:20px;padding:0 4px}.el-statistic .con span{display:inline-block;margin:0;line-height:100%}.el-popconfirm__main,.el-skeleton__image{display:-ms-flexbox;-webkit-box-align:center;display:-webkit-box}.el-popconfirm__main{display:flex;-ms-flex-align:center;align-items:center}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin:0}@-webkit-keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}@keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}.el-skeleton{width:100%}.el-skeleton__first-line,.el-skeleton__paragraph{height:16px;margin-top:16px;background:#f2f2f2}.el-skeleton.is-animated .el-skeleton__item{background:-webkit-gradient(linear,left top,right top,color-stop(25%,#f2f2f2),color-stop(37%,#e6e6e6),color-stop(63%,#f2f2f2));background:linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%);background-size:400% 100%;-webkit-animation:el-skeleton-loading 1.4s ease infinite;animation:el-skeleton-loading 1.4s ease infinite}.el-skeleton__item{background:#f2f2f2;display:inline-block;height:16px;border-radius:4px;width:100%}.el-skeleton__circle{border-radius:50%;width:36px;height:36px;line-height:36px}.el-skeleton__circle--lg{width:40px;height:40px;line-height:40px}.el-skeleton__circle--md{width:28px;height:28px;line-height:28px}.el-skeleton__button{height:40px;width:64px;border-radius:4px}.el-skeleton__p{width:100%}.el-skeleton__p.is-last{width:61%}.el-skeleton__p.is-first{width:33%}.el-skeleton__text{width:100%;height:13px}.el-skeleton__caption{height:12px}.el-skeleton__h1{height:20px}.el-skeleton__h3{height:18px}.el-skeleton__h5{height:16px}.el-skeleton__image{width:unset;display:flex;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:0}.el-skeleton__image svg{fill:#DCDDE0;width:22%;height:22%}.el-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 0}.el-empty__image{width:160px}.el-empty__image img{user-select:none;-o-object-fit:contain;object-fit:contain}.el-empty__image svg{fill:#DCDDE0}.el-empty__description{margin-top:20px}.el-empty__description p{margin:0;font-size:14px;color:#909399}.el-empty__bottom,.el-result__title{margin-top:20px}.el-descriptions{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;color:#303133}.el-descriptions__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:20px}.el-descriptions__title{font-size:16px;font-weight:700}.el-descriptions--mini,.el-descriptions--small{font-size:12px}.el-descriptions__body{color:#606266;background-color:#FFF}.el-descriptions__body .el-descriptions__table{border-collapse:collapse;width:100%;table-layout:fixed}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:left;font-weight:400;line-height:1.5}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-left{text-align:left}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-center{text-align:center}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-right{text-align:right}.el-descriptions .is-bordered{table-layout:auto}.el-descriptions .is-bordered .el-descriptions-item__cell{border:1px solid #EBEEF5;padding:12px 10px}.el-descriptions :not(.is-bordered) .el-descriptions-item__cell{padding-bottom:12px}.el-descriptions--medium.is-bordered .el-descriptions-item__cell{padding:10px}.el-descriptions--medium:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:10px}.el-descriptions--small.is-bordered .el-descriptions-item__cell{padding:8px 10px}.el-descriptions--small:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:8px}.el-descriptions--mini.is-bordered .el-descriptions-item__cell{padding:6px 10px}.el-descriptions--mini:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:6px}.el-descriptions-item{vertical-align:top}.el-descriptions-item__container{display:-webkit-box;display:-ms-flexbox;display:flex}.el-descriptions-item__container .el-descriptions-item__content,.el-descriptions-item__container .el-descriptions-item__label{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.el-descriptions-item__container .el-descriptions-item__content{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-descriptions-item__label.has-colon::after{content:':';position:relative;top:-.5px}.el-descriptions-item__label.is-bordered-label{font-weight:700;color:#909399;background:#fafafa}.el-descriptions-item__label:not(.is-bordered-label){margin-right:10px}.el-descriptions-item__content{word-break:break-word;overflow-wrap:break-word}.el-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 30px}.el-result__icon svg{width:64px;height:64px}.el-result__title p{margin:0;font-size:20px;color:#303133;line-height:1.3}.el-result__subtitle{margin-top:10px}.el-result__subtitle p{margin:0;font-size:14px;color:#606266;line-height:1.3}.el-result__extra{margin-top:30px }.el-result .icon-success{fill:#67C23A}.el-result .icon-error{fill:#F56C6C}.el-result .icon-info{fill:#909399}.el-result .icon-warning{fill:#E6A23C} \ No newline at end of file diff --git a/ruoyi-ui/src/App.vue b/ruoyi-ui/src/App.vue new file mode 100644 index 0000000..12df51d --- /dev/null +++ b/ruoyi-ui/src/App.vue @@ -0,0 +1,20 @@ + + + + diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js new file mode 100644 index 0000000..7b7388f --- /dev/null +++ b/ruoyi-ui/src/api/login.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + return request({ + url: '/login', + headers: { + isToken: false, + repeatSubmit: false + }, + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/menu.js b/ruoyi-ui/src/api/menu.js new file mode 100644 index 0000000..faef101 --- /dev/null +++ b/ruoyi-ui/src/api/menu.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取路由 +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/account.js b/ruoyi-ui/src/api/monitor/account.js new file mode 100644 index 0000000..ca340f5 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/account.js @@ -0,0 +1,62 @@ +import request from '@/utils/request' + +// 查询客户端账号列表 +export function listAccount(query) { + return request({ + url: '/monitor/account/list', + method: 'get', + params: query + }) +} + +// 查询客户端账号详细 +export function getAccount(id) { + return request({ + url: '/monitor/account/' + id, + method: 'get' + }) +} + +// 新增客户端账号 +export function addAccount(data) { + return request({ + url: '/monitor/account', + method: 'post', + data: data + }) +} + +// 修改客户端账号 +export function updateAccount(data) { + return request({ + url: '/monitor/account/update', + method: 'post', + data: data + }) +} + +// 删除客户端账号 +export function delAccount(id) { + return request({ + url: '/monitor/account/' + id, + method: 'delete' + }) +} + +// 注册客户端账号 +export function registerAccount(data) { + return request({ + url: '/monitor/account/register', + method: 'post', + data: data + }) +} + +// 验证用户名是否可用 +export function checkUsername(username) { + return request({ + url: '/monitor/account/check-username', + method: 'get', + params: { username } + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/alibaba1688.js b/ruoyi-ui/src/api/monitor/alibaba1688.js new file mode 100644 index 0000000..26a07d3 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/alibaba1688.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 查询1688爬取风控监控数据列表 +export function listAlibaba1688Monitor(query) { + return request({ + url: '/monitor/client/alibaba1688/list', + method: 'get', + params: query + }) +} + +// 查询1688爬取风控监控统计数据 +export function getAlibaba1688Statistics() { + return request({ + url: '/monitor/client/alibaba1688/statistics', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/cache.js b/ruoyi-ui/src/api/monitor/cache.js new file mode 100644 index 0000000..72c5f6a --- /dev/null +++ b/ruoyi-ui/src/api/monitor/cache.js @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +// 查询缓存详细 +export function getCache() { + return request({ + url: '/monitor/cache', + method: 'get' + }) +} + +// 查询缓存名称列表 +export function listCacheName() { + return request({ + url: '/monitor/cache/getNames', + method: 'get' + }) +} + +// 查询缓存键名列表 +export function listCacheKey(cacheName) { + return request({ + url: '/monitor/cache/getKeys/' + cacheName, + method: 'get' + }) +} + +// 查询缓存内容 +export function getCacheValue(cacheName, cacheKey) { + return request({ + url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey, + method: 'get' + }) +} + +// 清理指定名称缓存 +export function clearCacheName(cacheName) { + return request({ + url: '/monitor/cache/clearCacheName/' + cacheName, + method: 'delete' + }) +} + +// 清理指定键名缓存 +export function clearCacheKey(cacheKey) { + return request({ + url: '/monitor/cache/clearCacheKey/' + cacheKey, + method: 'delete' + }) +} + +// 清理全部缓存 +export function clearCacheAll() { + return request({ + url: '/monitor/cache/clearCacheAll', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/client.js b/ruoyi-ui/src/api/monitor/client.js new file mode 100644 index 0000000..10ecd91 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/client.js @@ -0,0 +1,130 @@ +import request from '@/utils/request' + +// 查询客户端信息列表 +export function listInfo(query) { + return request({ + url: '/monitor/client/info/list', + method: 'get', + params: query + }) +} + +// 查询客户端错误报告列表 +export function listError(query) { + return request({ + url: '/monitor/client/error/list', + method: 'get', + params: query + }) +} + +// 查询客户端事件日志列表 +export function listEventLog(query) { + return request({ + url: '/monitor/client/eventlog/list', + method: 'get', + params: query + }) +} + +// 查询客户端数据采集报告列表 +export function listData(query) { + return request({ + url: '/monitor/client/data/list', + method: 'get', + params: query + }) +} + +// 导出客户端信息 +export function exportInfo(query) { + return request({ + url: '/monitor/client/info/export', + method: 'get', + params: query + }) +} + +// 导出客户端错误报告 +export function exportError(query) { + return request({ + url: '/monitor/client/error/export', + method: 'get', + params: query + }) +} + +// 获取客户端统计信息 +export function getClientStatistics() { + return request({ + url: '/monitor/client/statistics', + method: 'get' + }) +} + +// 获取在线客户端数量 +export function getOnlineClientCount() { + return request({ + url: '/monitor/client/online/count', + method: 'get' + }) +} + +// 获取客户端活跃趋势 +export function getClientActiveTrend() { + return request({ + url: '/monitor/client/active/trend', + method: 'get' + }) +} + +// 获取数据类型分布 +export function getDataTypeDistribution() { + return request({ + url: '/monitor/client/data/distribution', + method: 'get' + }) +} + +// 获取客户端详细信息 +export function getClientDetail(clientId) { + return request({ + url: `/monitor/client/detail/${clientId}`, + method: 'get' + }) +} + +// 获取客户端日志内容 +export function getClientLogs(clientId) { + return request({ + url: `/monitor/client/logs/${clientId}`, + method: 'get' + }) +} + +// 下载客户端日志文件 +export function downloadClientLogs(clientId) { + return request({ + url: `/monitor/client/logs/${clientId}/download`, + method: 'get', + responseType: 'blob' + }) +} + +// 客户端日志上传 +export function uploadClientLogs(logData) { + return request({ + url: '/monitor/client/logs/upload', + method: 'post', + data: logData + }) +} + +// 客户端日志批量上传 +export function batchUploadClientLogs(batchLogData) { + return request({ + url: '/monitor/client/logs/batchUpload', + method: 'post', + data: batchLogData + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/job.js b/ruoyi-ui/src/api/monitor/job.js new file mode 100644 index 0000000..3815569 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/job.js @@ -0,0 +1,71 @@ +import request from '@/utils/request' + +// 查询定时任务调度列表 +export function listJob(query) { + return request({ + url: '/monitor/job/list', + method: 'get', + params: query + }) +} + +// 查询定时任务调度详细 +export function getJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'get' + }) +} + +// 新增定时任务调度 +export function addJob(data) { + return request({ + url: '/monitor/job', + method: 'post', + data: data + }) +} + +// 修改定时任务调度 +export function updateJob(data) { + return request({ + url: '/monitor/job', + method: 'put', + data: data + }) +} + +// 删除定时任务调度 +export function delJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'delete' + }) +} + +// 任务状态修改 +export function changeJobStatus(jobId, status) { + const data = { + jobId, + status + } + return request({ + url: '/monitor/job/changeStatus', + method: 'put', + data: data + }) +} + + +// 定时任务立即执行一次 +export function runJob(jobId, jobGroup) { + const data = { + jobId, + jobGroup + } + return request({ + url: '/monitor/job/run', + method: 'put', + data: data + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/jobLog.js b/ruoyi-ui/src/api/monitor/jobLog.js new file mode 100644 index 0000000..6e0be61 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/jobLog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询调度日志列表 +export function listJobLog(query) { + return request({ + url: '/monitor/jobLog/list', + method: 'get', + params: query + }) +} + +// 删除调度日志 +export function delJobLog(jobLogId) { + return request({ + url: '/monitor/jobLog/' + jobLogId, + method: 'delete' + }) +} + +// 清空调度日志 +export function cleanJobLog() { + return request({ + url: '/monitor/jobLog/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/key.js b/ruoyi-ui/src/api/monitor/key.js new file mode 100644 index 0000000..1d600a6 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/key.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询密钥列表 +export function listKey(query) { + return request({ + url: '/monitor/key/list', + method: 'get', + params: query + }) +} + +// 查询密钥详细 +export function getKey(id) { + return request({ + url: '/monitor/key/' + id, + method: 'get' + }) +} + +// 新增密钥 +export function addKey(data) { + return request({ + url: '/monitor/key', + method: 'post', + data: data + }) +} + +// 修改密钥 +export function updateKey(data) { + return request({ + url: '/monitor/key/update', + method: 'post', + data: data + }) +} + +// 删除密钥 +export function delKey(id) { + return request({ + url: '/monitor/key/' + id, + method: 'delete' + }) +} + +// 生成随机密钥 +export function generateKey() { + return request({ + url: '/monitor/key/generate', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/logininfor.js b/ruoyi-ui/src/api/monitor/logininfor.js new file mode 100644 index 0000000..4d112b7 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/logininfor.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 查询登录日志列表 +export function list(query) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query + }) +} + +// 删除登录日志 +export function delLogininfor(infoId) { + return request({ + url: '/monitor/logininfor/' + infoId, + method: 'delete' + }) +} + +// 解锁用户登录状态 +export function unlockLogininfor(userName) { + return request({ + url: '/monitor/logininfor/unlock/' + userName, + method: 'get' + }) +} + +// 清空登录日志 +export function cleanLogininfor() { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/online.js b/ruoyi-ui/src/api/monitor/online.js new file mode 100644 index 0000000..bd22137 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/online.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 查询在线用户列表 +export function list(query) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query + }) +} + +// 强退用户 +export function forceLogout(tokenId) { + return request({ + url: '/monitor/online/' + tokenId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/operlog.js b/ruoyi-ui/src/api/monitor/operlog.js new file mode 100644 index 0000000..a04bca8 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/operlog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list(query) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperlog(operId) { + return request({ + url: '/monitor/operlog/' + operId, + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperlog() { + return request({ + url: '/monitor/operlog/clean', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/monitor/server.js b/ruoyi-ui/src/api/monitor/server.js new file mode 100644 index 0000000..e1f9ca2 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/server.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取服务信息 +export function getServer() { + return request({ + url: '/monitor/server', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/monitor/version.js b/ruoyi-ui/src/api/monitor/version.js new file mode 100644 index 0000000..d1f1238 --- /dev/null +++ b/ruoyi-ui/src/api/monitor/version.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询版本信息 +export function getVersionInfo() { + return request({ + url: '/system/version/info', + method: 'get' + }) +} + +// 检查版本更新 +export function checkVersion(currentVersion) { + return request({ + url: '/system/version/check', + method: 'get', + params: { + currentVersion: currentVersion + } + }) +} + +// 上传文件到文件服务 +export function uploadFile(data) { + return request({ + url: '/file/uploads', + method: 'post', + data: data, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 更新版本信息和下载链接 +export function updateVersion(data) { + return request({ + url: '/system/version/update', + method: 'post', + params: data + }) +} + + + diff --git a/ruoyi-ui/src/api/prod/ozon.js b/ruoyi-ui/src/api/prod/ozon.js new file mode 100644 index 0000000..075e32f --- /dev/null +++ b/ruoyi-ui/src/api/prod/ozon.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 爬取图片 +export function scrapeImages() { + return request({ + url: '/prod/ozon/scrapeImages', + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/prod/products.js b/ruoyi-ui/src/api/prod/products.js new file mode 100644 index 0000000..6c0d4d3 --- /dev/null +++ b/ruoyi-ui/src/api/prod/products.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询prod列表 +export function listProducts(query) { + return request({ + url: '/prod/products/list', + method: 'get', + params: query + }) +} + +// 查询prod详细 +export function getProducts(productId) { + return request({ + url: '/prod/products/' + productId, + method: 'get' + }) +} + +// 新增prod +export function addProducts(data) { + return request({ + url: '/prod/products', + method: 'post', + data: data + }) +} + +// 修改prod +export function updateProducts(data) { + return request({ + url: '/prod/products', + method: 'put', + data: data + }) +} + +// 删除prod +export function delProducts(productId) { + return request({ + url: '/prod/products/' + productId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/prod/rakuten.js b/ruoyi-ui/src/api/prod/rakuten.js new file mode 100644 index 0000000..8c108a3 --- /dev/null +++ b/ruoyi-ui/src/api/prod/rakuten.js @@ -0,0 +1,106 @@ +import request from '@/utils/request' + +// 获取乐天市场商品数据 +export function scrapeRakutenProducts(query) { + return request({ + url: '/prod/rakuten/scrapeProducts', + method: 'get', + params: query + }) +} + +// 获取代理服务器列表 +export function getProxies() { + return request({ + url: '/tool/webmagic/proxies', + method: 'get' + }) +} + +// 设置当前使用的代理 +export function setProxy(proxyName) { + return request({ + url: '/tool/webmagic/proxy/set', + method: 'post', + params: { proxyName } + }) +} + +// 启动代理服务 +export function startProxy() { + return request({ + url: '/tool/webmagic/clash/start', + method: 'post' + }) +} + +// 停止代理服务 +export function stopProxy() { + return request({ + url: '/tool/webmagic/clash/stop', + method: 'post' + }) +} + +// 刷新1688 API Cookie +export function refresh1688Cookie() { + return request({ + url: '/figre/refreshCookie', + method: 'post' + }) +} + +// 批量上传图片URL到1688搜索 +export function batchUpload1688Images(imageUrls) { + return request({ + url: '/figre/batchUpload1688Images', + method: 'post', + data: { + imageUrls: imageUrls + } + }) +} + +// 爬取1688商品信息(包含ID、链接、标题等) +export function scrape1688Products(imageUrl) { + return request({ + url: '/prod/rakuten/scrape1688Products', + method: 'get', + params: { + imageUrl: imageUrl + } + }) +} + +// 图片代理接口 - 用于获取外部图片 +export function getProxyImage(imageUrl) { + return request({ + url: '/tool/banma/image-proxy', + method: 'get', + params: { url: imageUrl }, + responseType: 'blob' + }) +} + +// 上传文件到七牛云 +export function uploadFile(file) { + const formData = new FormData() + formData.append('files', file) + + return request({ + url: '/file/uploads', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 停止所有爬虫活动 +export function stopAllCrawling() { + return request({ + url: '/tool/webmagic/stop-crawling', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/api/system/config.js b/ruoyi-ui/src/api/system/config.js new file mode 100644 index 0000000..a404d82 --- /dev/null +++ b/ruoyi-ui/src/api/system/config.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询参数列表 +export function listConfig(query) { + return request({ + url: '/system/config/list', + method: 'get', + params: query + }) +} + +// 查询参数详细 +export function getConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'get' + }) +} + +// 根据参数键名查询参数值 +export function getConfigKey(configKey) { + return request({ + url: '/system/config/configKey/' + configKey, + method: 'get' + }) +} + +// 新增参数配置 +export function addConfig(data) { + return request({ + url: '/system/config', + method: 'post', + data: data + }) +} + +// 修改参数配置 +export function updateConfig(data) { + return request({ + url: '/system/config', + method: 'put', + data: data + }) +} + +// 删除参数配置 +export function delConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'delete' + }) +} + +// 刷新参数缓存 +export function refreshCache() { + return request({ + url: '/system/config/refreshCache', + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/dept.js b/ruoyi-ui/src/api/system/dept.js new file mode 100644 index 0000000..fc943cd --- /dev/null +++ b/ruoyi-ui/src/api/system/dept.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询部门列表 +export function listDept(query) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query + }) +} + +// 查询部门列表(排除节点) +export function listDeptExcludeChild(deptId) { + return request({ + url: '/system/dept/list/exclude/' + deptId, + method: 'get' + }) +} + +// 查询部门详细 +export function getDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'get' + }) +} + +// 新增部门 +export function addDept(data) { + return request({ + url: '/system/dept', + method: 'post', + data: data + }) +} + +// 修改部门 +export function updateDept(data) { + return request({ + url: '/system/dept', + method: 'put', + data: data + }) +} + +// 删除部门 +export function delDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/dict/data.js b/ruoyi-ui/src/api/system/dict/data.js new file mode 100644 index 0000000..6c9eb79 --- /dev/null +++ b/ruoyi-ui/src/api/system/dict/data.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询字典数据列表 +export function listData(query) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query + }) +} + +// 查询字典数据详细 +export function getData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'get' + }) +} + +// 根据字典类型查询字典数据信息 +export function getDicts(dictType) { + return request({ + url: '/system/dict/data/type/' + dictType, + method: 'get' + }) +} + +// 新增字典数据 +export function addData(data) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data + }) +} + +// 修改字典数据 +export function updateData(data) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data + }) +} + +// 删除字典数据 +export function delData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/dict/type.js b/ruoyi-ui/src/api/system/dict/type.js new file mode 100644 index 0000000..a7a6e01 --- /dev/null +++ b/ruoyi-ui/src/api/system/dict/type.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询字典类型列表 +export function listType(query) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query + }) +} + +// 查询字典类型详细 +export function getType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'get' + }) +} + +// 新增字典类型 +export function addType(data) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data + }) +} + +// 修改字典类型 +export function updateType(data) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data + }) +} + +// 删除字典类型 +export function delType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'delete' + }) +} + +// 刷新字典缓存 +export function refreshCache() { + return request({ + url: '/system/dict/type/refreshCache', + method: 'delete' + }) +} + +// 获取字典选择框列表 +export function optionselect() { + return request({ + url: '/system/dict/type/optionselect', + method: 'get' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/menu.js b/ruoyi-ui/src/api/system/menu.js new file mode 100644 index 0000000..f6415c6 --- /dev/null +++ b/ruoyi-ui/src/api/system/menu.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询菜单列表 +export function listMenu(query) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query + }) +} + +// 查询菜单详细 +export function getMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'get' + }) +} + +// 查询菜单下拉树结构 +export function treeselect() { + return request({ + url: '/system/menu/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询菜单下拉树结构 +export function roleMenuTreeselect(roleId) { + return request({ + url: '/system/menu/roleMenuTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增菜单 +export function addMenu(data) { + return request({ + url: '/system/menu', + method: 'post', + data: data + }) +} + +// 修改菜单 +export function updateMenu(data) { + return request({ + url: '/system/menu', + method: 'put', + data: data + }) +} + +// 删除菜单 +export function delMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/notice.js b/ruoyi-ui/src/api/system/notice.js new file mode 100644 index 0000000..c274ea5 --- /dev/null +++ b/ruoyi-ui/src/api/system/notice.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listNotice(query) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'get' + }) +} + +// 新增公告 +export function addNotice(data) { + return request({ + url: '/system/notice', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateNotice(data) { + return request({ + url: '/system/notice', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/api/system/post.js b/ruoyi-ui/src/api/system/post.js new file mode 100644 index 0000000..1a8e9ca --- /dev/null +++ b/ruoyi-ui/src/api/system/post.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询岗位列表 +export function listPost(query) { + return request({ + url: '/system/post/list', + method: 'get', + params: query + }) +} + +// 查询岗位详细 +export function getPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'get' + }) +} + +// 新增岗位 +export function addPost(data) { + return request({ + url: '/system/post', + method: 'post', + data: data + }) +} + +// 修改岗位 +export function updatePost(data) { + return request({ + url: '/system/post', + method: 'put', + data: data + }) +} + +// 删除岗位 +export function delPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/api/system/role.js b/ruoyi-ui/src/api/system/role.js new file mode 100644 index 0000000..f13e6f4 --- /dev/null +++ b/ruoyi-ui/src/api/system/role.js @@ -0,0 +1,119 @@ +import request from '@/utils/request' + +// 查询角色列表 +export function listRole(query) { + return request({ + url: '/system/role/list', + method: 'get', + params: query + }) +} + +// 查询角色详细 +export function getRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'get' + }) +} + +// 新增角色 +export function addRole(data) { + return request({ + url: '/system/role', + method: 'post', + data: data + }) +} + +// 修改角色 +export function updateRole(data) { + return request({ + url: '/system/role', + method: 'put', + data: data + }) +} + +// 角色数据权限 +export function dataScope(data) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data + }) +} + +// 角色状态修改 +export function changeRoleStatus(roleId, status) { + const data = { + roleId, + status + } + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: data + }) +} + +// 删除角色 +export function delRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'delete' + }) +} + +// 查询角色已授权用户列表 +export function allocatedUserList(query) { + return request({ + url: '/system/role/authUser/allocatedList', + method: 'get', + params: query + }) +} + +// 查询角色未授权用户列表 +export function unallocatedUserList(query) { + return request({ + url: '/system/role/authUser/unallocatedList', + method: 'get', + params: query + }) +} + +// 取消用户授权角色 +export function authUserCancel(data) { + return request({ + url: '/system/role/authUser/cancel', + method: 'put', + data: data + }) +} + +// 批量取消用户授权角色 +export function authUserCancelAll(data) { + return request({ + url: '/system/role/authUser/cancelAll', + method: 'put', + params: data + }) +} + +// 授权用户选择 +export function authUserSelectAll(data) { + return request({ + url: '/system/role/authUser/selectAll', + method: 'put', + params: data + }) +} + +// 根据角色ID查询部门树结构 +export function deptTreeSelect(roleId) { + return request({ + url: '/system/role/deptTree/' + roleId, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/system/user.js b/ruoyi-ui/src/api/system/user.js new file mode 100644 index 0000000..b5e3edd --- /dev/null +++ b/ruoyi-ui/src/api/system/user.js @@ -0,0 +1,136 @@ +import request from '@/utils/request' +import { parseStrEmpty } from "@/utils/ruoyi"; + +// 查询用户列表 +export function listUser(query) { + return request({ + url: '/system/user/list', + method: 'get', + params: query + }) +} + +// 查询用户详细 +export function getUser(userId) { + return request({ + url: '/system/user/' + parseStrEmpty(userId), + method: 'get' + }) +} + +// 新增用户 +export function addUser(data) { + return request({ + url: '/system/user', + method: 'post', + data: data + }) +} + +// 修改用户 +export function updateUser(data) { + return request({ + url: '/system/user', + method: 'put', + data: data + }) +} + +// 删除用户 +export function delUser(userId) { + return request({ + url: '/system/user/' + userId, + method: 'delete' + }) +} + +// 用户密码重置 +export function resetUserPwd(userId, password) { + const data = { + userId, + password + } + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: data + }) +} + +// 用户状态修改 +export function changeUserStatus(userId, status) { + const data = { + userId, + status + } + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: data + }) +} + +// 查询用户个人信息 +export function getUserProfile() { + return request({ + url: '/system/user/profile', + method: 'get' + }) +} + +// 修改用户个人信息 +export function updateUserProfile(data) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data + }) +} + +// 用户密码重置 +export function updateUserPwd(oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + data: data + }) +} + +// 用户头像上传 +export function uploadAvatar(data) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + data: data + }) +} + +// 查询授权角色 +export function getAuthRole(userId) { + return request({ + url: '/system/user/authRole/' + userId, + method: 'get' + }) +} + +// 保存授权角色 +export function updateAuthRole(data) { + return request({ + url: '/system/user/authRole', + method: 'put', + params: data + }) +} + +// 查询部门下拉树结构 +export function deptTreeSelect() { + return request({ + url: '/system/user/deptTree', + method: 'get' + }) +} diff --git a/ruoyi-ui/src/api/tool/gen.js b/ruoyi-ui/src/api/tool/gen.js new file mode 100644 index 0000000..2075677 --- /dev/null +++ b/ruoyi-ui/src/api/tool/gen.js @@ -0,0 +1,85 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 创建表 +export function createTable(data) { + return request({ + url: '/tool/gen/createTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/assets/401_images/401.gif b/ruoyi-ui/src/assets/401_images/401.gif new file mode 100644 index 0000000..cd6e0d9 Binary files /dev/null and b/ruoyi-ui/src/assets/401_images/401.gif differ diff --git a/ruoyi-ui/src/assets/404_images/404.png b/ruoyi-ui/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/ruoyi-ui/src/assets/404_images/404.png differ diff --git a/ruoyi-ui/src/assets/404_images/404_cloud.png b/ruoyi-ui/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/ruoyi-ui/src/assets/404_images/404_cloud.png differ diff --git a/ruoyi-ui/src/assets/icons/index.js b/ruoyi-ui/src/assets/icons/index.js new file mode 100644 index 0000000..2c6b309 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/ruoyi-ui/src/assets/icons/svg/404.svg b/ruoyi-ui/src/assets/icons/svg/404.svg new file mode 100644 index 0000000..6df5019 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/bug.svg b/ruoyi-ui/src/assets/icons/svg/bug.svg new file mode 100644 index 0000000..05a150d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/build.svg b/ruoyi-ui/src/assets/icons/svg/build.svg new file mode 100644 index 0000000..97c4688 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/button.svg b/ruoyi-ui/src/assets/icons/svg/button.svg new file mode 100644 index 0000000..904fddc --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/cascader.svg b/ruoyi-ui/src/assets/icons/svg/cascader.svg new file mode 100644 index 0000000..e256024 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/chart.svg b/ruoyi-ui/src/assets/icons/svg/chart.svg new file mode 100644 index 0000000..27728fb --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/checkbox.svg b/ruoyi-ui/src/assets/icons/svg/checkbox.svg new file mode 100644 index 0000000..013fd3a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/clipboard.svg b/ruoyi-ui/src/assets/icons/svg/clipboard.svg new file mode 100644 index 0000000..90923ff --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/code.svg b/ruoyi-ui/src/assets/icons/svg/code.svg new file mode 100644 index 0000000..5f9c5ab --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/color.svg b/ruoyi-ui/src/assets/icons/svg/color.svg new file mode 100644 index 0000000..44a81aa --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/component.svg b/ruoyi-ui/src/assets/icons/svg/component.svg new file mode 100644 index 0000000..29c3458 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/dashboard.svg b/ruoyi-ui/src/assets/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/date-range.svg b/ruoyi-ui/src/assets/icons/svg/date-range.svg new file mode 100644 index 0000000..fda571e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/date-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/date.svg b/ruoyi-ui/src/assets/icons/svg/date.svg new file mode 100644 index 0000000..52dc73e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/dict.svg b/ruoyi-ui/src/assets/icons/svg/dict.svg new file mode 100644 index 0000000..4849377 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/documentation.svg b/ruoyi-ui/src/assets/icons/svg/documentation.svg new file mode 100644 index 0000000..7043122 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/download.svg b/ruoyi-ui/src/assets/icons/svg/download.svg new file mode 100644 index 0000000..c896951 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/drag.svg b/ruoyi-ui/src/assets/icons/svg/drag.svg new file mode 100644 index 0000000..4185d3c --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/druid.svg b/ruoyi-ui/src/assets/icons/svg/druid.svg new file mode 100644 index 0000000..a2b4b4e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/edit.svg b/ruoyi-ui/src/assets/icons/svg/edit.svg new file mode 100644 index 0000000..d26101f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/education.svg b/ruoyi-ui/src/assets/icons/svg/education.svg new file mode 100644 index 0000000..7bfb01d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/email.svg b/ruoyi-ui/src/assets/icons/svg/email.svg new file mode 100644 index 0000000..74d25e2 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/enter.svg b/ruoyi-ui/src/assets/icons/svg/enter.svg new file mode 100644 index 0000000..f7cabf2 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/example.svg b/ruoyi-ui/src/assets/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/excel.svg b/ruoyi-ui/src/assets/icons/svg/excel.svg new file mode 100644 index 0000000..74d97b8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg b/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 0000000..485c128 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/eye-open.svg b/ruoyi-ui/src/assets/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/eye.svg b/ruoyi-ui/src/assets/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/form.svg b/ruoyi-ui/src/assets/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/fullscreen.svg b/ruoyi-ui/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 0000000..0e86b6f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/github.svg b/ruoyi-ui/src/assets/icons/svg/github.svg new file mode 100644 index 0000000..db0a0d4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/guide.svg b/ruoyi-ui/src/assets/icons/svg/guide.svg new file mode 100644 index 0000000..b271001 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/icon.svg b/ruoyi-ui/src/assets/icons/svg/icon.svg new file mode 100644 index 0000000..82be8ee --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/input.svg b/ruoyi-ui/src/assets/icons/svg/input.svg new file mode 100644 index 0000000..ab91381 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/international.svg b/ruoyi-ui/src/assets/icons/svg/international.svg new file mode 100644 index 0000000..e9b56ee --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/job.svg b/ruoyi-ui/src/assets/icons/svg/job.svg new file mode 100644 index 0000000..2a93a25 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/language.svg b/ruoyi-ui/src/assets/icons/svg/language.svg new file mode 100644 index 0000000..0082b57 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/link.svg b/ruoyi-ui/src/assets/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/list.svg b/ruoyi-ui/src/assets/icons/svg/list.svg new file mode 100644 index 0000000..20259ed --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/lock.svg b/ruoyi-ui/src/assets/icons/svg/lock.svg new file mode 100644 index 0000000..74fee54 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/log.svg b/ruoyi-ui/src/assets/icons/svg/log.svg new file mode 100644 index 0000000..d879d33 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/logininfor.svg b/ruoyi-ui/src/assets/icons/svg/logininfor.svg new file mode 100644 index 0000000..267f844 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/message.svg b/ruoyi-ui/src/assets/icons/svg/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/money.svg b/ruoyi-ui/src/assets/icons/svg/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/monitor.svg b/ruoyi-ui/src/assets/icons/svg/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/more-up.svg b/ruoyi-ui/src/assets/icons/svg/more-up.svg new file mode 100644 index 0000000..d30ac11 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/more-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/nested.svg b/ruoyi-ui/src/assets/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/number.svg b/ruoyi-ui/src/assets/icons/svg/number.svg new file mode 100644 index 0000000..ad5ce9a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/online.svg b/ruoyi-ui/src/assets/icons/svg/online.svg new file mode 100644 index 0000000..330a202 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/password.svg b/ruoyi-ui/src/assets/icons/svg/password.svg new file mode 100644 index 0000000..6c64def --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/pdf.svg b/ruoyi-ui/src/assets/icons/svg/pdf.svg new file mode 100644 index 0000000..957aa0c --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/people.svg b/ruoyi-ui/src/assets/icons/svg/people.svg new file mode 100644 index 0000000..2bd54ae --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/peoples.svg b/ruoyi-ui/src/assets/icons/svg/peoples.svg new file mode 100644 index 0000000..aab852e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/phone.svg b/ruoyi-ui/src/assets/icons/svg/phone.svg new file mode 100644 index 0000000..ab8e8c4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/post.svg b/ruoyi-ui/src/assets/icons/svg/post.svg new file mode 100644 index 0000000..2922c61 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/qq.svg b/ruoyi-ui/src/assets/icons/svg/qq.svg new file mode 100644 index 0000000..ee13d4e --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/question.svg b/ruoyi-ui/src/assets/icons/svg/question.svg new file mode 100644 index 0000000..cf75bd4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/radio.svg b/ruoyi-ui/src/assets/icons/svg/radio.svg new file mode 100644 index 0000000..0cde345 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/rate.svg b/ruoyi-ui/src/assets/icons/svg/rate.svg new file mode 100644 index 0000000..aa3b14d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/redis-list.svg b/ruoyi-ui/src/assets/icons/svg/redis-list.svg new file mode 100644 index 0000000..98a15b2 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/redis-list.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/redis.svg b/ruoyi-ui/src/assets/icons/svg/redis.svg new file mode 100644 index 0000000..2f1d62d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/row.svg b/ruoyi-ui/src/assets/icons/svg/row.svg new file mode 100644 index 0000000..0780992 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/search.svg b/ruoyi-ui/src/assets/icons/svg/search.svg new file mode 100644 index 0000000..84233dd --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/select.svg b/ruoyi-ui/src/assets/icons/svg/select.svg new file mode 100644 index 0000000..d628382 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/server.svg b/ruoyi-ui/src/assets/icons/svg/server.svg new file mode 100644 index 0000000..eb287e3 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/shopping.svg b/ruoyi-ui/src/assets/icons/svg/shopping.svg new file mode 100644 index 0000000..87513e7 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/size.svg b/ruoyi-ui/src/assets/icons/svg/size.svg new file mode 100644 index 0000000..ddb25b8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/skill.svg b/ruoyi-ui/src/assets/icons/svg/skill.svg new file mode 100644 index 0000000..a3b7312 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/slider.svg b/ruoyi-ui/src/assets/icons/svg/slider.svg new file mode 100644 index 0000000..fbe4f39 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/star.svg b/ruoyi-ui/src/assets/icons/svg/star.svg new file mode 100644 index 0000000..6cf86e6 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/swagger.svg b/ruoyi-ui/src/assets/icons/svg/swagger.svg new file mode 100644 index 0000000..05d4e7b --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/switch.svg b/ruoyi-ui/src/assets/icons/svg/switch.svg new file mode 100644 index 0000000..0ba61e3 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/system.svg b/ruoyi-ui/src/assets/icons/svg/system.svg new file mode 100644 index 0000000..5992593 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tab.svg b/ruoyi-ui/src/assets/icons/svg/tab.svg new file mode 100644 index 0000000..b4b48e4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/table.svg b/ruoyi-ui/src/assets/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/textarea.svg b/ruoyi-ui/src/assets/icons/svg/textarea.svg new file mode 100644 index 0000000..2709f29 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/theme.svg b/ruoyi-ui/src/assets/icons/svg/theme.svg new file mode 100644 index 0000000..5982a2f --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/time-range.svg b/ruoyi-ui/src/assets/icons/svg/time-range.svg new file mode 100644 index 0000000..13c1202 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/time-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/time.svg b/ruoyi-ui/src/assets/icons/svg/time.svg new file mode 100644 index 0000000..b376e32 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tool.svg b/ruoyi-ui/src/assets/icons/svg/tool.svg new file mode 100644 index 0000000..48e0e35 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tree-table.svg b/ruoyi-ui/src/assets/icons/svg/tree-table.svg new file mode 100644 index 0000000..8aafdb8 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/tree.svg b/ruoyi-ui/src/assets/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/upload.svg b/ruoyi-ui/src/assets/icons/svg/upload.svg new file mode 100644 index 0000000..bae49c0 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/user.svg b/ruoyi-ui/src/assets/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/validCode.svg b/ruoyi-ui/src/assets/icons/svg/validCode.svg new file mode 100644 index 0000000..cfb1021 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/wechat.svg b/ruoyi-ui/src/assets/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/zip.svg b/ruoyi-ui/src/assets/icons/svg/zip.svg new file mode 100644 index 0000000..f806fc4 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svgo.yml b/ruoyi-ui/src/assets/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/ruoyi-ui/src/assets/images/dark.svg b/ruoyi-ui/src/assets/images/dark.svg new file mode 100644 index 0000000..f646bd7 --- /dev/null +++ b/ruoyi-ui/src/assets/images/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/images/light.svg b/ruoyi-ui/src/assets/images/light.svg new file mode 100644 index 0000000..ab7cc08 --- /dev/null +++ b/ruoyi-ui/src/assets/images/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/images/login-background.jpg b/ruoyi-ui/src/assets/images/login-background.jpg new file mode 100644 index 0000000..8a89eb8 Binary files /dev/null and b/ruoyi-ui/src/assets/images/login-background.jpg differ diff --git a/ruoyi-ui/src/assets/images/pay.png b/ruoyi-ui/src/assets/images/pay.png new file mode 100644 index 0000000..bb8b967 Binary files /dev/null and b/ruoyi-ui/src/assets/images/pay.png differ diff --git a/ruoyi-ui/src/assets/images/profile.jpg b/ruoyi-ui/src/assets/images/profile.jpg new file mode 100644 index 0000000..b3a940b Binary files /dev/null and b/ruoyi-ui/src/assets/images/profile.jpg differ diff --git a/ruoyi-ui/src/assets/logo/logo.png b/ruoyi-ui/src/assets/logo/logo.png new file mode 100644 index 0000000..e263760 Binary files /dev/null and b/ruoyi-ui/src/assets/logo/logo.png differ diff --git a/ruoyi-ui/src/assets/styles/btn.scss b/ruoyi-ui/src/assets/styles/btn.scss new file mode 100644 index 0000000..e6ba1a8 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@import './variables.scss'; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/ruoyi-ui/src/assets/styles/element-ui.scss b/ruoyi-ui/src/assets/styles/element-ui.scss new file mode 100644 index 0000000..363092a --- /dev/null +++ b/ruoyi-ui/src/assets/styles/element-ui.scss @@ -0,0 +1,92 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0px; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0px; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} + +.el-menu--collapse + > div + > .el-submenu + > .el-submenu__title + .el-submenu__icon-arrow { + display: none; +} \ No newline at end of file diff --git a/ruoyi-ui/src/assets/styles/element-variables.scss b/ruoyi-ui/src/assets/styles/element-variables.scss new file mode 100644 index 0000000..1615ff2 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/element-variables.scss @@ -0,0 +1,31 @@ +/** +* I think element-ui's default theme color is too light for long-term use. +* So I modified the default color and you can modify it to your liking. +**/ + +/* theme color */ +$--color-primary: #1890ff; +$--color-success: #13ce66; +$--color-warning: #ffba00; +$--color-danger: #ff4949; +// $--color-info: #1E1E1E; + +$--button-font-weight: 400; + +// $--color-text-regular: #1f2d3d; + +$--border-color-light: #dfe4ed; +$--border-color-lighter: #e6ebf5; + +$--table-border: 1px solid #dfe6ec; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "~element-ui/packages/theme-chalk/src/index"; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + theme: $--color-primary; +} diff --git a/ruoyi-ui/src/assets/styles/index.scss b/ruoyi-ui/src/assets/styles/index.scss new file mode 100644 index 0000000..bb87292 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/index.scss @@ -0,0 +1,178 @@ +@import './variables.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; +@import './btn.scss'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +//main-container全局样式 +.app-container { + padding: 20px; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +.filter-container { + padding-bottom: 10px; + + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} diff --git a/ruoyi-ui/src/assets/styles/mixin.scss b/ruoyi-ui/src/assets/styles/mixin.scss new file mode 100644 index 0000000..06fa061 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/ruoyi-ui/src/assets/styles/ruoyi.scss b/ruoyi-ui/src/assets/styles/ruoyi.scss new file mode 100644 index 0000000..ab4a1d2 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/ruoyi.scss @@ -0,0 +1,287 @@ +/** +* 通用css样式布局处理 +* Copyright (c) 2019 ruoyi +*/ + +/** 基础通用 **/ +.pt5 { + padding-top: 5px; +} + +.pr5 { + padding-right: 5px; +} + +.pb5 { + padding-bottom: 5px; +} + +.mt5 { + margin-top: 5px; +} + +.mr5 { + margin-right: 5px; +} + +.mb5 { + margin-bottom: 5px; +} + +.mb8 { + margin-bottom: 8px; +} + +.ml5 { + margin-left: 5px; +} + +.mt10 { + margin-top: 10px; +} + +.mr10 { + margin-right: 10px; +} + +.mb10 { + margin-bottom: 10px; +} +.ml10 { + margin-left: 10px; +} + +.mt20 { + margin-top: 20px; +} + +.mr20 { + margin-right: 20px; +} + +.mb20 { + margin-bottom: 20px; +} +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-message-box__status + .el-message-box__message{ + word-break: break-word; +} + +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table { + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + word-break: break-word; + background-color: #f8f8f9; + color: #515a6e; + height: 40px; + font-size: 13px; + } + } + + .el-table__body-wrapper { + .el-button [class*="el-icon-"] + span { + margin-left: 1px; + } + } +} + +/** 表单布局 **/ +.form-header { + font-size: 15px; + color: #6379bb; + border-bottom: 1px solid #ddd; + margin: 8px 10px 25px 10px; + padding-bottom: 5px +} + +/** 表格布局 **/ +.pagination-container { + display: flex; + justify-content: flex-end; + margin-top: 20px; +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid #e5e6e7; + background: #FFFFFF none; + border-radius: 4px; +} + +@media (max-width: 768px) { + .pagination-container .el-pagination > .el-pagination__jump { + display: none !important; + } + .pagination-container .el-pagination > .el-pagination__sizes { + display: none !important; + } +} + +.el-table .fixed-width .el-button--mini { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link,.el-table .el-dropdown-selfdefine { + cursor: pointer; + margin-left: 5px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px; + min-height: 40px; +} + +.el-card__body { + padding: 15px 20px 20px 20px; +} + +.card-box { + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost { + opacity: .8; + color: #fff !important; + background: #42b983 !important; +} + +.top-right-btn { + position: relative; + float: right; +} + +/* 分割面板样式 */ +.splitpanes.default-theme .splitpanes__pane { + background-color: #fff!important; +} diff --git a/ruoyi-ui/src/assets/styles/sidebar.scss b/ruoyi-ui/src/assets/styles/sidebar.scss new file mode 100644 index 0000000..3558293 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/sidebar.scss @@ -0,0 +1,227 @@ +#app { + + .main-container { + height: 100%; + transition: margin-left .28s; + margin-left: $base-sidebar-width; + position: relative; + } + + .sidebarHide { + margin-left: 0!important; + } + + .sidebar-container { + -webkit-transition: width .28s; + transition: width 0.28s; + width: $base-sidebar-width !important; + background-color: $base-menu-background; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + .el-menu-item, .el-submenu__title { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + // menu hover + .submenu-title-noDropdown, + .el-submenu__title { + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .is-active > .el-submenu__title { + color: $base-menu-color-active !important; + } + + & .nest-menu .el-submenu>.el-submenu__title, + & .el-submenu .el-menu-item { + min-width: $base-sidebar-width !important; + + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .nest-menu .el-submenu>.el-submenu__title, + & .theme-dark .el-submenu .el-menu-item { + background-color: $base-sub-menu-background !important; + + &:hover { + background-color: $base-sub-menu-hover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .submenu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + } + } + + .el-submenu { + overflow: hidden; + + &>.el-submenu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + } + } + + .el-menu--collapse { + .el-submenu { + &>.el-submenu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-submenu { + min-width: $base-sidebar-width !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: $base-sidebar-width !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$base-sidebar-width, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + } + + .nest-menu .el-submenu>.el-submenu__title, + .el-menu-item { + &:hover { + // you can use $subMenuHover + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + // the scroll bar appears when the subMenu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/ruoyi-ui/src/assets/styles/transition.scss b/ruoyi-ui/src/assets/styles/transition.scss new file mode 100644 index 0000000..073f8c6 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/transition.scss @@ -0,0 +1,49 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform--move, +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/ruoyi-ui/src/assets/styles/variables.scss b/ruoyi-ui/src/assets/styles/variables.scss new file mode 100644 index 0000000..34484d4 --- /dev/null +++ b/ruoyi-ui/src/assets/styles/variables.scss @@ -0,0 +1,54 @@ +// base color +$blue:#324157; +$light-blue:#3A71A8; +$red:#C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow:#FEC171; +$panGreen: #30B08F; + +// 默认菜单主题风格 +$base-menu-color:#bfcbd9; +$base-menu-color-active:#f4f4f5; +$base-menu-background:#304156; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#1f2d3d; +$base-sub-menu-hover:#001528; + +// 自定义暗色菜单风格 +/** +$base-menu-color:hsla(0,0%,100%,.65); +$base-menu-color-active:#fff; +$base-menu-background:#001529; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#000c17; +$base-sub-menu-hover:#001528; +*/ + +$base-sidebar-width: 200px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuColor: $base-menu-color; + menuLightColor: $base-menu-light-color; + menuColorActive: $base-menu-color-active; + menuBackground: $base-menu-background; + menuLightBackground: $base-menu-light-background; + subMenuBackground: $base-sub-menu-background; + subMenuHover: $base-sub-menu-hover; + sideBarWidth: $base-sidebar-width; + logoTitleColor: $base-logo-title-color; + logoLightTitleColor: $base-logo-light-title-color +} diff --git a/ruoyi-ui/src/components/Breadcrumb/index.vue b/ruoyi-ui/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..080595a --- /dev/null +++ b/ruoyi-ui/src/components/Breadcrumb/index.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/ruoyi-ui/src/components/Charts/LineChart.vue b/ruoyi-ui/src/components/Charts/LineChart.vue new file mode 100644 index 0000000..18f8486 --- /dev/null +++ b/ruoyi-ui/src/components/Charts/LineChart.vue @@ -0,0 +1,107 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Charts/PieChart.vue b/ruoyi-ui/src/components/Charts/PieChart.vue new file mode 100644 index 0000000..8d81187 --- /dev/null +++ b/ruoyi-ui/src/components/Charts/PieChart.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Crontab/day.vue b/ruoyi-ui/src/components/Crontab/day.vue new file mode 100644 index 0000000..d3faed7 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/day.vue @@ -0,0 +1,161 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/hour.vue b/ruoyi-ui/src/components/Crontab/hour.vue new file mode 100644 index 0000000..5659bfb --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/hour.vue @@ -0,0 +1,120 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/index.vue b/ruoyi-ui/src/components/Crontab/index.vue new file mode 100644 index 0000000..395f615 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/index.vue @@ -0,0 +1,430 @@ + + + + diff --git a/ruoyi-ui/src/components/Crontab/min.vue b/ruoyi-ui/src/components/Crontab/min.vue new file mode 100644 index 0000000..ec56a44 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/min.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Crontab/month.vue b/ruoyi-ui/src/components/Crontab/month.vue new file mode 100644 index 0000000..da60132 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/month.vue @@ -0,0 +1,114 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/result.vue b/ruoyi-ui/src/components/Crontab/result.vue new file mode 100644 index 0000000..4e2952d --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/result.vue @@ -0,0 +1,558 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/second.vue b/ruoyi-ui/src/components/Crontab/second.vue new file mode 100644 index 0000000..9201e06 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/second.vue @@ -0,0 +1,117 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/week.vue b/ruoyi-ui/src/components/Crontab/week.vue new file mode 100644 index 0000000..75c42ed --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/week.vue @@ -0,0 +1,202 @@ + + + diff --git a/ruoyi-ui/src/components/Crontab/year.vue b/ruoyi-ui/src/components/Crontab/year.vue new file mode 100644 index 0000000..8c18c78 --- /dev/null +++ b/ruoyi-ui/src/components/Crontab/year.vue @@ -0,0 +1,131 @@ + + + diff --git a/ruoyi-ui/src/components/DictData/index.js b/ruoyi-ui/src/components/DictData/index.js new file mode 100644 index 0000000..7b85d4a --- /dev/null +++ b/ruoyi-ui/src/components/DictData/index.js @@ -0,0 +1,49 @@ +import Vue from 'vue' +import store from '@/store' +import DataDict from '@/utils/dict' +import { getDicts as getDicts } from '@/api/system/dict/data' + +function searchDictByKey(dict, key) { + if (key == null && key == "") { + return null + } + try { + for (let i = 0; i < dict.length; i++) { + if (dict[i].key == key) { + return dict[i].value + } + } + } catch (e) { + return null + } +} + +function install() { + Vue.use(DataDict, { + metas: { + '*': { + labelField: 'dictLabel', + valueField: 'dictValue', + request(dictMeta) { + const storeDict = searchDictByKey(store.getters.dict, dictMeta.type) + if (storeDict) { + return new Promise(resolve => { resolve(storeDict) }) + } else { + return new Promise((resolve, reject) => { + getDicts(dictMeta.type).then(res => { + store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data }) + resolve(res.data) + }).catch(error => { + reject(error) + }) + }) + } + }, + }, + }, + }) +} + +export default { + install, +} \ No newline at end of file diff --git a/ruoyi-ui/src/components/DictTag/index.vue b/ruoyi-ui/src/components/DictTag/index.vue new file mode 100644 index 0000000..73dcf91 --- /dev/null +++ b/ruoyi-ui/src/components/DictTag/index.vue @@ -0,0 +1,89 @@ + + + + diff --git a/ruoyi-ui/src/components/Editor/index.vue b/ruoyi-ui/src/components/Editor/index.vue new file mode 100644 index 0000000..553f952 --- /dev/null +++ b/ruoyi-ui/src/components/Editor/index.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/ruoyi-ui/src/components/FileUpload/index.vue b/ruoyi-ui/src/components/FileUpload/index.vue new file mode 100644 index 0000000..ca37c55 --- /dev/null +++ b/ruoyi-ui/src/components/FileUpload/index.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/ruoyi-ui/src/components/Hamburger/index.vue b/ruoyi-ui/src/components/Hamburger/index.vue new file mode 100644 index 0000000..368b002 --- /dev/null +++ b/ruoyi-ui/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/ruoyi-ui/src/components/HeaderSearch/index.vue b/ruoyi-ui/src/components/HeaderSearch/index.vue new file mode 100644 index 0000000..a936bdf --- /dev/null +++ b/ruoyi-ui/src/components/HeaderSearch/index.vue @@ -0,0 +1,264 @@ + + + + + + diff --git a/ruoyi-ui/src/components/IconSelect/index.vue b/ruoyi-ui/src/components/IconSelect/index.vue new file mode 100644 index 0000000..8dadc02 --- /dev/null +++ b/ruoyi-ui/src/components/IconSelect/index.vue @@ -0,0 +1,104 @@ + + + + + + diff --git a/ruoyi-ui/src/components/IconSelect/requireIcons.js b/ruoyi-ui/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..99e5c54 --- /dev/null +++ b/ruoyi-ui/src/components/IconSelect/requireIcons.js @@ -0,0 +1,11 @@ + +const req = require.context('../../assets/icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/ruoyi-ui/src/components/ImagePreview/index.vue b/ruoyi-ui/src/components/ImagePreview/index.vue new file mode 100644 index 0000000..8b4738b --- /dev/null +++ b/ruoyi-ui/src/components/ImagePreview/index.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/ruoyi-ui/src/components/ImageUpload/index.vue b/ruoyi-ui/src/components/ImageUpload/index.vue new file mode 100644 index 0000000..6b24f6e --- /dev/null +++ b/ruoyi-ui/src/components/ImageUpload/index.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/ruoyi-ui/src/components/Pagination/index.vue b/ruoyi-ui/src/components/Pagination/index.vue new file mode 100644 index 0000000..08ac487 --- /dev/null +++ b/ruoyi-ui/src/components/Pagination/index.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/ruoyi-ui/src/components/PanThumb/index.vue b/ruoyi-ui/src/components/PanThumb/index.vue new file mode 100644 index 0000000..b24e026 --- /dev/null +++ b/ruoyi-ui/src/components/PanThumb/index.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/ruoyi-ui/src/components/ParentView/index.vue b/ruoyi-ui/src/components/ParentView/index.vue new file mode 100644 index 0000000..7bf6148 --- /dev/null +++ b/ruoyi-ui/src/components/ParentView/index.vue @@ -0,0 +1,3 @@ + diff --git a/ruoyi-ui/src/components/RightToolbar/index.vue b/ruoyi-ui/src/components/RightToolbar/index.vue new file mode 100644 index 0000000..7865214 --- /dev/null +++ b/ruoyi-ui/src/components/RightToolbar/index.vue @@ -0,0 +1,154 @@ + + + diff --git a/ruoyi-ui/src/components/RuoYi/Doc/index.vue b/ruoyi-ui/src/components/RuoYi/Doc/index.vue new file mode 100644 index 0000000..75fa864 --- /dev/null +++ b/ruoyi-ui/src/components/RuoYi/Doc/index.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/RuoYi/Git/index.vue b/ruoyi-ui/src/components/RuoYi/Git/index.vue new file mode 100644 index 0000000..bdafbae --- /dev/null +++ b/ruoyi-ui/src/components/RuoYi/Git/index.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/components/Screenfull/index.vue b/ruoyi-ui/src/components/Screenfull/index.vue new file mode 100644 index 0000000..d4e539c --- /dev/null +++ b/ruoyi-ui/src/components/Screenfull/index.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/ruoyi-ui/src/components/SizeSelect/index.vue b/ruoyi-ui/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..e513948 --- /dev/null +++ b/ruoyi-ui/src/components/SizeSelect/index.vue @@ -0,0 +1,55 @@ + + + diff --git a/ruoyi-ui/src/components/SvgIcon/index.vue b/ruoyi-ui/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..e4bf5ad --- /dev/null +++ b/ruoyi-ui/src/components/SvgIcon/index.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/ruoyi-ui/src/components/ThemePicker/index.vue b/ruoyi-ui/src/components/ThemePicker/index.vue new file mode 100644 index 0000000..a69ee45 --- /dev/null +++ b/ruoyi-ui/src/components/ThemePicker/index.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/ruoyi-ui/src/components/TopNav/index.vue b/ruoyi-ui/src/components/TopNav/index.vue new file mode 100644 index 0000000..b3f3331 --- /dev/null +++ b/ruoyi-ui/src/components/TopNav/index.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/ruoyi-ui/src/components/iFrame/index.vue b/ruoyi-ui/src/components/iFrame/index.vue new file mode 100644 index 0000000..f4334f3 --- /dev/null +++ b/ruoyi-ui/src/components/iFrame/index.vue @@ -0,0 +1,36 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Navbar.vue b/ruoyi-ui/src/layout/components/Navbar.vue new file mode 100644 index 0000000..5c31196 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Navbar.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Settings/index.vue b/ruoyi-ui/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..1023f7f --- /dev/null +++ b/ruoyi-ui/src/layout/components/Settings/index.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js b/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..6823726 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js @@ -0,0 +1,25 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +} diff --git a/ruoyi-ui/src/layout/components/Sidebar/Item.vue b/ruoyi-ui/src/layout/components/Sidebar/Item.vue new file mode 100644 index 0000000..be3285d --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Item.vue @@ -0,0 +1,33 @@ + diff --git a/ruoyi-ui/src/layout/components/Sidebar/Link.vue b/ruoyi-ui/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..8b0bc93 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,43 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/Logo.vue b/ruoyi-ui/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..4ec53eb --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue b/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..bddb65f --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,99 @@ + + + diff --git a/ruoyi-ui/src/layout/components/Sidebar/index.vue b/ruoyi-ui/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..cd86ea6 --- /dev/null +++ b/ruoyi-ui/src/layout/components/Sidebar/index.vue @@ -0,0 +1,57 @@ + + + diff --git a/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue new file mode 100644 index 0000000..bb753a1 --- /dev/null +++ b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/ruoyi-ui/src/layout/components/TagsView/index.vue b/ruoyi-ui/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..f0b0837 --- /dev/null +++ b/ruoyi-ui/src/layout/components/TagsView/index.vue @@ -0,0 +1,337 @@ + + + + + + + diff --git a/ruoyi-ui/src/layout/components/index.js b/ruoyi-ui/src/layout/components/index.js new file mode 100644 index 0000000..104bd3a --- /dev/null +++ b/ruoyi-ui/src/layout/components/index.js @@ -0,0 +1,5 @@ +export { default as AppMain } from './AppMain' +export { default as Navbar } from './Navbar' +export { default as Settings } from './Settings' +export { default as Sidebar } from './Sidebar/index.vue' +export { default as TagsView } from './TagsView/index.vue' diff --git a/ruoyi-ui/src/layout/index.vue b/ruoyi-ui/src/layout/index.vue new file mode 100644 index 0000000..05e2fa9 --- /dev/null +++ b/ruoyi-ui/src/layout/index.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/ruoyi-ui/src/layout/mixin/ResizeHandler.js b/ruoyi-ui/src/layout/mixin/ResizeHandler.js new file mode 100644 index 0000000..e8d0df8 --- /dev/null +++ b/ruoyi-ui/src/layout/mixin/ResizeHandler.js @@ -0,0 +1,45 @@ +import store from '@/store' + +const { body } = document +const WIDTH = 992 // refer to Bootstrap's responsive design + +export default { + watch: { + $route(route) { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('app/closeSideBar', { withoutAnimation: false }) + } + } + }, + beforeMount() { + window.addEventListener('resize', this.$_resizeHandler) + }, + beforeDestroy() { + window.removeEventListener('resize', this.$_resizeHandler) + }, + mounted() { + const isMobile = this.$_isMobile() + if (isMobile) { + store.dispatch('app/toggleDevice', 'mobile') + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_isMobile() { + const rect = body.getBoundingClientRect() + return rect.width - 1 < WIDTH + }, + $_resizeHandler() { + if (!document.hidden) { + const isMobile = this.$_isMobile() + store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') + + if (isMobile) { + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + } + } + } +} diff --git a/ruoyi-ui/src/main.js b/ruoyi-ui/src/main.js new file mode 100644 index 0000000..da01750 --- /dev/null +++ b/ruoyi-ui/src/main.js @@ -0,0 +1,83 @@ +import Vue from 'vue' + +import Cookies from 'js-cookie' + +import Element from 'element-ui' +import './assets/styles/element-variables.scss' + +import '@/assets/styles/index.scss' // global css +import '@/assets/styles/ruoyi.scss' // ruoyi css +import App from './App' +import store from './store' +import router from './router' +import directive from './directive' // directive +import plugins from './plugins' // plugins +import { download } from '@/utils/request' + +import './assets/icons' // icon +import './permission' // permission control +import { getDicts } from "@/api/system/dict/data" +import { getConfigKey } from "@/api/system/config" +import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi" +// 分页组件 +import Pagination from "@/components/Pagination" +// 自定义表格工具组件 +import RightToolbar from "@/components/RightToolbar" +// 富文本组件 +import Editor from "@/components/Editor" +// 文件上传组件 +import FileUpload from "@/components/FileUpload" +// 图片上传组件 +import ImageUpload from "@/components/ImageUpload" +// 图片预览组件 +import ImagePreview from "@/components/ImagePreview" +// 字典标签组件 +import DictTag from '@/components/DictTag' +// 字典数据组件 +import DictData from '@/components/DictData' + +// 全局方法挂载 +Vue.prototype.getDicts = getDicts +Vue.prototype.getConfigKey = getConfigKey +Vue.prototype.parseTime = parseTime +Vue.prototype.resetForm = resetForm +Vue.prototype.addDateRange = addDateRange +Vue.prototype.selectDictLabel = selectDictLabel +Vue.prototype.selectDictLabels = selectDictLabels +Vue.prototype.download = download +Vue.prototype.handleTree = handleTree + +// 全局组件挂载 +Vue.component('DictTag', DictTag) +Vue.component('Pagination', Pagination) +Vue.component('RightToolbar', RightToolbar) +Vue.component('Editor', Editor) +Vue.component('FileUpload', FileUpload) +Vue.component('ImageUpload', ImageUpload) +Vue.component('ImagePreview', ImagePreview) + +Vue.use(directive) +Vue.use(plugins) +DictData.install() + +/** + * If you don't want to use mock-server + * you want to use MockJs for mock api + * you can execute: mockXHR() + * + * Currently MockJs will be used in the production environment, + * please remove it before going online! ! ! + */ + +Vue.use(Element, { + size: Cookies.get('size') || 'medium' // set element-ui default size +}) + +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/ruoyi-ui/src/permission.js b/ruoyi-ui/src/permission.js new file mode 100644 index 0000000..b66190b --- /dev/null +++ b/ruoyi-ui/src/permission.js @@ -0,0 +1,63 @@ +import router from './router' +import store from './store' +import { Message } from 'element-ui' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { getToken } from '@/utils/auth' +import { isPathMatch } from '@/utils/validate' +import { isRelogin } from '@/utils/request' + +NProgress.configure({ showSpinner: false }) + +const whiteList = ['/login', '/register'] + +const isWhiteList = (path) => { + return whiteList.some(pattern => isPathMatch(pattern, path)) +} + +router.beforeEach((to, from, next) => { + NProgress.start() + if (getToken()) { + to.meta.title && store.dispatch('settings/setTitle', to.meta.title) + /* has token*/ + if (to.path === '/login') { + next({ path: '/' }) + NProgress.done() + } else if (isWhiteList(to.path)) { + next() + } else { + if (store.getters.roles.length === 0) { + isRelogin.show = true + // 判断当前用户是否已拉取完user_info信息 + store.dispatch('GetInfo').then(() => { + isRelogin.show = false + store.dispatch('GenerateRoutes').then(accessRoutes => { + // 根据roles权限生成可访问的路由表 + router.addRoutes(accessRoutes) // 动态添加可访问路由表 + next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 + }) + }).catch(err => { + store.dispatch('LogOut').then(() => { + Message.error(err) + next({ path: '/' }) + }) + }) + } else { + next() + } + } + } else { + // 没有token + if (isWhiteList(to.path)) { + // 在免登录白名单,直接进入 + next() + } else { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 + NProgress.done() + } + } +}) + +router.afterEach(() => { + NProgress.done() +}) diff --git a/ruoyi-ui/src/plugins/auth.js b/ruoyi-ui/src/plugins/auth.js new file mode 100644 index 0000000..3b91c14 --- /dev/null +++ b/ruoyi-ui/src/plugins/auth.js @@ -0,0 +1,60 @@ +import store from '@/store' + +function authPermission(permission) { + const all_permission = "*:*:*" + const permissions = store.getters && store.getters.permissions + if (permission && permission.length > 0) { + return permissions.some(v => { + return all_permission === v || v === permission + }) + } else { + return false + } +} + +function authRole(role) { + const super_admin = "admin" + const roles = store.getters && store.getters.roles + if (role && role.length > 0) { + return roles.some(v => { + return super_admin === v || v === role + }) + } else { + return false + } +} + +export default { + // 验证用户是否具备某权限 + hasPermi(permission) { + return authPermission(permission) + }, + // 验证用户是否含有指定权限,只需包含其中一个 + hasPermiOr(permissions) { + return permissions.some(item => { + return authPermission(item) + }) + }, + // 验证用户是否含有指定权限,必须全部拥有 + hasPermiAnd(permissions) { + return permissions.every(item => { + return authPermission(item) + }) + }, + // 验证用户是否具备某角色 + hasRole(role) { + return authRole(role) + }, + // 验证用户是否含有指定角色,只需包含其中一个 + hasRoleOr(roles) { + return roles.some(item => { + return authRole(item) + }) + }, + // 验证用户是否含有指定角色,必须全部拥有 + hasRoleAnd(roles) { + return roles.every(item => { + return authRole(item) + }) + } +} diff --git a/ruoyi-ui/src/plugins/cache.js b/ruoyi-ui/src/plugins/cache.js new file mode 100644 index 0000000..ea8a53f --- /dev/null +++ b/ruoyi-ui/src/plugins/cache.js @@ -0,0 +1,79 @@ +const sessionCache = { + set (key, value) { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(key, value) + } + }, + get (key) { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + return null + }, + remove (key) { + sessionStorage.removeItem(key) + } +} +const localCache = { + set (key, value) { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(key, value) + } + }, + get (key) { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + return null + }, + remove (key) { + localStorage.removeItem(key) + } +} + +export default { + /** + * 会话级缓存 + */ + session: sessionCache, + /** + * 本地缓存 + */ + local: localCache +} diff --git a/ruoyi-ui/src/plugins/download.js b/ruoyi-ui/src/plugins/download.js new file mode 100644 index 0000000..8dc83ea --- /dev/null +++ b/ruoyi-ui/src/plugins/download.js @@ -0,0 +1,79 @@ +import axios from 'axios' +import {Loading, Message} from 'element-ui' +import { saveAs } from 'file-saver' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { blobValidate } from "@/utils/ruoyi" + +const baseURL = process.env.VUE_APP_BASE_API +let downloadLoadingInstance + +export default { + name(name, isDelete = true) { + var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data) + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data) + } + }) + }, + resource(resource) { + var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource) + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data) + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data) + } + }) + }, + zip(url, name) { + var url = baseURL + url + downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }) + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data) + if (isBlob) { + const blob = new Blob([res.data], { type: 'application/zip' }) + this.saveAs(blob, name) + } else { + this.printErrMsg(res.data) + } + downloadLoadingInstance.close() + }).catch((r) => { + console.error(r) + Message.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close() + }) + }, + saveAs(text, name, opts) { + saveAs(text, name, opts) + }, + async printErrMsg(data) { + const resText = await data.text() + const rspObj = JSON.parse(resText) + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + Message.error(errMsg) + } +} + diff --git a/ruoyi-ui/src/plugins/index.js b/ruoyi-ui/src/plugins/index.js new file mode 100644 index 0000000..d000f2d --- /dev/null +++ b/ruoyi-ui/src/plugins/index.js @@ -0,0 +1,20 @@ +import tab from './tab' +import auth from './auth' +import cache from './cache' +import modal from './modal' +import download from './download' + +export default { + install(Vue) { + // 页签操作 + Vue.prototype.$tab = tab + // 认证对象 + Vue.prototype.$auth = auth + // 缓存对象 + Vue.prototype.$cache = cache + // 模态框对象 + Vue.prototype.$modal = modal + // 下载文件 + Vue.prototype.$download = download + } +} diff --git a/ruoyi-ui/src/plugins/modal.js b/ruoyi-ui/src/plugins/modal.js new file mode 100644 index 0000000..92bc1ef --- /dev/null +++ b/ruoyi-ui/src/plugins/modal.js @@ -0,0 +1,83 @@ +import { Message, MessageBox, Notification, Loading } from 'element-ui' + +let loadingInstance + +export default { + // 消息提示 + msg(content) { + Message.info(content) + }, + // 错误消息 + msgError(content) { + Message.error(content) + }, + // 成功消息 + msgSuccess(content) { + Message.success(content) + }, + // 警告消息 + msgWarning(content) { + Message.warning(content) + }, + // 弹出提示 + alert(content) { + MessageBox.alert(content, "系统提示") + }, + // 错误提示 + alertError(content) { + MessageBox.alert(content, "系统提示", { type: 'error' }) + }, + // 成功提示 + alertSuccess(content) { + MessageBox.alert(content, "系统提示", { type: 'success' }) + }, + // 警告提示 + alertWarning(content) { + MessageBox.alert(content, "系统提示", { type: 'warning' }) + }, + // 通知提示 + notify(content) { + Notification.info(content) + }, + // 错误通知 + notifyError(content) { + Notification.error(content) + }, + // 成功通知 + notifySuccess(content) { + Notification.success(content) + }, + // 警告通知 + notifyWarning(content) { + Notification.warning(content) + }, + // 确认窗体 + confirm(content) { + return MessageBox.confirm(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 提交内容 + prompt(content) { + return MessageBox.prompt(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 打开遮罩层 + loading(content) { + loadingInstance = Loading.service({ + lock: true, + text: content, + spinner: "el-icon-loading", + background: "rgba(0, 0, 0, 0.7)", + }) + }, + // 关闭遮罩层 + closeLoading() { + loadingInstance.close() + } +} diff --git a/ruoyi-ui/src/plugins/tab.js b/ruoyi-ui/src/plugins/tab.js new file mode 100644 index 0000000..f21467d --- /dev/null +++ b/ruoyi-ui/src/plugins/tab.js @@ -0,0 +1,71 @@ +import store from '@/store' +import router from '@/router' + +export default { + // 刷新当前tab页签 + refreshPage(obj) { + const { path, query, matched } = router.currentRoute + if (obj === undefined) { + matched.forEach((m) => { + if (m.components && m.components.default && m.components.default.name) { + if (!['Layout', 'ParentView'].includes(m.components.default.name)) { + obj = { name: m.components.default.name, path: path, query: query } + } + } + }) + } + return store.dispatch('tagsView/delCachedView', obj).then(() => { + const { path, query } = obj + router.replace({ + path: '/redirect' + path, + query: query + }) + }) + }, + // 关闭当前tab页签,打开新页签 + closeOpenPage(obj) { + store.dispatch("tagsView/delView", router.currentRoute) + if (obj !== undefined) { + return router.push(obj) + } + }, + // 关闭指定tab页签 + closePage(obj) { + if (obj === undefined) { + return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => { + const latestView = visitedViews.slice(-1)[0] + if (latestView) { + return router.push(latestView.fullPath) + } + return router.push('/') + }) + } + return store.dispatch('tagsView/delView', obj) + }, + // 关闭所有tab页签 + closeAllPage() { + return store.dispatch('tagsView/delAllViews') + }, + // 关闭左侧tab页签 + closeLeftPage(obj) { + return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute) + }, + // 关闭右侧tab页签 + closeRightPage(obj) { + return store.dispatch('tagsView/delRightTags', obj || router.currentRoute) + }, + // 关闭其他tab页签 + closeOtherPage(obj) { + return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute) + }, + // 添加tab页签 + openPage(title, url, params) { + const obj = { path: url, meta: { title: title } } + store.dispatch('tagsView/addView', obj) + return router.push({ path: url, query: params }) + }, + // 修改tab页签 + updatePage(obj) { + return store.dispatch('tagsView/updateVisitedView', obj) + } +} diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js new file mode 100644 index 0000000..bbfd237 --- /dev/null +++ b/ruoyi-ui/src/router/index.js @@ -0,0 +1,183 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/* Layout */ +import Layout from '@/layout' + +/** + * Note: 路由配置项 + * + * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 + * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 + * // 若你想不管路由下面的 children 声明的个数都显示你的根路由 + * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 + * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题 + * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 + * roles: ['admin', 'common'] // 访问路由的角色权限 + * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 + * meta : { + noCache: true // 如果设置为true,则不会被 缓存(默认 false) + title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 + icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg + breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 + activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 + } + */ + +// 公共路由 +export const constantRoutes = [ + { + path: '/redirect', + component: Layout, + hidden: true, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect') + } + ] + }, + { + path: '/login', + component: () => import('@/views/login'), + hidden: true + }, + { + path: '/register', + component: () => import('@/views/register'), + hidden: true + }, + { + path: '/404', + component: () => import('@/views/error/404'), + hidden: true + }, + { + path: '/401', + component: () => import('@/views/error/401'), + hidden: true + }, + { + path: '', + component: Layout, + redirect: 'index', + children: [ + { + path: 'index', + component: () => import('@/views/index'), + name: 'Index', + meta: { title: '首页', icon: 'dashboard', affix: true } + } + ] + }, + { + path: '/user', + component: Layout, + hidden: true, + redirect: 'noredirect', + children: [ + { + path: 'profile', + component: () => import('@/views/system/user/profile/index'), + name: 'Profile', + meta: { title: '个人中心', icon: 'user' } + } + ] + } +] + +// 动态路由,基于用户权限动态去加载 +export const dynamicRoutes = [ + { + path: '/system/user-auth', + component: Layout, + hidden: true, + permissions: ['system:user:edit'], + children: [ + { + path: 'role/:userId(\\d+)', + component: () => import('@/views/system/user/authRole'), + name: 'AuthRole', + meta: { title: '分配角色', activeMenu: '/system/user' } + } + ] + }, + { + path: '/system/role-auth', + component: Layout, + hidden: true, + permissions: ['system:role:edit'], + children: [ + { + path: 'user/:roleId(\\d+)', + component: () => import('@/views/system/role/authUser'), + name: 'AuthUser', + meta: { title: '分配用户', activeMenu: '/system/role' } + } + ] + }, + { + path: '/system/dict-data', + component: Layout, + hidden: true, + permissions: ['system:dict:list'], + children: [ + { + path: 'index/:dictId(\\d+)', + component: () => import('@/views/system/dict/data'), + name: 'Data', + meta: { title: '字典数据', activeMenu: '/system/dict' } + } + ] + }, + { + path: '/monitor/job-log', + component: Layout, + hidden: true, + permissions: ['monitor:job:list'], + children: [ + { + path: 'index/:jobId(\\d+)', + component: () => import('@/views/monitor/job/log'), + name: 'JobLog', + meta: { title: '调度日志', activeMenu: '/monitor/job' } + } + ] + }, + { + path: '/tool/gen-edit', + component: Layout, + hidden: true, + permissions: ['tool:gen:edit'], + children: [ + { + path: 'index/:tableId(\\d+)', + component: () => import('@/views/tool/gen/editTable'), + name: 'GenEdit', + meta: { title: '修改生成配置', activeMenu: '/tool/gen' } + } + ] + } +] + +// 防止连续点击多次路由报错 +let routerPush = Router.prototype.push +let routerReplace = Router.prototype.replace +// push +Router.prototype.push = function push(location) { + return routerPush.call(this, location).catch(err => err) +} +// replace +Router.prototype.replace = function push(location) { + return routerReplace.call(this, location).catch(err => err) +} + +export default new Router({ + mode: 'history', // 去掉url中的# + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes +}) diff --git a/ruoyi-ui/src/settings.js b/ruoyi-ui/src/settings.js new file mode 100644 index 0000000..bb37ecf --- /dev/null +++ b/ruoyi-ui/src/settings.js @@ -0,0 +1,56 @@ +module.exports = { + /** + * 网页标题 + */ + title: process.env.VUE_APP_TITLE, + + /** + * 侧边栏主题 深色主题theme-dark,浅色主题theme-light + */ + sideTheme: 'theme-dark', + + /** + * 系统布局配置 + */ + showSettings: true, + + /** + * 是否显示顶部导航 + */ + topNav: false, + + /** + * 是否显示 tagsView + */ + tagsView: true, + + /** + * 显示页签图标 + */ + tagsIcon: false, + + /** + * 是否固定头部 + */ + fixedHeader: false, + + /** + * 是否显示logo + */ + sidebarLogo: true, + + /** + * 是否显示动态标题 + */ + dynamicTitle: false, + + /** + * 是否显示底部版权 + */ + footerVisible: false, + + /** + * 底部版权文本内容 + */ + footerContent: 'Copyright © 2018-2025 RuoYi. All Rights Reserved.' +} diff --git a/ruoyi-ui/src/store/getters.js b/ruoyi-ui/src/store/getters.js new file mode 100644 index 0000000..3680f95 --- /dev/null +++ b/ruoyi-ui/src/store/getters.js @@ -0,0 +1,21 @@ +const getters = { + sidebar: state => state.app.sidebar, + size: state => state.app.size, + device: state => state.app.device, + dict: state => state.dict.dict, + visitedViews: state => state.tagsView.visitedViews, + cachedViews: state => state.tagsView.cachedViews, + token: state => state.user.token, + avatar: state => state.user.avatar, + id: state => state.user.id, + name: state => state.user.name, + nickName: state => state.user.nickName, + introduction: state => state.user.introduction, + roles: state => state.user.roles, + permissions: state => state.user.permissions, + permission_routes: state => state.permission.routes, + topbarRouters: state => state.permission.topbarRouters, + defaultRoutes: state => state.permission.defaultRoutes, + sidebarRouters: state => state.permission.sidebarRouters +} +export default getters diff --git a/ruoyi-ui/src/store/index.js b/ruoyi-ui/src/store/index.js new file mode 100644 index 0000000..97aaef8 --- /dev/null +++ b/ruoyi-ui/src/store/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import app from './modules/app' +import dict from './modules/dict' +import user from './modules/user' +import tagsView from './modules/tagsView' +import permission from './modules/permission' +import settings from './modules/settings' +import getters from './getters' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + dict, + user, + tagsView, + permission, + settings + }, + getters +}) + +export default store diff --git a/ruoyi-ui/src/store/modules/app.js b/ruoyi-ui/src/store/modules/app.js new file mode 100644 index 0000000..9f99f39 --- /dev/null +++ b/ruoyi-ui/src/store/modules/app.js @@ -0,0 +1,66 @@ +import Cookies from 'js-cookie' + +const state = { + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false, + hide: false + }, + device: 'desktop', + size: Cookies.get('size') || 'medium' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + if (state.sidebar.hide) { + return false + } + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + }, + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 0) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + }, + SET_SIZE: (state, size) => { + state.size = size + Cookies.set('size', size) + }, + SET_SIDEBAR_HIDE: (state, status) => { + state.sidebar.hide = status + } +} + +const actions = { + toggleSideBar({ commit }) { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + }, + setSize({ commit }, size) { + commit('SET_SIZE', size) + }, + toggleSideBarHide({ commit }, status) { + commit('SET_SIDEBAR_HIDE', status) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/ruoyi-ui/src/store/modules/dict.js b/ruoyi-ui/src/store/modules/dict.js new file mode 100644 index 0000000..7a1b2f0 --- /dev/null +++ b/ruoyi-ui/src/store/modules/dict.js @@ -0,0 +1,50 @@ +const state = { + dict: new Array() +} +const mutations = { + SET_DICT: (state, { key, value }) => { + if (key !== null && key !== "") { + state.dict.push({ + key: key, + value: value + }) + } + }, + REMOVE_DICT: (state, key) => { + try { + for (let i = 0; i < state.dict.length; i++) { + if (state.dict[i].key == key) { + state.dict.splice(i, 1) + return true + } + } + } catch (e) { + } + }, + CLEAN_DICT: (state) => { + state.dict = new Array() + } +} + +const actions = { + // 设置字典 + setDict({ commit }, data) { + commit('SET_DICT', data) + }, + // 删除字典 + removeDict({ commit }, key) { + commit('REMOVE_DICT', key) + }, + // 清空字典 + cleanDict({ commit }) { + commit('CLEAN_DICT') + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/ruoyi-ui/src/store/modules/permission.js b/ruoyi-ui/src/store/modules/permission.js new file mode 100644 index 0000000..b549ef0 --- /dev/null +++ b/ruoyi-ui/src/store/modules/permission.js @@ -0,0 +1,122 @@ +import auth from '@/plugins/auth' +import router, { constantRoutes, dynamicRoutes } from '@/router' +import { getRouters } from '@/api/menu' +import Layout from '@/layout/index' +import ParentView from '@/components/ParentView' +import InnerLink from '@/layout/components/InnerLink' + +const permission = { + state: { + routes: [], + addRoutes: [], + defaultRoutes: [], + topbarRouters: [], + sidebarRouters: [] + }, + mutations: { + SET_ROUTES: (state, routes) => { + state.addRoutes = routes + state.routes = constantRoutes.concat(routes) + }, + SET_DEFAULT_ROUTES: (state, routes) => { + state.defaultRoutes = constantRoutes.concat(routes) + }, + SET_TOPBAR_ROUTES: (state, routes) => { + state.topbarRouters = routes + }, + SET_SIDEBAR_ROUTERS: (state, routes) => { + state.sidebarRouters = routes + }, + }, + actions: { + // 生成路由 + GenerateRoutes({ commit }) { + return new Promise(resolve => { + // 向后端请求路由数据 + getRouters().then(res => { + const sdata = JSON.parse(JSON.stringify(res.data)) + const rdata = JSON.parse(JSON.stringify(res.data)) + const sidebarRoutes = filterAsyncRouter(sdata) + const rewriteRoutes = filterAsyncRouter(rdata, false, true) + const asyncRoutes = filterDynamicRoutes(dynamicRoutes) + rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) + router.addRoutes(asyncRoutes) + commit('SET_ROUTES', rewriteRoutes) + commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) + commit('SET_DEFAULT_ROUTES', sidebarRoutes) + commit('SET_TOPBAR_ROUTES', sidebarRoutes) + resolve(rewriteRoutes) + }) + }) + } + } +} + +// 遍历后台传来的路由字符串,转换为组件对象 +function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { + return asyncRouterMap.filter(route => { + if (type && route.children) { + route.children = filterChildren(route.children) + } + if (route.component) { + // Layout ParentView 组件特殊处理 + if (route.component === 'Layout') { + route.component = Layout + } else if (route.component === 'ParentView') { + route.component = ParentView + } else if (route.component === 'InnerLink') { + route.component = InnerLink + } else { + route.component = loadView(route.component) + } + } + if (route.children != null && route.children && route.children.length) { + route.children = filterAsyncRouter(route.children, route, type) + } else { + delete route['children'] + delete route['redirect'] + } + return true + }) +} + +function filterChildren(childrenMap, lastRouter = false) { + var children = [] + childrenMap.forEach(el => { + el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path + if (el.children && el.children.length && el.component === 'ParentView') { + children = children.concat(filterChildren(el.children, el)) + } else { + children.push(el) + } + }) + return children +} + +// 动态路由遍历,验证是否具备权限 +export function filterDynamicRoutes(routes) { + const res = [] + routes.forEach(route => { + if (route.permissions) { + if (auth.hasPermiOr(route.permissions)) { + res.push(route) + } + } else if (route.roles) { + if (auth.hasRoleOr(route.roles)) { + res.push(route) + } + } + }) + return res +} + +export const loadView = (view) => { + if (process.env.NODE_ENV === 'development') { + return (resolve) => require([`@/views/${view}`], resolve) + } else { + // 使用 import 实现生产环境的路由懒加载 + return () => import(`@/views/${view}`) + } +} + +export default permission diff --git a/ruoyi-ui/src/store/modules/settings.js b/ruoyi-ui/src/store/modules/settings.js new file mode 100644 index 0000000..22bfccb --- /dev/null +++ b/ruoyi-ui/src/store/modules/settings.js @@ -0,0 +1,47 @@ +import defaultSettings from '@/settings' +import { useDynamicTitle } from '@/utils/dynamicTitle' + +const { sideTheme, showSettings, topNav, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings + +const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || '' +const state = { + title: '', + theme: storageSetting.theme || '#409EFF', + sideTheme: storageSetting.sideTheme || sideTheme, + showSettings: showSettings, + topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav, + tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, + tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon, + fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, + sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, + dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle, + footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible, + footerContent: footerContent +} +const mutations = { + CHANGE_SETTING: (state, { key, value }) => { + if (state.hasOwnProperty(key)) { + state[key] = value + } + } +} + +const actions = { + // 修改布局设置 + changeSetting({ commit }, data) { + commit('CHANGE_SETTING', data) + }, + // 设置网页标题 + setTitle({ commit }, title) { + state.title = title + useDynamicTitle() + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/ruoyi-ui/src/store/modules/tagsView.js b/ruoyi-ui/src/store/modules/tagsView.js new file mode 100644 index 0000000..5fc011c --- /dev/null +++ b/ruoyi-ui/src/store/modules/tagsView.js @@ -0,0 +1,228 @@ +const state = { + visitedViews: [], + cachedViews: [], + iframeViews: [] +} + +const mutations = { + ADD_IFRAME_VIEW: (state, view) => { + if (state.iframeViews.some(v => v.path === view.path)) return + state.iframeViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_VISITED_VIEW: (state, view) => { + if (state.visitedViews.some(v => v.path === view.path)) return + state.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_CACHED_VIEW: (state, view) => { + if (state.cachedViews.includes(view.name)) return + if (view.meta && !view.meta.noCache) { + state.cachedViews.push(view.name) + } + }, + DEL_VISITED_VIEW: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews.splice(i, 1) + break + } + } + state.iframeViews = state.iframeViews.filter(item => item.path !== view.path) + }, + DEL_IFRAME_VIEW: (state, view) => { + state.iframeViews = state.iframeViews.filter(item => item.path !== view.path) + }, + DEL_CACHED_VIEW: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + index > -1 && state.cachedViews.splice(index, 1) + }, + + DEL_OTHERS_VISITED_VIEWS: (state, view) => { + state.visitedViews = state.visitedViews.filter(v => { + return v.meta.affix || v.path === view.path + }) + state.iframeViews = state.iframeViews.filter(item => item.path === view.path) + }, + DEL_OTHERS_CACHED_VIEWS: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + if (index > -1) { + state.cachedViews = state.cachedViews.slice(index, index + 1) + } else { + state.cachedViews = [] + } + }, + DEL_ALL_VISITED_VIEWS: state => { + // keep affix tags + const affixTags = state.visitedViews.filter(tag => tag.meta.affix) + state.visitedViews = affixTags + state.iframeViews = [] + }, + DEL_ALL_CACHED_VIEWS: state => { + state.cachedViews = [] + }, + UPDATE_VISITED_VIEW: (state, view) => { + for (let v of state.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + }, + DEL_RIGHT_VIEWS: (state, view) => { + const index = state.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + state.visitedViews = state.visitedViews.filter((item, idx) => { + if (idx <= index || (item.meta && item.meta.affix)) { + return true + } + const i = state.cachedViews.indexOf(item.name) + if (i > -1) { + state.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = state.iframeViews.findIndex(v => v.path === item.path) + state.iframeViews.splice(fi, 1) + } + return false + }) + }, + DEL_LEFT_VIEWS: (state, view) => { + const index = state.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + state.visitedViews = state.visitedViews.filter((item, idx) => { + if (idx >= index || (item.meta && item.meta.affix)) { + return true + } + const i = state.cachedViews.indexOf(item.name) + if (i > -1) { + state.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = state.iframeViews.findIndex(v => v.path === item.path) + state.iframeViews.splice(fi, 1) + } + return false + }) + } +} + +const actions = { + addView({ dispatch }, view) { + dispatch('addVisitedView', view) + dispatch('addCachedView', view) + }, + addIframeView({ commit }, view) { + commit('ADD_IFRAME_VIEW', view) + }, + addVisitedView({ commit }, view) { + commit('ADD_VISITED_VIEW', view) + }, + addCachedView({ commit }, view) { + commit('ADD_CACHED_VIEW', view) + }, + delView({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delVisitedView', view) + dispatch('delCachedView', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delVisitedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_VISITED_VIEW', view) + resolve([...state.visitedViews]) + }) + }, + delIframeView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_IFRAME_VIEW', view) + resolve([...state.iframeViews]) + }) + }, + delCachedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_CACHED_VIEW', view) + resolve([...state.cachedViews]) + }) + }, + delOthersViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delOthersVisitedViews', view) + dispatch('delOthersCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delOthersVisitedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_VISITED_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delOthersCachedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_CACHED_VIEWS', view) + resolve([...state.cachedViews]) + }) + }, + delAllViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delAllVisitedViews', view) + dispatch('delAllCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delAllVisitedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_VISITED_VIEWS') + resolve([...state.visitedViews]) + }) + }, + delAllCachedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_CACHED_VIEWS') + resolve([...state.cachedViews]) + }) + }, + updateVisitedView({ commit }, view) { + commit('UPDATE_VISITED_VIEW', view) + }, + delRightTags({ commit }, view) { + return new Promise(resolve => { + commit('DEL_RIGHT_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delLeftTags({ commit }, view) { + return new Promise(resolve => { + commit('DEL_LEFT_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/ruoyi-ui/src/store/modules/user.js b/ruoyi-ui/src/store/modules/user.js new file mode 100644 index 0000000..6a7b710 --- /dev/null +++ b/ruoyi-ui/src/store/modules/user.js @@ -0,0 +1,125 @@ +import router from '@/router' +import { MessageBox, } from 'element-ui' +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' +import { isHttp, isEmpty } from "@/utils/validate" +import defAva from '@/assets/images/profile.jpg' + +const user = { + state: { + token: getToken(), + id: '', + name: '', + nickName: '', + avatar: '', + roles: [], + permissions: [] + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_ID: (state, id) => { + state.id = id + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_NICK_NAME: (state, nickName) => { + state.nickName = nickName + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_PERMISSIONS: (state, permissions) => { + state.permissions = permissions + } + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + login(username, password, code, uuid).then(res => { + setToken(res.token) + commit('SET_TOKEN', res.token) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo().then(res => { + const user = res.user + let avatar = user.avatar || "" + if (!isHttp(avatar)) { + avatar = (isEmpty(avatar)) ? defAva : process.env.VUE_APP_BASE_API + avatar + } + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + commit('SET_ROLES', res.roles) + commit('SET_PERMISSIONS', res.permissions) + } else { + commit('SET_ROLES', ['ROLE_DEFAULT']) + } + commit('SET_ID', user.userId) + commit('SET_NAME', user.userName) + commit('SET_NICK_NAME', user.nickName) + commit('SET_AVATAR', avatar) + /* 初始密码提示 */ + if(res.isDefaultModifyPwd) { + MessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + /* 过期密码提示 */ + if(!res.isDefaultModifyPwd && res.isPasswordExpired) { + MessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 退出系统 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + removeToken() + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 前端 登出 + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_TOKEN', '') + removeToken() + resolve() + }) + } + } +} + +export default user diff --git a/ruoyi-ui/src/utils/auth.js b/ruoyi-ui/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/ruoyi-ui/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'Admin-Token' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/ruoyi-ui/src/utils/dict/Dict.js b/ruoyi-ui/src/utils/dict/Dict.js new file mode 100644 index 0000000..994f6f7 --- /dev/null +++ b/ruoyi-ui/src/utils/dict/Dict.js @@ -0,0 +1,82 @@ +import Vue from 'vue' +import { mergeRecursive } from "@/utils/ruoyi" +import DictMeta from './DictMeta' +import DictData from './DictData' + +const DEFAULT_DICT_OPTIONS = { + types: [], +} + +/** + * @classdesc 字典 + * @property {Object} label 标签对象,内部属性名为字典类型名称 + * @property {Object} dict 字段数组,内部属性名为字典类型名称 + * @property {Array.} _dictMetas 字典元数据数组 + */ +export default class Dict { + constructor() { + this.owner = null + this.label = {} + this.type = {} + } + + init(options) { + if (options instanceof Array) { + options = { types: options } + } + const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options) + if (opts.types === undefined) { + throw new Error('need dict types') + } + const ps = [] + this._dictMetas = opts.types.map(t => DictMeta.parse(t)) + this._dictMetas.forEach(dictMeta => { + const type = dictMeta.type + Vue.set(this.label, type, {}) + Vue.set(this.type, type, []) + if (dictMeta.lazy) { + return + } + ps.push(loadDict(this, dictMeta)) + }) + return Promise.all(ps) + } + + /** + * 重新加载字典 + * @param {String} type 字典类型 + */ + reloadDict(type) { + const dictMeta = this._dictMetas.find(e => e.type === type) + if (dictMeta === undefined) { + return Promise.reject(`the dict meta of ${type} was not found`) + } + return loadDict(this, dictMeta) + } +} + +/** + * 加载字典 + * @param {Dict} dict 字典 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {Promise} + */ +function loadDict(dict, dictMeta) { + return dictMeta.request(dictMeta) + .then(response => { + const type = dictMeta.type + let dicts = dictMeta.responseConverter(response, dictMeta) + if (!(dicts instanceof Array)) { + console.error('the return of responseConverter must be Array.') + dicts = [] + } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) { + console.error('the type of elements in dicts must be DictData') + dicts = [] + } + dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts) + dicts.forEach(d => { + Vue.set(dict.label[type], d.value, d.label) + }) + return dicts + }) +} diff --git a/ruoyi-ui/src/utils/dict/DictConverter.js b/ruoyi-ui/src/utils/dict/DictConverter.js new file mode 100644 index 0000000..0cf5df8 --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictConverter.js @@ -0,0 +1,17 @@ +import DictOptions from './DictOptions' +import DictData from './DictData' + +export default function(dict, dictMeta) { + const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS) + const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS) + return new DictData(dict[label], dict[value], dict) +} + +/** + * 确定字典字段 + * @param {DictData} dict + * @param {...String} fields + */ +function determineDictField(dict, ...fields) { + return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f)) +} diff --git a/ruoyi-ui/src/utils/dict/DictData.js b/ruoyi-ui/src/utils/dict/DictData.js new file mode 100644 index 0000000..afc763e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictData.js @@ -0,0 +1,13 @@ +/** + * @classdesc 字典数据 + * @property {String} label 标签 + * @property {*} value 标签 + * @property {Object} raw 原始数据 + */ +export default class DictData { + constructor(label, value, raw) { + this.label = label + this.value = value + this.raw = raw + } +} diff --git a/ruoyi-ui/src/utils/dict/DictMeta.js b/ruoyi-ui/src/utils/dict/DictMeta.js new file mode 100644 index 0000000..617cc00 --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictMeta.js @@ -0,0 +1,38 @@ +import { mergeRecursive } from "@/utils/ruoyi" +import DictOptions from './DictOptions' + +/** + * @classdesc 字典元数据 + * @property {String} type 类型 + * @property {Function} request 请求 + * @property {String} label 标签字段 + * @property {String} value 值字段 + */ +export default class DictMeta { + constructor(options) { + this.type = options.type + this.request = options.request + this.responseConverter = options.responseConverter + this.labelField = options.labelField + this.valueField = options.valueField + this.lazy = options.lazy === true + } +} + + +/** + * 解析字典元数据 + * @param {Object} options + * @returns {DictMeta} + */ +DictMeta.parse= function(options) { + let opts = null + if (typeof options === 'string') { + opts = DictOptions.metas[options] || {} + opts.type = options + } else if (typeof options === 'object') { + opts = options + } + opts = mergeRecursive(DictOptions.metas['*'], opts) + return new DictMeta(opts) +} diff --git a/ruoyi-ui/src/utils/dict/DictOptions.js b/ruoyi-ui/src/utils/dict/DictOptions.js new file mode 100644 index 0000000..5fd1425 --- /dev/null +++ b/ruoyi-ui/src/utils/dict/DictOptions.js @@ -0,0 +1,51 @@ +import { mergeRecursive } from "@/utils/ruoyi" +import dictConverter from './DictConverter' + +export const options = { + metas: { + '*': { + /** + * 字典请求,方法签名为function(dictMeta: DictMeta): Promise + */ + request: (dictMeta) => { + console.log(`load dict ${dictMeta.type}`) + return Promise.resolve([]) + }, + /** + * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictData + */ + responseConverter, + labelField: 'label', + valueField: 'value', + }, + }, + /** + * 默认标签字段 + */ + DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'], + /** + * 默认值字段 + */ + DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'], +} + +/** + * 映射字典 + * @param {Object} response 字典数据 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {DictData} + */ +function responseConverter(response, dictMeta) { + const dicts = response.content instanceof Array ? response.content : response + if (dicts === undefined) { + console.warn(`no dict data of "${dictMeta.type}" found in the response`) + return [] + } + return dicts.map(d => dictConverter(d, dictMeta)) +} + +export function mergeOptions(src) { + mergeRecursive(options, src) +} + +export default options diff --git a/ruoyi-ui/src/utils/dict/index.js b/ruoyi-ui/src/utils/dict/index.js new file mode 100644 index 0000000..215eb9e --- /dev/null +++ b/ruoyi-ui/src/utils/dict/index.js @@ -0,0 +1,33 @@ +import Dict from './Dict' +import { mergeOptions } from './DictOptions' + +export default function(Vue, options) { + mergeOptions(options) + Vue.mixin({ + data() { + if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) { + return {} + } + const dict = new Dict() + dict.owner = this + return { + dict + } + }, + created() { + if (!(this.dict instanceof Dict)) { + return + } + options.onCreated && options.onCreated(this.dict) + this.dict.init(this.$options.dicts).then(() => { + options.onReady && options.onReady(this.dict) + this.$nextTick(() => { + this.$emit('dictReady', this.dict) + if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) { + this.$options.methods.onDictReady.call(this, this.dict) + } + }) + }) + }, + }) +} diff --git a/ruoyi-ui/src/utils/dynamicTitle.js b/ruoyi-ui/src/utils/dynamicTitle.js new file mode 100644 index 0000000..1b57efb --- /dev/null +++ b/ruoyi-ui/src/utils/dynamicTitle.js @@ -0,0 +1,13 @@ +import store from '@/store' +import defaultSettings from '@/settings' + +/** + * 动态修改标题 + */ +export function useDynamicTitle() { + if (store.state.settings.dynamicTitle) { + document.title = store.state.settings.title + ' - ' + defaultSettings.title + } else { + document.title = defaultSettings.title + } +} \ No newline at end of file diff --git a/ruoyi-ui/src/utils/errorCode.js b/ruoyi-ui/src/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/ruoyi-ui/src/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/ruoyi-ui/src/utils/excel.js b/ruoyi-ui/src/utils/excel.js new file mode 100644 index 0000000..167c55b --- /dev/null +++ b/ruoyi-ui/src/utils/excel.js @@ -0,0 +1,100 @@ +/** + * Excel工具类 - 用于导入导出Excel文件 + */ +import axios from 'axios' +import { saveAs } from 'file-saver' +import * as XLSX from 'xlsx' +import { getToken } from '@/utils/auth' + +/** + * 解析Excel文件 + * @param {String} url - Excel文件URL + * @returns {Promise} - 解析后的数据数组 + */ +export function parseExcel(url) { + return new Promise((resolve, reject) => { + axios({ + method: 'get', + url: process.env.VUE_APP_BASE_API + url, + responseType: 'arraybuffer', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then(response => { + try { + // 使用xlsx库解析Excel数据 + const data = new Uint8Array(response.data) + const workbook = XLSX.read(data, { type: 'array' }) + const firstSheetName = workbook.SheetNames[0] + const worksheet = workbook.Sheets[firstSheetName] + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) + resolve(jsonData) + } catch (error) { + reject('解析Excel失败: ' + error.message) + } + }).catch(error => { + reject('下载Excel失败: ' + error.message) + }) + }) +} + +/** + * 从本地文件直接读取Excel数据 + * @param {File} file - 上传的Excel文件对象 + * @returns {Promise} - 解析后的数据数组 + */ +export function parseExcelFile(file) { + return new Promise((resolve, reject) => { + try { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = new Uint8Array(e.target.result); + const workbook = XLSX.read(data, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + resolve(jsonData); + } catch (error) { + reject('解析Excel文件失败: ' + error.message); + } + }; + reader.onerror = (error) => reject('读取文件失败: ' + error); + reader.readAsArrayBuffer(file); + } catch (error) { + reject('处理文件失败: ' + error.message); + } + }); +} + +/** + * 导出数据到Excel文件 + * @param {Array} data - 要导出的数据数组 + * @param {String} filename - 文件名(不含扩展名) + * @param {String} ext - 文件扩展名,默认为.xlsx + */ +export function exportExcel(data, filename, ext = '.xlsx') { + const worksheet = XLSX.utils.aoa_to_sheet(data) + const workbook = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1') + + // 确定文件类型 + let bookType = 'xlsx'; + let mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + + // 根据扩展名确定文件类型 + if (ext === '.xls') { + bookType = 'xls'; + mimeType = 'application/vnd.ms-excel'; + } else if (ext === '.et') { + // WPS表格格式,实际上仍使用xlsx格式但改变扩展名 + bookType = 'xlsx'; + mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + + // 生成Excel文件并下载 + const excelBuffer = XLSX.write(workbook, { bookType: bookType, type: 'array' }) + const blob = new Blob([excelBuffer], { type: mimeType }) + + // 确保扩展名正确 + const fileExt = ext.startsWith('.') ? ext : `.${ext}`; + saveAs(blob, filename + fileExt) +} \ No newline at end of file diff --git a/ruoyi-ui/src/utils/generator/config.js b/ruoyi-ui/src/utils/generator/config.js new file mode 100644 index 0000000..7abf227 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/config.js @@ -0,0 +1,438 @@ +export const formConf = { + formRef: 'elForm', + formModel: 'formData', + size: 'medium', + labelPosition: 'right', + labelWidth: 100, + formRules: 'rules', + gutter: 15, + disabled: false, + span: 24, + formBtns: true +} + +export const inputComponents = [ + { + label: '单行文本', + tag: 'el-input', + tagIcon: 'input', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': '', + 'suffix-icon': '', + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '多行文本', + tag: 'el-input', + tagIcon: 'textarea', + type: 'textarea', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + labelWidth: null, + autosize: { + minRows: 4, + maxRows: 4 + }, + style: { width: '100%' }, + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '密码', + tag: 'el-input', + tagIcon: 'password', + placeholder: '请输入', + defaultValue: undefined, + span: 24, + 'show-password': true, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': '', + 'suffix-icon': '', + maxlength: null, + 'show-word-limit': false, + readonly: false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input' + }, + { + label: '计数器', + tag: 'el-input-number', + tagIcon: 'number', + placeholder: '', + defaultValue: undefined, + span: 24, + labelWidth: null, + min: undefined, + max: undefined, + step: undefined, + 'step-strictly': false, + precision: undefined, + 'controls-position': '', + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/input-number' + } +] + +export const selectComponents = [ + { + label: '下拉选择', + tag: 'el-select', + tagIcon: 'select', + placeholder: '请选择', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: { width: '100%' }, + clearable: true, + disabled: false, + required: true, + filterable: false, + multiple: false, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/select' + }, + { + label: '级联选择', + tag: 'el-cascader', + tagIcon: 'cascader', + placeholder: '请选择', + defaultValue: [], + span: 24, + labelWidth: null, + style: { width: '100%' }, + props: { + props: { + multiple: false + } + }, + 'show-all-levels': true, + disabled: false, + clearable: true, + filterable: false, + required: true, + options: [{ + id: 1, + value: 1, + label: '选项1', + children: [{ + id: 2, + value: 2, + label: '选项1-1' + }] + }], + dataType: 'dynamic', + labelKey: 'label', + valueKey: 'value', + childrenKey: 'children', + separator: '/', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/cascader' + }, + { + label: '单选框组', + tag: 'el-radio-group', + tagIcon: 'radio', + defaultValue: undefined, + span: 24, + labelWidth: null, + style: {}, + optionType: 'default', + border: false, + size: 'medium', + disabled: false, + required: true, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/radio' + }, + { + label: '多选框组', + tag: 'el-checkbox-group', + tagIcon: 'checkbox', + defaultValue: [], + span: 24, + labelWidth: null, + style: {}, + optionType: 'default', + border: false, + size: 'medium', + disabled: false, + required: true, + options: [{ + label: '选项一', + value: 1 + }, { + label: '选项二', + value: 2 + }], + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/checkbox' + }, + { + label: '开关', + tag: 'el-switch', + tagIcon: 'switch', + defaultValue: false, + span: 24, + labelWidth: null, + style: {}, + disabled: false, + required: true, + 'active-text': '', + 'inactive-text': '', + 'active-color': null, + 'inactive-color': null, + 'active-value': true, + 'inactive-value': false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/switch' + }, + { + label: '滑块', + tag: 'el-slider', + tagIcon: 'slider', + defaultValue: null, + span: 24, + labelWidth: null, + disabled: false, + required: true, + min: 0, + max: 100, + step: 1, + 'show-stops': false, + range: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/slider' + }, + { + label: '时间选择', + tag: 'el-time-picker', + tagIcon: 'time', + placeholder: '请选择', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + 'picker-options': { + selectableRange: '00:00:00-23:59:59' + }, + format: 'HH:mm:ss', + 'value-format': 'HH:mm:ss', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/time-picker' + }, + { + label: '时间范围', + tag: 'el-time-picker', + tagIcon: 'time-range', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + 'is-range': true, + 'range-separator': '至', + 'start-placeholder': '开始时间', + 'end-placeholder': '结束时间', + format: 'HH:mm:ss', + 'value-format': 'HH:mm:ss', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/time-picker' + }, + { + label: '日期选择', + tag: 'el-date-picker', + tagIcon: 'date', + placeholder: '请选择', + defaultValue: null, + type: 'date', + span: 24, + labelWidth: null, + style: { width: '100%' }, + disabled: false, + clearable: true, + required: true, + format: 'yyyy-MM-dd', + 'value-format': 'yyyy-MM-dd', + readonly: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/date-picker' + }, + { + label: '日期范围', + tag: 'el-date-picker', + tagIcon: 'date-range', + defaultValue: null, + span: 24, + labelWidth: null, + style: { width: '100%' }, + type: 'daterange', + 'range-separator': '至', + 'start-placeholder': '开始日期', + 'end-placeholder': '结束日期', + disabled: false, + clearable: true, + required: true, + format: 'yyyy-MM-dd', + 'value-format': 'yyyy-MM-dd', + readonly: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/date-picker' + }, + { + label: '评分', + tag: 'el-rate', + tagIcon: 'rate', + defaultValue: 0, + span: 24, + labelWidth: null, + style: {}, + max: 5, + 'allow-half': false, + 'show-text': false, + 'show-score': false, + disabled: false, + required: true, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/rate' + }, + { + label: '颜色选择', + tag: 'el-color-picker', + tagIcon: 'color', + defaultValue: null, + labelWidth: null, + 'show-alpha': false, + 'color-format': '', + disabled: false, + required: true, + size: 'medium', + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/color-picker' + }, + { + label: '上传', + tag: 'el-upload', + tagIcon: 'upload', + action: 'https://jsonplaceholder.typicode.com/posts/', + defaultValue: null, + labelWidth: null, + disabled: false, + required: true, + accept: '', + name: 'file', + 'auto-upload': true, + showTip: false, + buttonText: '点击上传', + fileSize: 2, + sizeUnit: 'MB', + 'list-type': 'text', + multiple: false, + regList: [], + changeTag: true, + document: 'https://element.eleme.cn/#/zh-CN/component/upload' + } +] + +export const layoutComponents = [ + { + layout: 'rowFormItem', + tagIcon: 'row', + type: 'default', + justify: 'start', + align: 'top', + label: '行容器', + layoutTree: true, + children: [], + document: 'https://element.eleme.cn/#/zh-CN/component/layout' + }, + { + layout: 'colFormItem', + label: '按钮', + changeTag: true, + labelWidth: null, + tag: 'el-button', + tagIcon: 'button', + span: 24, + default: '主要按钮', + type: 'primary', + icon: 'el-icon-search', + size: 'medium', + disabled: false, + document: 'https://element.eleme.cn/#/zh-CN/component/button' + } +] + +// 组件rule的触发方式,无触发方式的组件不生成rule +export const trigger = { + 'el-input': 'blur', + 'el-input-number': 'blur', + 'el-select': 'change', + 'el-radio-group': 'change', + 'el-checkbox-group': 'change', + 'el-cascader': 'change', + 'el-time-picker': 'change', + 'el-date-picker': 'change', + 'el-rate': 'change' +} diff --git a/ruoyi-ui/src/utils/generator/css.js b/ruoyi-ui/src/utils/generator/css.js new file mode 100644 index 0000000..c1c62e6 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/css.js @@ -0,0 +1,18 @@ +const styles = { + 'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}', + 'el-upload': '.el-upload__tip{line-height: 1.2;}' +} + +function addCss(cssList, el) { + const css = styles[el.tag] + css && cssList.indexOf(css) === -1 && cssList.push(css) + if (el.children) { + el.children.forEach(el2 => addCss(cssList, el2)) + } +} + +export function makeUpCss(conf) { + const cssList = [] + conf.fields.forEach(el => addCss(cssList, el)) + return cssList.join('\n') +} diff --git a/ruoyi-ui/src/utils/generator/drawingDefault.js b/ruoyi-ui/src/utils/generator/drawingDefault.js new file mode 100644 index 0000000..09f133c --- /dev/null +++ b/ruoyi-ui/src/utils/generator/drawingDefault.js @@ -0,0 +1,29 @@ +export default [ + { + layout: 'colFormItem', + tagIcon: 'input', + label: '手机号', + vModel: 'mobile', + formId: 6, + tag: 'el-input', + placeholder: '请输入手机号', + defaultValue: '', + span: 24, + style: { width: '100%' }, + clearable: true, + prepend: '', + append: '', + 'prefix-icon': 'el-icon-mobile', + 'suffix-icon': '', + maxlength: 11, + 'show-word-limit': true, + readonly: false, + disabled: false, + required: true, + changeTag: true, + regList: [{ + pattern: '/^1(3|4|5|7|8|9)\\d{9}$/', + message: '手机号格式错误' + }] + } +] diff --git a/ruoyi-ui/src/utils/generator/html.js b/ruoyi-ui/src/utils/generator/html.js new file mode 100644 index 0000000..3b3388d --- /dev/null +++ b/ruoyi-ui/src/utils/generator/html.js @@ -0,0 +1,358 @@ +import { trigger } from './config' + +let confGlobal +let someSpanIsNot24 + +export function dialogWrapper(str) { + return ` + ${str} +
+ 取消 + 确定 +
+
` +} + +export function vueTemplate(str) { + return `` +} + +export function vueScript(str) { + return `` +} + +export function cssStyle(cssStr) { + return `` +} + +function buildFormTemplate(conf, child, type) { + let labelPosition = '' + if (conf.labelPosition !== 'right') { + labelPosition = `label-position="${conf.labelPosition}"` + } + const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : '' + let str = ` + ${child} + ${buildFromBtns(conf, type)} + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + return str +} + +function buildFromBtns(conf, type) { + let str = '' + if (conf.formBtns && type === 'file') { + str = ` + 提交 + 重置 + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + } + return str +} + +// span不为24的用el-col包裹 +function colWrapper(element, str) { + if (someSpanIsNot24 || element.span !== 24) { + return ` + ${str} + ` + } + return str +} + +const layouts = { + colFormItem(element) { + let labelWidth = '' + if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) { + labelWidth = `label-width="${element.labelWidth}px"` + } + const required = !trigger[element.tag] && element.required ? 'required' : '' + const tagDom = tags[element.tag] ? tags[element.tag](element) : null + let str = ` + ${tagDom} + ` + str = colWrapper(element, str) + return str + }, + rowFormItem(element) { + const type = element.type === 'default' ? '' : `type="${element.type}"` + const justify = element.type === 'default' ? '' : `justify="${element.justify}"` + const align = element.type === 'default' ? '' : `align="${element.align}"` + const gutter = element.gutter ? `gutter="${element.gutter}"` : '' + const children = element.children.map(el => layouts[el.layout](el)) + let str = ` + ${children.join('\n')} + ` + str = colWrapper(element, str) + return str + } +} + +const tags = { + 'el-button': el => { + const { + tag, disabled + } = attrBuilder(el) + const type = el.type ? `type="${el.type}"` : '' + const icon = el.icon ? `icon="${el.icon}"` : '' + const size = el.size ? `size="${el.size}"` : '' + let child = buildElButtonChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}` + }, + 'el-input': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : '' + const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : '' + const readonly = el.readonly ? 'readonly' : '' + const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : '' + const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : '' + const showPassword = el['show-password'] ? 'show-password' : '' + const type = el.type ? `type="${el.type}"` : '' + const autosize = el.autosize && el.autosize.minRows + ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"` + : '' + let child = buildElInputChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}` + }, + 'el-input-number': el => { + const { disabled, vModel, placeholder } = attrBuilder(el) + const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : '' + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const stepStrictly = el['step-strictly'] ? 'step-strictly' : '' + const precision = el.precision ? `:precision='${el.precision}'` : '' + + return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}>` + }, + 'el-select': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const filterable = el.filterable ? 'filterable' : '' + const multiple = el.multiple ? 'multiple' : '' + let child = buildElSelectChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}` + }, + 'el-radio-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + let child = buildElRadioGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${size} ${disabled}>${child}` + }, + 'el-checkbox-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const min = el.min ? `:min="${el.min}"` : '' + const max = el.max ? `:max="${el.max}"` : '' + let child = buildElCheckboxGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}` + }, + 'el-switch': el => { + const { disabled, vModel } = attrBuilder(el) + const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : '' + const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : '' + const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : '' + const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : '' + const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : '' + const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : '' + + return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}>` + }, + 'el-cascader': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const options = el.options ? `:options="${el.vModel}Options"` : '' + const props = el.props ? `:props="${el.vModel}Props"` : '' + const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"' + const filterable = el.filterable ? 'filterable' : '' + const separator = el.separator === '/' ? '' : `separator="${el.separator}"` + + return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}>` + }, + 'el-slider': el => { + const { disabled, vModel } = attrBuilder(el) + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const range = el.range ? 'range' : '' + const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : '' + + return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}>` + }, + 'el-time-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const isRange = el['is-range'] ? 'is-range' : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : '' + + return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}>` + }, + 'el-date-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const type = el.type === 'date' ? '' : `type="${el.type}"` + const readonly = el.readonly ? 'readonly' : '' + + return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}>` + }, + 'el-rate': el => { + const { disabled, vModel } = attrBuilder(el) + const max = el.max ? `:max='${el.max}'` : '' + const allowHalf = el['allow-half'] ? 'allow-half' : '' + const showText = el['show-text'] ? 'show-text' : '' + const showScore = el['show-score'] ? 'show-score' : '' + + return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}>` + }, + 'el-color-picker': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const showAlpha = el['show-alpha'] ? 'show-alpha' : '' + const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : '' + + return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}>` + }, + 'el-upload': el => { + const disabled = el.disabled ? ':disabled=\'true\'' : '' + const action = el.action ? `:action="${el.vModel}Action"` : '' + const multiple = el.multiple ? 'multiple' : '' + const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : '' + const accept = el.accept ? `accept="${el.accept}"` : '' + const name = el.name !== 'file' ? `name="${el.name}"` : '' + const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : '' + const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"` + const fileList = `:file-list="${el.vModel}fileList"` + const ref = `ref="${el.vModel}"` + let child = buildElUploadChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}` + } +} + +function attrBuilder(el) { + return { + vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`, + clearable: el.clearable ? 'clearable' : '', + placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '', + width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '', + disabled: el.disabled ? ':disabled=\'true\'' : '' + } +} + +// el-buttin 子级 +function buildElButtonChild(conf) { + const children = [] + if (conf.default) { + children.push(conf.default) + } + return children.join('\n') +} + +// el-input innerHTML +function buildElInputChild(conf) { + const children = [] + if (conf.prepend) { + children.push(``) + } + if (conf.append) { + children.push(``) + } + return children.join('\n') +} + +function buildElSelectChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + children.push(``) + } + return children.join('\n') +} + +function buildElRadioGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElCheckboxGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElUploadChild(conf) { + const list = [] + if (conf['list-type'] === 'picture-card') list.push('') + else list.push(`${conf.buttonText}`) + if (conf.showTip) list.push(`
只能上传不超过 ${conf.fileSize}${conf.sizeUnit} 的${conf.accept}文件
`) + return list.join('\n') +} + +export function makeUpHtml(conf, type) { + const htmlList = [] + confGlobal = conf + someSpanIsNot24 = conf.fields.some(item => item.span !== 24) + conf.fields.forEach(el => { + htmlList.push(layouts[el.layout](el)) + }) + const htmlStr = htmlList.join('\n') + + let temp = buildFormTemplate(conf, htmlStr, type) + if (type === 'dialog') { + temp = dialogWrapper(temp) + } + confGlobal = null + return temp +} diff --git a/ruoyi-ui/src/utils/generator/icon.json b/ruoyi-ui/src/utils/generator/icon.json new file mode 100644 index 0000000..2d9999a --- /dev/null +++ b/ruoyi-ui/src/utils/generator/icon.json @@ -0,0 +1 @@ +["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"] \ No newline at end of file diff --git a/ruoyi-ui/src/utils/generator/js.js b/ruoyi-ui/src/utils/generator/js.js new file mode 100644 index 0000000..ee8668d --- /dev/null +++ b/ruoyi-ui/src/utils/generator/js.js @@ -0,0 +1,235 @@ +import { exportDefault, titleCase } from '@/utils/index' +import { trigger } from './config' + +const units = { + KB: '1024', + MB: '1024 / 1024', + GB: '1024 / 1024 / 1024' +} +let confGlobal +const inheritAttrs = { + file: '', + dialog: 'inheritAttrs: false,' +} + + +export function makeUpJs(conf, type) { + confGlobal = conf = JSON.parse(JSON.stringify(conf)) + const dataList = [] + const ruleList = [] + const optionsList = [] + const propsList = [] + const methodList = mixinMethod(type) + const uploadVarList = [] + + conf.fields.forEach(el => { + buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) + }) + + const script = buildexport( + conf, + type, + dataList.join('\n'), + ruleList.join('\n'), + optionsList.join('\n'), + uploadVarList.join('\n'), + propsList.join('\n'), + methodList.join('\n') + ) + confGlobal = null + return script +} + +function buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) { + buildData(el, dataList) + buildRules(el, ruleList) + + if (el.options && el.options.length) { + buildOptions(el, optionsList) + if (el.dataType === 'dynamic') { + const model = `${el.vModel}Options` + const options = titleCase(model) + buildOptionMethod(`get${options}`, model, methodList) + } + } + + if (el.props && el.props.props) { + buildProps(el, propsList) + } + + if (el.action && el.tag === 'el-upload') { + uploadVarList.push( + `${el.vModel}Action: '${el.action}', + ${el.vModel}fileList: [],` + ) + methodList.push(buildBeforeUpload(el)) + if (!el['auto-upload']) { + methodList.push(buildSubmitUpload(el)) + } + } + + if (el.children) { + el.children.forEach(el2 => { + buildAttributes(el2, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) + }) + } +} + +function mixinMethod(type) { + const list = []; const + minxins = { + file: confGlobal.formBtns ? { + submitForm: `submitForm() { + this.$refs['${confGlobal.formRef}'].validate(valid => { + if(!valid) return + // TODO 提交表单 + }) + },`, + resetForm: `resetForm() { + this.$refs['${confGlobal.formRef}'].resetFields() + },` + } : null, + dialog: { + onOpen: 'onOpen() {},', + onClose: `onClose() { + this.$refs['${confGlobal.formRef}'].resetFields() + },`, + close: `close() { + this.$emit('update:visible', false) + },`, + handleConfirm: `handleConfirm() { + this.$refs['${confGlobal.formRef}'].validate(valid => { + if(!valid) return + this.close() + }) + },` + } + } + + const methods = minxins[type] + if (methods) { + Object.keys(methods).forEach(key => { + list.push(methods[key]) + }) + } + + return list +} + +function buildData(conf, dataList) { + if (conf.vModel === undefined) return + let defaultValue + if (typeof (conf.defaultValue) === 'string' && !conf.multiple) { + defaultValue = `'${conf.defaultValue}'` + } else { + defaultValue = `${JSON.stringify(conf.defaultValue)}` + } + dataList.push(`${conf.vModel}: ${defaultValue},`) +} + +function buildRules(conf, ruleList) { + if (conf.vModel === undefined) return + const rules = [] + if (trigger[conf.tag]) { + if (conf.required) { + const type = Array.isArray(conf.defaultValue) ? 'type: \'array\',' : '' + let message = Array.isArray(conf.defaultValue) ? `请至少选择一个${conf.vModel}` : conf.placeholder + if (message === undefined) message = `${conf.label}不能为空` + rules.push(`{ required: true, ${type} message: '${message}', trigger: '${trigger[conf.tag]}' }`) + } + if (conf.regList && Array.isArray(conf.regList)) { + conf.regList.forEach(item => { + if (item.pattern) { + rules.push(`{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${trigger[conf.tag]}' }`) + } + }) + } + ruleList.push(`${conf.vModel}: [${rules.join(',')}],`) + } +} + +function buildOptions(conf, optionsList) { + if (conf.vModel === undefined) return + if (conf.dataType === 'dynamic') { conf.options = [] } + const str = `${conf.vModel}Options: ${JSON.stringify(conf.options)},` + optionsList.push(str) +} + +function buildProps(conf, propsList) { + if (conf.dataType === 'dynamic') { + conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey) + conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey) + conf.childrenKey !== 'children' && (conf.props.props.children = conf.childrenKey) + } + const str = `${conf.vModel}Props: ${JSON.stringify(conf.props.props)},` + propsList.push(str) +} + +function buildBeforeUpload(conf) { + const unitNum = units[conf.sizeUnit]; let rightSizeCode = ''; let acceptCode = ''; const + returnList = [] + if (conf.fileSize) { + rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize} + if(!isRightSize){ + this.$message.error('文件大小超过 ${conf.fileSize}${conf.sizeUnit}') + }` + returnList.push('isRightSize') + } + if (conf.accept) { + acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type) + if(!isAccept){ + this.$message.error('应该选择${conf.accept}类型的文件') + }` + returnList.push('isAccept') + } + const str = `${conf.vModel}BeforeUpload(file) { + ${rightSizeCode} + ${acceptCode} + return ${returnList.join('&&')} + },` + return returnList.length ? str : '' +} + +function buildSubmitUpload(conf) { + const str = `submitUpload() { + this.$refs['${conf.vModel}'].submit() + },` + return str +} + +function buildOptionMethod(methodName, model, methodList) { + const str = `${methodName}() { + // TODO 发起请求获取数据 + this.${model} + },` + methodList.push(str) +} + +function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods) { + const str = `${exportDefault}{ + ${inheritAttrs[type]} + components: {}, + props: [], + data () { + return { + ${conf.formModel}: { + ${data} + }, + ${conf.formRules}: { + ${rules} + }, + ${uploadVar} + ${selectOptions} + ${props} + } + }, + computed: {}, + watch: {}, + created () {}, + mounted () {}, + methods: { + ${methods} + } +}` + return str +} diff --git a/ruoyi-ui/src/utils/generator/render.js b/ruoyi-ui/src/utils/generator/render.js new file mode 100644 index 0000000..e8640f0 --- /dev/null +++ b/ruoyi-ui/src/utils/generator/render.js @@ -0,0 +1,126 @@ +import { makeMap } from '@/utils/index' + +// 参考https://github.com/vuejs/vue/blob/v2.6.10/src/platforms/web/server/util.js +const isAttr = makeMap( + 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' + + 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' + + 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' + + 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' + + 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' + + 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' + + 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' + + 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' + + 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' + + 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' + + 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' + + 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' + + 'target,title,type,usemap,value,width,wrap' +) + +function vModel(self, dataObject, defaultValue) { + dataObject.props.value = defaultValue + + dataObject.on.input = val => { + self.$emit('input', val) + } +} + +const componentChild = { + 'el-button': { + default(h, conf, key) { + return conf[key] + }, + }, + 'el-input': { + prepend(h, conf, key) { + return + }, + append(h, conf, key) { + return + } + }, + 'el-select': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + list.push() + }) + return list + } + }, + 'el-radio-group': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + if (conf.optionType === 'button') list.push({item.label}) + else list.push({item.label}) + }) + return list + } + }, + 'el-checkbox-group': { + options(h, conf, key) { + const list = [] + conf.options.forEach(item => { + if (conf.optionType === 'button') { + list.push({item.label}) + } else { + list.push({item.label}) + } + }) + return list + } + }, + 'el-upload': { + 'list-type': (h, conf, key) => { + const list = [] + if (conf['list-type'] === 'picture-card') { + list.push() + } else { + list.push({conf.buttonText}) + } + if (conf.showTip) { + list.push(
只能上传不超过 {conf.fileSize}{conf.sizeUnit} 的{conf.accept}文件
) + } + return list + } + } +} + +export default { + render(h) { + const dataObject = { + attrs: {}, + props: {}, + on: {}, + style: {} + } + const confClone = JSON.parse(JSON.stringify(this.conf)) + const children = [] + + const childObjs = componentChild[confClone.tag] + if (childObjs) { + Object.keys(childObjs).forEach(key => { + const childFunc = childObjs[key] + if (confClone[key]) { + children.push(childFunc(h, confClone, key)) + } + }) + } + + Object.keys(confClone).forEach(key => { + const val = confClone[key] + if (key === 'vModel') { + vModel(this, dataObject, confClone.defaultValue) + } else if (dataObject[key]) { + dataObject[key] = val + } else if (!isAttr(key)) { + dataObject.props[key] = val + } else { + dataObject.attrs[key] = val + } + }) + return h(this.conf.tag, dataObject, children) + }, + props: ['conf'] +} diff --git a/ruoyi-ui/src/utils/index.js b/ruoyi-ui/src/utils/index.js new file mode 100644 index 0000000..9375db7 --- /dev/null +++ b/ruoyi-ui/src/utils/index.js @@ -0,0 +1,390 @@ +import { parseTime } from './ruoyi' + +/** + * 表格时间格式化 + */ +export function formatDate(cellValue) { + if (cellValue == null || cellValue == "") return "" + var date = new Date(cellValue) + var year = date.getFullYear() + var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() + var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function getQueryObject(url) { + url = url == null ? window.location.href : url + const search = url.substring(url.lastIndexOf('?') + 1) + const obj = {} + const reg = /([^?&=]+)=([^?&=]*)/g + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1) + let val = decodeURIComponent($2) + val = String(val) + obj[name] = val + return rs + }) + return obj +} + +/** + * @param {string} input value + * @returns {number} output value + */ +export function byteLength(str) { + // returns the byte length of an utf8 string + let s = str.length + for (var i = str.length - 1; i >= 0; i--) { + const code = str.charCodeAt(i) + if (code > 0x7f && code <= 0x7ff) s++ + else if (code > 0x7ff && code <= 0xffff) s += 2 + if (code >= 0xDC00 && code <= 0xDFFF) i-- + } + return s +} + +/** + * @param {Array} actual + * @returns {Array} + */ +export function cleanArray(actual) { + const newArray = [] + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]) + } + } + return newArray +} + +/** + * @param {Object} json + * @returns {Array} + */ +export function param(json) { + if (!json) return '' + return cleanArray( + Object.keys(json).map(key => { + if (json[key] === undefined) return '' + return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]) + }) + ).join('&') +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +/** + * @param {string} val + * @returns {string} + */ +export function html2Text(val) { + const div = document.createElement('div') + div.innerHTML = val + return div.textContent || div.innerText +} + +/** + * Merges two objects, giving the last one precedence + * @param {Object} target + * @param {(Object|Array)} source + * @returns {Object} + */ +export function objectMerge(target, source) { + if (typeof target !== 'object') { + target = {} + } + if (Array.isArray(source)) { + return source.slice() + } + Object.keys(source).forEach(property => { + const sourceProperty = source[property] + if (typeof sourceProperty === 'object') { + target[property] = objectMerge(target[property], sourceProperty) + } else { + target[property] = sourceProperty + } + }) + return target +} + +/** + * @param {HTMLElement} element + * @param {string} className + */ +export function toggleClass(element, className) { + if (!element || !className) { + return + } + let classString = element.className + const nameIndex = classString.indexOf(className) + if (nameIndex === -1) { + classString += '' + className + } else { + classString = + classString.substr(0, nameIndex) + + classString.substr(nameIndex + className.length) + } + element.className = classString +} + +/** + * @param {string} type + * @returns {Date} + */ +export function getTime(type) { + if (type === 'start') { + return new Date().getTime() - 3600 * 1000 * 24 * 90 + } else { + return new Date(new Date().toDateString()) + } +} + +/** + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + * @return {*} + */ +export function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result + + const later = function() { + // 据上一次触发时间间隔 + const last = +new Date() - timestamp + + // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 + if (!immediate) { + result = func.apply(context, args) + if (!timeout) context = args = null + } + } + } + + return function(...args) { + context = this + timestamp = +new Date() + const callNow = immediate && !timeout + // 如果延时不存在,重新设定延时 + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } +} + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} + */ +export function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(keys => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} + +/** + * @param {Array} arr + * @returns {Array} + */ +export function uniqueArr(arr) { + return Array.from(new Set(arr)) +} + +/** + * @returns {string} + */ +export function createUniqueString() { + const timestamp = +new Date() + '' + const randomNum = parseInt((1 + Math.random()) * 65536) + '' + return (+(randomNum + timestamp)).toString(32) +} + +/** + * Check if an element has a class + * @param {HTMLElement} elm + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele, cls) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function addClass(ele, cls) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function removeClass(ele, cls) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} + +export function makeMap(str, expectsLowerCase) { + const map = Object.create(null) + const list = str.split(',') + for (let i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase + ? val => map[val.toLowerCase()] + : val => map[val] +} + +export const exportDefault = 'export default ' + +export const beautifierConf = { + html: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'separate', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: false, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + }, + js: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'normal', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: true, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + } +} + +// 首字母大小 +export function titleCase(str) { + return str.replace(/( |^)[a-z]/g, L => L.toUpperCase()) +} + +// 下划转驼峰 +export function camelCase(str) { + return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase()) +} + +export function isNumberStr(str) { + return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str) +} + diff --git a/ruoyi-ui/src/utils/jsencrypt.js b/ruoyi-ui/src/utils/jsencrypt.js new file mode 100644 index 0000000..78d9523 --- /dev/null +++ b/ruoyi-ui/src/utils/jsencrypt.js @@ -0,0 +1,30 @@ +import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' + +// 密钥对生成 http://web.chacuo.net/netrsakeypair + +const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export function encrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export function decrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} + diff --git a/ruoyi-ui/src/utils/permission.js b/ruoyi-ui/src/utils/permission.js new file mode 100644 index 0000000..0263bbc --- /dev/null +++ b/ruoyi-ui/src/utils/permission.js @@ -0,0 +1,47 @@ +import store from '@/store' + +/** + * 字符权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkPermi(value) { + if (value && value instanceof Array && value.length > 0) { + const permissions = store.getters && store.getters.permissions + const permissionDatas = value + const all_permission = "*:*:*" + + const hasPermission = permissions.some(permission => { + return all_permission === permission || permissionDatas.includes(permission) + }) + + return hasPermission + + } else { + console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`) + return false + } +} + +/** + * 角色权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkRole(value) { + if (value && value instanceof Array && value.length > 0) { + const roles = store.getters && store.getters.roles + const permissionRoles = value + const super_admin = "admin" + + const hasRole = roles.some(role => { + return super_admin === role || permissionRoles.includes(role) + }) + + return hasRole + + } else { + console.error(`need roles! Like checkRole="['admin','editor']"`) + return false + } +} \ No newline at end of file diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js new file mode 100644 index 0000000..757871f --- /dev/null +++ b/ruoyi-ui/src/utils/request.js @@ -0,0 +1,154 @@ +import axios from 'axios' +import { Notification, MessageBox, Message, Loading } from 'element-ui' +import store from '@/store' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { tansParams, blobValidate } from "@/utils/ruoyi" +import cache from '@/plugins/cache' +import { saveAs } from 'file-saver' + +let downloadLoadingInstance +// 是否显示重新登录 +export let isRelogin = { show: false } + +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' +// 创建axios实例 +const service = axios.create({ + // axios中请求配置有baseURL选项,表示请求URL公共部分 + baseURL: process.env.VUE_APP_BASE_API, + // 超时 + timeout: 99999999 +}) + +// request拦截器 +service.interceptors.request.use(config => { + config.timeout = 99999999; + + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + if (getToken() && !isToken) { + config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + tansParams(config.params) + url = url.slice(0, -1) + config.params = {} + config.url = url + } + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { + const requestObj = { + url: config.url, + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, + time: new Date().getTime() + } + const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小 + const limitSize = 5 * 1024 * 1024 // 限制存放数据5M + if (requestSize >= limitSize) { + console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') + return config + } + const sessionObj = cache.session.getJSON('sessionObj') + if (sessionObj === undefined || sessionObj === null || sessionObj === '') { + cache.session.setJSON('sessionObj', requestObj) + } else { + const s_url = sessionObj.url // 请求地址 + const s_data = sessionObj.data // 请求数据 + const s_time = sessionObj.time // 请求时间 + const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交 + if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { + const message = '数据正在处理,请勿重复提交' + console.warn(`[${s_url}]: ` + message) + return Promise.reject(new Error(message)) + } else { + cache.session.setJSON('sessionObj', requestObj) + } + } + } + return config +}, error => { + console.log(error) + Promise.reject(error) +}) + +// 响应拦截器 +service.interceptors.response.use(res => { + // 未设置状态码则默认成功状态 + const code = res.data.code || 200 + // 获取错误信息 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + // 二进制数据则直接返回 + if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { + return res.data + } + if (code === 401) { + if (!isRelogin.show) { + isRelogin.show = true + MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + isRelogin.show = false + store.dispatch('LogOut').then(() => { + location.href = '/index' + }) + }).catch(() => { + isRelogin.show = false + }) + } + return Promise.reject('无效的会话,或者会话已过期,请重新登录。') + } else if (code === 500) { + Message({ message: msg, type: 'error' }) + return Promise.reject(new Error(msg)) + } else if (code === 601) { + Message({ message: msg, type: 'warning' }) + return Promise.reject('error') + } else if (code !== 200) { + Notification.error({ title: msg }) + return Promise.reject('error') + } else { + return res.data + } + }, + error => { + console.log('err' + error) + let { message } = error + if (message == "Network Error") { + message = "后端接口连接异常" + } else if (message.includes("timeout")) { + message = "系统接口请求超时" + } else if (message.includes("Request failed with status code")) { + message = "系统接口" + message.substr(message.length - 3) + "异常" + } + Message({ message: message, type: 'error', duration: 5 * 1000 }) + return Promise.reject(error) + } +) + +// 通用下载方法 +export function download(url, params, filename, config) { + downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }) + return service.post(url, params, { + transformRequest: [(params) => { return tansParams(params) }], + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + responseType: 'blob', + ...config + }).then(async (data) => { + const isBlob = blobValidate(data) + if (isBlob) { + const blob = new Blob([data]) + saveAs(blob, filename) + } else { + const resText = await data.text() + const rspObj = JSON.parse(resText) + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + Message.error(errMsg) + } + downloadLoadingInstance.close() + }).catch((r) => { + console.error(r) + Message.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close() + }) +} + +export default service diff --git a/ruoyi-ui/src/utils/ruoyi.js b/ruoyi-ui/src/utils/ruoyi.js new file mode 100644 index 0000000..3de2d98 --- /dev/null +++ b/ruoyi-ui/src/utils/ruoyi.js @@ -0,0 +1,228 @@ +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +// 日期格式化 +export function parseTime(time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '') + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +// 表单重置 +export function resetForm(refName) { + if (this.$refs[refName]) { + this.$refs[refName].resetFields() + } +} + +// 添加日期范围 +export function addDateRange(params, dateRange, propName) { + let search = params + search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {} + dateRange = Array.isArray(dateRange) ? dateRange : [] + if (typeof (propName) === 'undefined') { + search.params['beginTime'] = dateRange[0] + search.params['endTime'] = dateRange[1] + } else { + search.params['begin' + propName] = dateRange[0] + search.params['end' + propName] = dateRange[1] + } + return search +} + +// 回显数据字典 +export function selectDictLabel(datas, value) { + if (value === undefined) { + return "" + } + var actions = [] + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + value)) { + actions.push(datas[key].label) + return true + } + }) + if (actions.length === 0) { + actions.push(value) + } + return actions.join('') +} + +// 回显数据字典(字符串、数组) +export function selectDictLabels(datas, value, separator) { + if (value === undefined || value.length ===0) { + return "" + } + if (Array.isArray(value)) { + value = value.join(",") + } + var actions = [] + var currentSeparator = undefined === separator ? "," : separator + var temp = value.split(currentSeparator) + Object.keys(value.split(currentSeparator)).some((val) => { + var match = false + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + temp[val])) { + actions.push(datas[key].label + currentSeparator) + match = true + } + }) + if (!match) { + actions.push(temp[val] + currentSeparator) + } + }) + return actions.join('').substring(0, actions.join('').length - 1) +} + +// 字符串格式化(%s ) +export function sprintf(str) { + var args = arguments, flag = true, i = 1 + str = str.replace(/%s/g, function () { + var arg = args[i++] + if (typeof arg === 'undefined') { + flag = false + return '' + } + return arg + }) + return flag ? str : '' +} + +// 转换字符串,undefined,null等转化为"" +export function parseStrEmpty(str) { + if (!str || str == "undefined" || str == "null") { + return "" + } + return str +} + +// 数据合并 +export function mergeRecursive(source, target) { + for (var p in target) { + try { + if (target[p].constructor == Object) { + source[p] = mergeRecursive(source[p], target[p]) + } else { + source[p] = target[p] + } + } catch (e) { + source[p] = target[p] + } + } + return source +} + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export function handleTree(data, id, parentId, children) { + let config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + } + + var childrenListMap = {} + var tree = [] + for (let d of data) { + let id = d[config.id] + childrenListMap[id] = d + if (!d[config.childrenList]) { + d[config.childrenList] = [] + } + } + + for (let d of data) { + let parentId = d[config.parentId] + let parentObj = childrenListMap[parentId] + if (!parentObj) { + tree.push(d) + } else { + parentObj[config.childrenList].push(d) + } + } + return tree +} + +/** +* 参数处理 +* @param {*} params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName] + var part = encodeURIComponent(propName) + "=" + if (value !== null && value !== "" && typeof (value) !== "undefined") { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']' + var subPart = encodeURIComponent(params) + "=" + result += subPart + encodeURIComponent(value[key]) + "&" + } + } + } else { + result += part + encodeURIComponent(value) + "&" + } + } + } + return result +} + +// 返回项目路径 +export function getNormalPath(p) { + if (p.length === 0 || !p || p == 'undefined') { + return p + } + let res = p.replace('//', '/') + if (res[res.length - 1] === '/') { + return res.slice(0, res.length - 1) + } + return res +} + +// 验证是否为blob格式 +export function blobValidate(data) { + return data.type !== 'application/json' +} diff --git a/ruoyi-ui/src/utils/scroll-to.js b/ruoyi-ui/src/utils/scroll-to.js new file mode 100644 index 0000000..c5d8e04 --- /dev/null +++ b/ruoyi-ui/src/utils/scroll-to.js @@ -0,0 +1,58 @@ +Math.easeInOutQuad = function(t, b, c, d) { + t /= d / 2 + if (t < 1) { + return c / 2 * t * t + b + } + t-- + return -c / 2 * (t * (t - 2) - 1) + b +} + +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts +var requestAnimFrame = (function() { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } +})() + +/** + * Because it's so fucking difficult to detect the scrolling element, just move them all + * @param {number} amount + */ +function move(amount) { + document.documentElement.scrollTop = amount + document.body.parentNode.scrollTop = amount + document.body.scrollTop = amount +} + +function position() { + return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop +} + +/** + * @param {number} to + * @param {number} duration + * @param {Function} callback + */ +export function scrollTo(to, duration, callback) { + const start = position() + const change = to - start + const increment = 20 + let currentTime = 0 + duration = (typeof (duration) === 'undefined') ? 500 : duration + var animateScroll = function() { + // increment the time + currentTime += increment + // find the value with the quadratic in-out easing function + var val = Math.easeInOutQuad(currentTime, start, change, duration) + // move the document.body + move(val) + // do the animation unless its over + if (currentTime < duration) { + requestAnimFrame(animateScroll) + } else { + if (callback && typeof (callback) === 'function') { + // the animation is done so lets callback + callback() + } + } + } + animateScroll() +} diff --git a/ruoyi-ui/src/utils/validate.js b/ruoyi-ui/src/utils/validate.js new file mode 100644 index 0000000..6a4c0c5 --- /dev/null +++ b/ruoyi-ui/src/utils/validate.js @@ -0,0 +1,114 @@ +/** + * 路径匹配器 + * @param {string} pattern + * @param {string} path + * @returns {Boolean} + */ +export function isPathMatch(pattern, path) { + const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*') + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(path) +} + +/** + * 判断value字符串是否为空 + * @param {string} value + * @returns {Boolean} + */ +export function isEmpty(value) { + if (value == null || value == "" || value == undefined || value == "undefined") { + return true + } + return false +} + +/** + * 判断url是否是http或https + * @param {string} url + * @returns {Boolean} + */ +export function isHttp(url) { + return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1 +} + +/** + * 判断path是否为外链 + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} + +/** + * @param {string} url + * @returns {Boolean} + */ +export function validURL(url) { + const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validLowerCase(str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUpperCase(str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validAlphabets(str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param {string} email + * @returns {Boolean} + */ +export function validEmail(email) { + const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString(str) { + return typeof str === 'string' || str instanceof String +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray(arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} diff --git a/ruoyi-ui/src/views/banma/orders/index.vue b/ruoyi-ui/src/views/banma/orders/index.vue new file mode 100644 index 0000000..0af883f --- /dev/null +++ b/ruoyi-ui/src/views/banma/orders/index.vue @@ -0,0 +1,858 @@ + + + + + diff --git a/ruoyi-ui/src/views/dashboard/BarChart.vue b/ruoyi-ui/src/views/dashboard/BarChart.vue new file mode 100644 index 0000000..88e7ef6 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/BarChart.vue @@ -0,0 +1,102 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/LineChart.vue b/ruoyi-ui/src/views/dashboard/LineChart.vue new file mode 100644 index 0000000..702ff73 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/LineChart.vue @@ -0,0 +1,135 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/PanelGroup.vue b/ruoyi-ui/src/views/dashboard/PanelGroup.vue new file mode 100644 index 0000000..1a1081f --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/PanelGroup.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/ruoyi-ui/src/views/dashboard/PieChart.vue b/ruoyi-ui/src/views/dashboard/PieChart.vue new file mode 100644 index 0000000..63f0d84 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/PieChart.vue @@ -0,0 +1,79 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/RaddarChart.vue b/ruoyi-ui/src/views/dashboard/RaddarChart.vue new file mode 100644 index 0000000..312e018 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/RaddarChart.vue @@ -0,0 +1,116 @@ + + + diff --git a/ruoyi-ui/src/views/dashboard/mixins/resize.js b/ruoyi-ui/src/views/dashboard/mixins/resize.js new file mode 100644 index 0000000..b1e76e9 --- /dev/null +++ b/ruoyi-ui/src/views/dashboard/mixins/resize.js @@ -0,0 +1,56 @@ +import { debounce } from '@/utils' + +export default { + data() { + return { + $_sidebarElm: null, + $_resizeHandler: null + } + }, + mounted() { + this.initListener() + }, + activated() { + if (!this.$_resizeHandler) { + // avoid duplication init + this.initListener() + } + + // when keep-alive chart activated, auto resize + this.resize() + }, + beforeDestroy() { + this.destroyListener() + }, + deactivated() { + this.destroyListener() + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_sidebarResizeHandler(e) { + if (e.propertyName === 'width') { + this.$_resizeHandler() + } + }, + initListener() { + this.$_resizeHandler = debounce(() => { + this.resize() + }, 100) + window.addEventListener('resize', this.$_resizeHandler) + + this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] + this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) + }, + destroyListener() { + window.removeEventListener('resize', this.$_resizeHandler) + this.$_resizeHandler = null + + this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) + }, + resize() { + const { chart } = this + chart && chart.resize() + } + } +} diff --git a/ruoyi-ui/src/views/error/401.vue b/ruoyi-ui/src/views/error/401.vue new file mode 100644 index 0000000..448b6ec --- /dev/null +++ b/ruoyi-ui/src/views/error/401.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/ruoyi-ui/src/views/error/404.vue b/ruoyi-ui/src/views/error/404.vue new file mode 100644 index 0000000..96f075c --- /dev/null +++ b/ruoyi-ui/src/views/error/404.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/ruoyi-ui/src/views/index.vue b/ruoyi-ui/src/views/index.vue new file mode 100644 index 0000000..490c0a7 --- /dev/null +++ b/ruoyi-ui/src/views/index.vue @@ -0,0 +1,744 @@ + + + + + + diff --git a/ruoyi-ui/src/views/index_v1.vue b/ruoyi-ui/src/views/index_v1.vue new file mode 100644 index 0000000..d2d2ec6 --- /dev/null +++ b/ruoyi-ui/src/views/index_v1.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue new file mode 100644 index 0000000..5bdda11 --- /dev/null +++ b/ruoyi-ui/src/views/login.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/ruoyi-ui/src/views/monitor/account/index.vue b/ruoyi-ui/src/views/monitor/account/index.vue new file mode 100644 index 0000000..067aa07 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/account/index.vue @@ -0,0 +1,516 @@ + + + + + diff --git a/ruoyi-ui/src/views/monitor/alibaba1688/index.vue b/ruoyi-ui/src/views/monitor/alibaba1688/index.vue new file mode 100644 index 0000000..9868cca --- /dev/null +++ b/ruoyi-ui/src/views/monitor/alibaba1688/index.vue @@ -0,0 +1,287 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/monitor/cache/index.vue b/ruoyi-ui/src/views/monitor/cache/index.vue new file mode 100644 index 0000000..f8b6648 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/cache/index.vue @@ -0,0 +1,148 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/cache/list.vue b/ruoyi-ui/src/views/monitor/cache/list.vue new file mode 100644 index 0000000..9e0523e --- /dev/null +++ b/ruoyi-ui/src/views/monitor/cache/list.vue @@ -0,0 +1,241 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/druid/index.vue b/ruoyi-ui/src/views/monitor/druid/index.vue new file mode 100644 index 0000000..8c3a257 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/druid/index.vue @@ -0,0 +1,15 @@ + + diff --git a/ruoyi-ui/src/views/monitor/job/index.vue b/ruoyi-ui/src/views/monitor/job/index.vue new file mode 100644 index 0000000..783873f --- /dev/null +++ b/ruoyi-ui/src/views/monitor/job/index.vue @@ -0,0 +1,513 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/job/log.vue b/ruoyi-ui/src/views/monitor/job/log.vue new file mode 100644 index 0000000..22d4e31 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/job/log.vue @@ -0,0 +1,295 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/key/index.vue b/ruoyi-ui/src/views/monitor/key/index.vue new file mode 100644 index 0000000..2a30001 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/key/index.vue @@ -0,0 +1,463 @@ + + + + + diff --git a/ruoyi-ui/src/views/monitor/logininfor/index.vue b/ruoyi-ui/src/views/monitor/logininfor/index.vue new file mode 100644 index 0000000..f16a3dc --- /dev/null +++ b/ruoyi-ui/src/views/monitor/logininfor/index.vue @@ -0,0 +1,246 @@ + + + + diff --git a/ruoyi-ui/src/views/monitor/online/index.vue b/ruoyi-ui/src/views/monitor/online/index.vue new file mode 100644 index 0000000..048e51d --- /dev/null +++ b/ruoyi-ui/src/views/monitor/online/index.vue @@ -0,0 +1,122 @@ + + + + diff --git a/ruoyi-ui/src/views/monitor/operlog/index.vue b/ruoyi-ui/src/views/monitor/operlog/index.vue new file mode 100644 index 0000000..6f80177 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/operlog/index.vue @@ -0,0 +1,323 @@ + + + + diff --git a/ruoyi-ui/src/views/monitor/server/index.vue b/ruoyi-ui/src/views/monitor/server/index.vue new file mode 100644 index 0000000..36b20f8 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/server/index.vue @@ -0,0 +1,207 @@ + + + diff --git a/ruoyi-ui/src/views/monitor/version/index.vue b/ruoyi-ui/src/views/monitor/version/index.vue new file mode 100644 index 0000000..7398416 --- /dev/null +++ b/ruoyi-ui/src/views/monitor/version/index.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/ruoyi-ui/src/views/prod/ozon/index.vue b/ruoyi-ui/src/views/prod/ozon/index.vue new file mode 100644 index 0000000..27b0742 --- /dev/null +++ b/ruoyi-ui/src/views/prod/ozon/index.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/ruoyi-ui/src/views/prod/products/index.vue b/ruoyi-ui/src/views/prod/products/index.vue new file mode 100644 index 0000000..3fec9e8 --- /dev/null +++ b/ruoyi-ui/src/views/prod/products/index.vue @@ -0,0 +1,1224 @@ + + + + + diff --git a/ruoyi-ui/src/views/prod/rakuten/index.vue b/ruoyi-ui/src/views/prod/rakuten/index.vue new file mode 100644 index 0000000..3745105 --- /dev/null +++ b/ruoyi-ui/src/views/prod/rakuten/index.vue @@ -0,0 +1,1046 @@ + + + + + diff --git a/ruoyi-ui/src/views/redirect.vue b/ruoyi-ui/src/views/redirect.vue new file mode 100644 index 0000000..db4c1d6 --- /dev/null +++ b/ruoyi-ui/src/views/redirect.vue @@ -0,0 +1,12 @@ + diff --git a/ruoyi-ui/src/views/register.vue b/ruoyi-ui/src/views/register.vue new file mode 100644 index 0000000..d624fc6 --- /dev/null +++ b/ruoyi-ui/src/views/register.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/ruoyi-ui/src/views/system/config/index.vue b/ruoyi-ui/src/views/system/config/index.vue new file mode 100644 index 0000000..a0794e1 --- /dev/null +++ b/ruoyi-ui/src/views/system/config/index.vue @@ -0,0 +1,343 @@ + + + diff --git a/ruoyi-ui/src/views/system/dept/index.vue b/ruoyi-ui/src/views/system/dept/index.vue new file mode 100644 index 0000000..088f09d --- /dev/null +++ b/ruoyi-ui/src/views/system/dept/index.vue @@ -0,0 +1,340 @@ + + + diff --git a/ruoyi-ui/src/views/system/dict/data.vue b/ruoyi-ui/src/views/system/dict/data.vue new file mode 100644 index 0000000..2102e9e --- /dev/null +++ b/ruoyi-ui/src/views/system/dict/data.vue @@ -0,0 +1,402 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/dict/index.vue b/ruoyi-ui/src/views/system/dict/index.vue new file mode 100644 index 0000000..ada7e07 --- /dev/null +++ b/ruoyi-ui/src/views/system/dict/index.vue @@ -0,0 +1,347 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/menu/index.vue b/ruoyi-ui/src/views/system/menu/index.vue new file mode 100644 index 0000000..b61cf6f --- /dev/null +++ b/ruoyi-ui/src/views/system/menu/index.vue @@ -0,0 +1,477 @@ + + + diff --git a/ruoyi-ui/src/views/system/notice/index.vue b/ruoyi-ui/src/views/system/notice/index.vue new file mode 100644 index 0000000..148b856 --- /dev/null +++ b/ruoyi-ui/src/views/system/notice/index.vue @@ -0,0 +1,312 @@ + + + diff --git a/ruoyi-ui/src/views/system/post/index.vue b/ruoyi-ui/src/views/system/post/index.vue new file mode 100644 index 0000000..60a1859 --- /dev/null +++ b/ruoyi-ui/src/views/system/post/index.vue @@ -0,0 +1,309 @@ + + + diff --git a/ruoyi-ui/src/views/system/role/authUser.vue b/ruoyi-ui/src/views/system/role/authUser.vue new file mode 100644 index 0000000..4859d59 --- /dev/null +++ b/ruoyi-ui/src/views/system/role/authUser.vue @@ -0,0 +1,199 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/role/index.vue b/ruoyi-ui/src/views/system/role/index.vue new file mode 100644 index 0000000..ab01a4a --- /dev/null +++ b/ruoyi-ui/src/views/system/role/index.vue @@ -0,0 +1,605 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/role/selectUser.vue b/ruoyi-ui/src/views/system/role/selectUser.vue new file mode 100644 index 0000000..d2b21bf --- /dev/null +++ b/ruoyi-ui/src/views/system/role/selectUser.vue @@ -0,0 +1,136 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/authRole.vue b/ruoyi-ui/src/views/system/user/authRole.vue new file mode 100644 index 0000000..1f862d7 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/authRole.vue @@ -0,0 +1,123 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/user/index.vue b/ruoyi-ui/src/views/system/user/index.vue new file mode 100644 index 0000000..0261c7a --- /dev/null +++ b/ruoyi-ui/src/views/system/user/index.vue @@ -0,0 +1,553 @@ + + + \ No newline at end of file diff --git a/ruoyi-ui/src/views/system/user/profile/index.vue b/ruoyi-ui/src/views/system/user/profile/index.vue new file mode 100644 index 0000000..c070cd1 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/index.vue @@ -0,0 +1,95 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/profile/resetPwd.vue b/ruoyi-ui/src/views/system/user/profile/resetPwd.vue new file mode 100644 index 0000000..954d341 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/resetPwd.vue @@ -0,0 +1,69 @@ + + + diff --git a/ruoyi-ui/src/views/system/user/profile/userAvatar.vue b/ruoyi-ui/src/views/system/user/profile/userAvatar.vue new file mode 100644 index 0000000..59afb35 --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/userAvatar.vue @@ -0,0 +1,184 @@ + + + + diff --git a/ruoyi-ui/src/views/system/user/profile/userInfo.vue b/ruoyi-ui/src/views/system/user/profile/userInfo.vue new file mode 100644 index 0000000..486adfc --- /dev/null +++ b/ruoyi-ui/src/views/system/user/profile/userInfo.vue @@ -0,0 +1,88 @@ + + + diff --git a/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue b/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue new file mode 100644 index 0000000..b5c2e2e --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/CodeTypeDialog.vue @@ -0,0 +1,106 @@ + + diff --git a/ruoyi-ui/src/views/tool/build/DraggableItem.vue b/ruoyi-ui/src/views/tool/build/DraggableItem.vue new file mode 100644 index 0000000..e881778 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/DraggableItem.vue @@ -0,0 +1,100 @@ + diff --git a/ruoyi-ui/src/views/tool/build/IconsDialog.vue b/ruoyi-ui/src/views/tool/build/IconsDialog.vue new file mode 100644 index 0000000..958be50 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/IconsDialog.vue @@ -0,0 +1,123 @@ + + + diff --git a/ruoyi-ui/src/views/tool/build/RightPanel.vue b/ruoyi-ui/src/views/tool/build/RightPanel.vue new file mode 100644 index 0000000..c2760eb --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/RightPanel.vue @@ -0,0 +1,946 @@ + + + + + diff --git a/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue b/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue new file mode 100644 index 0000000..e913d0f --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/TreeNodeDialog.vue @@ -0,0 +1,148 @@ + + diff --git a/ruoyi-ui/src/views/tool/build/index.vue b/ruoyi-ui/src/views/tool/build/index.vue new file mode 100644 index 0000000..5849b34 --- /dev/null +++ b/ruoyi-ui/src/views/tool/build/index.vue @@ -0,0 +1,767 @@ + + + + + diff --git a/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue b/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue new file mode 100644 index 0000000..3a6e2ed --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/basicInfoForm.vue @@ -0,0 +1,60 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/createTable.vue b/ruoyi-ui/src/views/tool/gen/createTable.vue new file mode 100644 index 0000000..e71996c --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/createTable.vue @@ -0,0 +1,45 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/editTable.vue b/ruoyi-ui/src/views/tool/gen/editTable.vue new file mode 100644 index 0000000..512f66e --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/editTable.vue @@ -0,0 +1,229 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/genInfoForm.vue b/ruoyi-ui/src/views/tool/gen/genInfoForm.vue new file mode 100644 index 0000000..60d9e87 --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/genInfoForm.vue @@ -0,0 +1,312 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/importTable.vue b/ruoyi-ui/src/views/tool/gen/importTable.vue new file mode 100644 index 0000000..cb080ce --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/importTable.vue @@ -0,0 +1,120 @@ + + + diff --git a/ruoyi-ui/src/views/tool/gen/index.vue b/ruoyi-ui/src/views/tool/gen/index.vue new file mode 100644 index 0000000..83c026f --- /dev/null +++ b/ruoyi-ui/src/views/tool/gen/index.vue @@ -0,0 +1,347 @@ + + + diff --git a/ruoyi-ui/src/views/tool/swagger/index.vue b/ruoyi-ui/src/views/tool/swagger/index.vue new file mode 100644 index 0000000..dd6b4a2 --- /dev/null +++ b/ruoyi-ui/src/views/tool/swagger/index.vue @@ -0,0 +1,15 @@ + + diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js new file mode 100644 index 0000000..9750740 --- /dev/null +++ b/ruoyi-ui/vue.config.js @@ -0,0 +1,137 @@ +'use strict' +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, dir) +} + +const CompressionPlugin = require('compression-webpack-plugin') + +const name = 'ERP管理系统' // 网页标题 + +//const baseUrl = 'https://erp.tashowz.com' // 后端接口 +const baseUrl = 'http://8.138.23.49:8080' +// const baseUrl = 'http://192.168.1.89:8080' +const port = process.env.port || process.env.npm_config_port || 80 // 端口 + +// vue.config.js 配置说明 +//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions +// 这里只列一部分,具体配置参考文档 +module.exports = { + // 部署生产环境和开发环境下的URL。 + // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上 + // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 + publicPath: process.env.NODE_ENV === "production" ? "/" : "/", + // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist) + outputDir: 'dist', + // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) + assetsDir: 'static', + // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 + productionSourceMap: false, + transpileDependencies: ['quill'], + // webpack-dev-server 相关配置 + devServer: { + host: '0.0.0.0', + port: port, + open: true, + proxy: { + // detail: https://cli.vuejs.org/config/#devserver-proxy + [process.env.VUE_APP_BASE_API]: { + target: baseUrl, + changeOrigin: true, + pathRewrite: { + ['^' + process.env.VUE_APP_BASE_API]: '' + } + }, + // springdoc proxy + '^/v3/api-docs/(.*)': { + target: baseUrl, + changeOrigin: true + } + }, + disableHostCheck: true + }, + css: { + loaderOptions: { + sass: { + sassOptions: { outputStyle: "expanded" } + } + } + }, + configureWebpack: { + name: name, + resolve: { + alias: { + '@': resolve('src') + } + }, + plugins: [ + // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 + new CompressionPlugin({ + cache: false, // 不启用文件缓存 + test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式 + filename: '[path][base].gz[query]', // 压缩后的文件名 + algorithm: 'gzip', // 使用gzip压缩 + minRatio: 0.8, // 压缩比例,小于 80% 的文件不会被压缩 + deleteOriginalAssets: false // 压缩后删除原文件 + }) + ], + }, + chainWebpack(config) { + config.plugins.delete('preload') // TODO: need test + config.plugins.delete('prefetch') // TODO: need test + + // set svg-sprite-loader + config.module + .rule('svg') + .exclude.add(resolve('src/assets/icons')) + .end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/assets/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + config.when(process.env.NODE_ENV !== 'development', config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [{ + // `runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/ + }]) + .end() + + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // only package third parties that are initially dependent + }, + elementUI: { + name: 'chunk-elementUI', // split elementUI into a single package + test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm + priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true + } + } + }) + config.optimization.runtimeChunk('single') + }) + } +} diff --git a/sql/refresh_token.sql b/sql/refresh_token.sql new file mode 100644 index 0000000..5403359 --- /dev/null +++ b/sql/refresh_token.sql @@ -0,0 +1,16 @@ +-- 创建刷新令牌表 +CREATE TABLE IF NOT EXISTS `refresh_token` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', + `account_id` bigint(20) NOT NULL COMMENT '账号ID', + `device_id` varchar(100) DEFAULT NULL COMMENT '设备ID', + `token` varchar(255) NOT NULL COMMENT '刷新令牌', + `expire_time` datetime NOT NULL COMMENT '过期时间', + `revoked` char(1) NOT NULL DEFAULT '0' COMMENT '是否已撤销(0正常 1撤销)', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_token` (`token`), + KEY `idx_account_id` (`account_id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_expire_time` (`expire_time`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='刷新令牌表'; \ No newline at end of file diff --git a/sql/user_device_session.sql b/sql/user_device_session.sql new file mode 100644 index 0000000..65f6d88 --- /dev/null +++ b/sql/user_device_session.sql @@ -0,0 +1,50 @@ +-- 用户设备会话管理表 +CREATE TABLE `user_device_session` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `device_id` varchar(100) NOT NULL COMMENT '设备ID(基于主板序列号生成)', + `device_name` varchar(100) DEFAULT NULL COMMENT '设备名称', + `device_type` varchar(50) DEFAULT 'PC' COMMENT '设备类型', + `os_name` varchar(50) DEFAULT NULL COMMENT '操作系统', + `os_version` varchar(50) DEFAULT NULL COMMENT '系统版本', + `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址', + `hostname` varchar(100) DEFAULT NULL COMMENT '主机名', + `first_login_time` datetime DEFAULT NULL COMMENT '首次登录时间', + `last_active_time` datetime DEFAULT NULL COMMENT '最后活跃时间', + `online_status` char(1) DEFAULT '0' COMMENT '在线状态(0离线 1在线)', + `login_count` bigint(20) DEFAULT 1 COMMENT '登录次数', + `token_hash` varchar(64) DEFAULT NULL COMMENT 'JWT令牌哈希(用于识别)', + `status` char(1) DEFAULT '0' COMMENT '设备状态(0正常 1已踢出 2已禁用)', + `expire_time` datetime DEFAULT NULL COMMENT '过期时间', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username_device` (`username`, `device_id`), + KEY `idx_username` (`username`), + KEY `idx_device_id` (`device_id`), + KEY `idx_token_hash` (`token_hash`), + KEY `idx_online_status` (`online_status`), + KEY `idx_last_active_time` (`last_active_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户设备会话管理表'; + +-- 插入测试数据 +INSERT INTO `user_device_session` (`username`, `device_id`, `device_name`, `device_type`, `os_name`, `os_version`, `ip_address`, `hostname`, `first_login_time`, `last_active_time`, `online_status`, `login_count`, `status`) VALUES +('admin', 'DEVICE-ABC123DEF456', 'Windows 工作站', 'PC', 'Windows 11', '10.0.22000', '192.168.1.100', 'DESKTOP-ADMIN', NOW(), NOW(), '1', 1, '0'), +('admin', 'DEVICE-XYZ789GHI012', 'MacBook Pro', 'PC', 'Windows 10', '10.0.19045', '192.168.1.101', 'LAPTOP-ADMIN', DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), '0', 5, '0'), +('testuser', 'DEVICE-QWE456RTY789', 'Ubuntu 桌面', 'PC', 'Windows 11', '10.0.22000', '192.168.1.102', 'UBUNTU-TEST', DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 30 MINUTE), '1', 3, '0'); + +-- 创建定时清理过期设备的事件(可选) +-- DELIMITER $$ +-- CREATE EVENT IF NOT EXISTS `clean_expired_devices` +-- ON SCHEDULE EVERY 1 DAY +-- STARTS CURRENT_TIMESTAMP +-- DO +-- BEGIN +-- DELETE FROM user_device_session +-- WHERE (expire_time IS NOT NULL AND expire_time < NOW()) +-- OR (online_status = '0' AND last_active_time < DATE_SUB(NOW(), INTERVAL 7 DAY)); +-- END$$ +-- DELIMITER ; + +-- 设置事件调度器开启(如果使用事件) +-- SET GLOBAL event_scheduler = ON; \ No newline at end of file