diff --git a/LICENSE b/LICENSE index c81e7a2..e2fd825 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 blossom-editor +Copyright (c) 2023 小贼贼子 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..3af5054 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Blossom 笔记软件 + +Blossom 是一个**云端存储的笔记软件**,你可以搭建自己的服务器来保存你的所有笔记,图片,个人计划安排,并在任意设备之间实时同步。 + +![](./doc/imgs/home_ld.jpg "Blossom 首页") + +当前版本: `1.0.0` + +> 更新日志 + +--- + +#### Blossom 具有以下优势: + +1. **不再需要为同步设备数量付费**。 +2. **不再需要为公网访问付费**。 +3. **不再需要为软件付费**,基于[MIT协议](https://choosealicense.com/licenses/mit/)完全开源 +4. **自带截图功能**(仅windows),你不再需要使用其他截图工具截图后保存在本地再上传到文章了。 +5. **文章与图片都在你的服务器存储**,不需要再使用任何三方图床,不需要购买任何对象存储。 +6. **没有任何私有协议**,基于 Markdown 语法,采用约定格式拓展样式。迁移到其他软件可以正常显示。 +7. **所有文章和图片一键打包下载**,你可以一键导出迁移笔记。 +8. 多账号权限,可以和你的朋友一起使用。或者将自己的生活/工作笔记存放在不同账号中。 + +#### Blossom 具有以下功能: +1. **文章编辑**:文章编写,以及文章公网访问权限设置。 +2. **双链笔记**:内部文章与外部链接引用形成的双链笔记。 +3. **图片管理**:按文章目录分类你的图片,或自定义图片目录。 +4. **快捷便签**:方便你记录随意的日常信息。 +5. **计划安排**:阶段性的长期计划,或者日历中的短期计划安排。 +6. **网站收藏**:常用网站快捷访问,或者说是一个书签功能。 +7. 笔记总字数折线图,笔记编辑数量热力图,服务器请求量折线图展示。 + +更多内容,可前往[源码仓库](https://gitee.com/blossom-projects/blossom)或[文档](https://www.wangyunf.com/blossomdoc)查看。 diff --git a/blossom-backend/.gitignore b/blossom-backend/.gitignore new file mode 100644 index 0000000..faacf08 --- /dev/null +++ b/blossom-backend/.gitignore @@ -0,0 +1,32 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ +/logs/ diff --git a/blossom-backend/README.md b/blossom-backend/README.md new file mode 100644 index 0000000..25d3516 --- /dev/null +++ b/blossom-backend/README.md @@ -0,0 +1,149 @@ +# 一、项目介绍 + +私密信息:如`Git`账号等不在此列。 +--- + +# 二、系统基本信息 + +本项目为Web项目,除`SpringBoot`使用`2.x`最新版本外,其余不做强制版本要求,只需兼容`JDK8`即可。中间件版本跟随需求变更。 + +`SpringBoot`版本说明请查阅:https://spring.io/projects/spring-boot#support。 + +简略的注意事项: +1. 接口文档使用`SmartDoc`,不要引入其他三方接口文档框架,如`Swagger`等。用法请查看第【4.1】项。 +2. 接口调用需授权时,请查看第【4.2】项的系统登录。 +3. 项目的模块说明请查看第【三】项。 +4. 需遵循第【五】项中说明的开发规范。 +5. 本项目依赖其他开源项目,本身为闭源项目。并遵循各类框架的开源协议。 + +--- + +# 三、模块说明 + +``` +root + ├─ aodc 相关文档 + ├─ backend-admin 后台应用,当不需要多应用部署时,默认使用该应用对外提供所有服务,该应用只写 Controller 层。 + ├─ backend-service 业务代码 Service 层与持久层 Mapper。 + | ├─ service-base 提供基础的用户登录,RBAC,文件上传,系统字典,参数等功能,通常所有 Service 模块都依赖该模块。 + | ├─ service-biz 业务代码,实现业务需求的模块。 + | └─ service-flow 工作流引擎,如果系统需要使用工作流,那么通常所有 Service 模块都依赖该模块。 + | + ├─ common 项目公共模块 + | ├─ common-base 基础对象(如公共响应 R.class),工具类,异常捕获,动态日志级别,Caffeine 等。 + | ├─ common-cache 缓存封装,Redis 封装,Redis Cache 封装。 + | ├─ common-db 数据库模块,使用 mybatis plus,dynamic-datasource,慢SQL监控。 + | ├─ common-es ElasticSearch 功能封装。 + | ├─ common-iaas 云厂商功能的封装,如对象存储,SMS短信服务,IM即时消息等。 + | └─ common-wechat 微信相关功能的封装。 + | + ├─ expand-sentinel 对 Alibaba Sentinel 的封装,拓展了一些功能,实现了一些本地流量查询接口。 + ├─ expand-tracker 自研的链路追踪核心模块, 源自 tracker-core, 仅提供本地日志记录和日志中插入 Trace 的功能。 + └─ smart-doc 本地接口文档,在 Smart-Doc 上修改了样式,使之更符合开发阅读习惯。 +``` + +--- + +# 四、使用说明 + +--- + +## 4.1 SmartDoc 接口文档使用说明 + +本系统接口文档依赖于`smart-doc`,并修改了`smart-doc`的文档样式,所以需要本地编译`smart-doc`才可使用。官方文档地址 + +**第一步:**在 smart-doc 目录执行如下命令。 +``` +mvn clean install -Dmaven.test.skip=true -Dfile.encoding=UTF-8 -Dmaven.javadoc.skip=true +``` + +**第二步:**搜索 `SmartDocConfig.class` 类,并执行该类生成接口文档。 + +--- + +## 4.2 系统登录 + +登录请访问`//login` + +请求体: +```json +{ + // 授权客户端的ID, 见配置文件 prohect.auth.clients.client-id + "clientId": "web", + // 授权客户端的登录方式, 见配置文件 prohect.auth.clients.grant-type + "grantType":"password", + "username": "1", + "password": "123" +} +``` +登录返回的 token 信息需要放在请求头的 `Authorization` 中, 并以 `Bearer `开头, 例如 + +``` +Authorization:Bearer ac27fd57303d4bddb0229c3b6d71b611 +``` + +> token 的用法遵循 Bearer Token 规范, 可见:https://learning.postman.com/docs/sending-requests/authorization/#bearer-token +--- + +# 五、代码规范 + +## 5.1 API接口规范 +1. **查询请求一律使用`GET`,对数据有变更时一律使用`POST`**。 +2. `GET`请求如果传入参数`>=5`个时,需使用`@ModelAttribute`接收对象。 +3. `GET`不要传入请求体。 +4. URL中**不要包含大写字母,不要包含下划线`_`,不要使用驼峰,分割字符串需使用`-`**。 +5. API不需要严格按照`Restful`设计,但仍需要遵循一些规则。 + 1. 尽量不要包含动词,如查询用户:不要使用`GET /getUser`,而是使用`GET /user` + 2. 为了协调多种开发习惯,不使用`put`请求,不使用`delete`请求,不使用`head`请求等等。 + 3. 基础增删改查路径为:列表`list`,分页`page`,详情`info`,新增修改`save`,删除`del` +6. 接口统一返回`Http`响应码`200`,除非强制要求按规范返回响应码。 +7. 自定义响应码至少5位数,响应码可以是英文+数字的组合,但数字的前三位必须是规范的`Http`响应码。 + 1. 【正例】:`40001`,`AUTH-40101`,`40402`,`50003`。 + 2. 【反例】:使用`40400`表示用户未授权。使用`30000`表示服务器处理异常。 +8. 对请求参数或请求体有加签需求时,签名一律放在`Header`中。 +9. 授权信息一律放在请求头的`Authorization`字段中。 + +## 5.2 框架规范 + +### 5.2.1 `Spring` +1. `common`包,`expand`包下的对象要求使用`spring.factories`自动装配。 +2. `backend-service`包下的对象要求使用`ServiceXXXScan`指定扫描路径,如`com.blossom.service.base.ServiceBaseScan`。 +3. 没有多实现类需求时,不需要创建接口,如各类业务`Service`,多数在整个项目周期内都不会有多个实现类,增加接口徒增工作量。但多个实现类时有必要使用接口(例如:`common.iaas.OSManager.class`) +4. 功能为单独模块时,需使用`spring.factories`自动装配,不要在`SpringBootApplication`中指定扫描路径。 +5. 不优先使用`spring-security`作为授权框,该框架过于复杂,不同人使用时差异过大,除非有非常非常严格的代码审查与安全扫描。 +6. 不强制要求使用`validator`做参数校验,内部调用时,`service`内仍然会做必填等校验。 +7. 在保证逻辑清晰且实现唯一的情况下,为了防止相同逻辑在多处实现,允许循环依赖,但仍然需要劲量避免循环依赖。 +8. 优先使用`@Autowired`注入对象,不使用`@Resource`。JDK11中移除了`javax.annotation`包,需要手动加入才可使用`@Resource`。 +9. 使用自定义配置时,不建议使用`@Value`注解,而是统一使用`ConfigurationProperties`创建配置对象类。为了统一维护以及方便查找。 +10. 优先使用`Springboot`默认适配框架。 + 1. 使用`jackson`作为`json`序列化工具,不要使用`fastjson`,`gson`或其他工具。 + 2. 使用`logback`作为日志工具类。 + 3. 使用`hikari`作为数据库连接池。 + +### 5.2.2 `MyBatis` +1. 使用驼峰命名转换`mybatis.configuration.map-underscore-to-camel-case: true`,`mapper.xml`文件中不要使用`resultMap`。 +2. `Mapper.java`接口使用`@Mapper`注解,扫描时通过`@MapperScan(annotationClass = Mapper.class)`查找,不要使用路径的方式配置。这样是为了将`Mapper.java`文件于业务service等放在同一目录下。 +3. 涉及到多表查询,复杂查询,使用xml显式编写SQL语句,不要使用`MyBatis Plus`,原则上只有单表简单查询时允许使用`MyBatis Plus`。 +4. `Entity`类中不需要使用`@TableField`标明数据库字段。 +5. `Entity`类中包含非数据库字段时,需要使用`@TableField(exist = false)`标明该字段非数据库字段。 + +### 5.2.3 其他 +1. 使用`hutool`作为工具类,不要使用`common-lang3`,不要使用`guava`,不要使用`spring`自带工具类。 +2. 本地缓存工具类使用`caffeine`或基于Java自己实现,不要使用`hutool`(存在性能问题),不要使用`guava`。 + + +## 5.3 代码规范 +1. 本项目代码根据业务归类。 + 1. 【正例】:用户`user`相关的`service,mapper,pojo`等都在`com.xx.user`路径下,不要根据类型分类。 + 2. 【反例】:所有业务的`Service`类都在同一个路径`com.xx.service`下。 + 3. 由于可能存在多个应用,所以`Controller`在应用所在的目录下,而不根据业务归类。 +2. 请求实体类使用`XxxReq.class` +3. 响应实体类使用`XxxRes.class` +4. 数据库实体类使用`XxxEntity.class` +5. 非必要情况下不需要使用DTO,多数情况下只是多了一层无必要的转换。 + 1. 若要求多应用服务,且共用模块,可以直接使用Entity来代替DTO。 + 2. 若需要使用 Dubbo 等RPC框,则需要对外提供DTO来作为接口参数。 +6. 所有实体类需要继承`AbstractPOJO.class`,以便使用`to()`方法转换对象。 +7. 实体类中所有对象使用封装类型,尤其注意 Boolean 类型的字段,非封装类型生成的 `getter` 方法为 `is` 。 +8. 由于接口文档依赖于注释,所以`Controller`,实体类的注释必须填写。 +9. 配置文件需要写明注释。 diff --git a/blossom-backend/adoc/restart-springboot.sh b/blossom-backend/adoc/restart-springboot.sh new file mode 100644 index 0000000..89725e7 --- /dev/null +++ b/blossom-backend/adoc/restart-springboot.sh @@ -0,0 +1,9 @@ +#!/dash +# 重启 blossom +pid=`ps aux | grep backend-blossom.jar | grep -v grep | awk '{print $2}'` +echo "进程ID : " $pid +kill -9 $pid +echo "进程" $pid "已被杀死" +echo "开始重启 backend-blossom 服务器" +nohup java -Xms1024m -Xmx1024m -jar /usr/local/jasmine/blossom/backend/backend-blossom.jar & +echo "backend-blossom 正在启动,请查看日志 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓" diff --git a/blossom-backend/adoc/sql/blossom.sql b/blossom-backend/adoc/sql/blossom.sql new file mode 100644 index 0000000..13e125c --- /dev/null +++ b/blossom-backend/adoc/sql/blossom.sql @@ -0,0 +1,330 @@ +/* + Navicat Premium Data Transfer + + Source Server : 0_内网:【root&jasmine888】 + Source Server Type : MySQL + Source Server Version : 80033 + Source Host : 192.168.31.99:3306 + Source Schema : blossom + + Target Server Type : MySQL + Target Server Version : 80033 + File Encoding : 65001 + + Date: 06/08/2023 22:20:26 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for base_sys_param +-- ---------------------------- +DROP TABLE IF EXISTS `base_sys_param`; +CREATE TABLE `base_sys_param` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '参数ID', + `param_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数名称', + `param_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数值', + `param_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数说明', + `open_state` int NOT NULL COMMENT '开放状态 [YesNo]', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unq_param_name`(`param_name`) USING BTREE COMMENT '参数名称唯一' +) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统参数' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of base_sys_param +-- ---------------------------- +INSERT INTO `base_sys_param` VALUES (1, 'WEB_ARTICLE_URL', 'https://www.domain.com/blossom/#/articles?articleId=', 'WEB端文章地址,用于PC端直接调往WEB端阅读文章', 1, '2023-04-04 08:20:57', '2023-08-06 22:19:07'); +INSERT INTO `base_sys_param` VALUES (3, 'ARTICLE_LOG_EXP_DAYS', '30', '文章修改记录保存天数, 超过该天数将被删除', 1, '2023-08-02 17:46:58', '2023-08-02 18:03:43'); +INSERT INTO `base_sys_param` VALUES (11, 'HEFENG_KEY', 'ABC', '和风天气的KEY', 1, '2023-07-31 19:28:54', '2023-08-06 22:19:11'); +INSERT INTO `base_sys_param` VALUES (21, 'GITEE_ACCESS_TOKEN', 'ABC', '[过时配置]GITEE API 的访问 token', 1, '2023-07-31 20:12:05', '2023-08-06 22:20:12'); + +-- ---------------------------- +-- Table structure for base_user +-- ---------------------------- +DROP TABLE IF EXISTS `base_user`; +CREATE TABLE `base_user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `type` tinyint(1) NOT NULL DEFAULT 2 COMMENT '用户类型: 1:管理员; 2:普通用户; 3:只读用户;', + `username` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名', + `phone` varchar(13) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户手机号', + `password` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户密码', + `salt` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '密码加盐', + `nick_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '昵称', + `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '真实姓名', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户头像', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '备注', + `cre_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '创建人ID,名称', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `upd_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '修改人ID,名称', + `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + `del_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '删除人ID,名称', + `del_time` bigint NOT NULL DEFAULT 0 COMMENT '删除时间', + `location` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '101100101' COMMENT '和风天气的位置, 官方文档:https://github.com/qwd/LocationList/blob/master/China-City-List-latest.csv', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unq_user_username`(`username`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10002 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of base_user +-- ---------------------------- +INSERT INTO `base_user` VALUES (1, 1, 'blos', '', '$2a$10$SgMx8T/06595PEq3EA9US.ja1oHxpIDG/XnERmBXS.wYS8qbxAGDa', 'UVeESP5NgXwb8JmjCHUK', '用户', 'blos', '', '预设管理员账号, 用户名密码都是 blos', '0,SYS', '2023-08-04 16:48:28', '0,SYS', '2023-08-06 22:17:35', '0', 0, '101100101'); + +-- ---------------------------- +-- Table structure for blossom_article +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_article`; +CREATE TABLE `blossom_article` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `pid` bigint NOT NULL COMMENT '文件夹ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文章名称', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文章图标', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标签集合', + `sort` int NOT NULL DEFAULT 1 COMMENT '排序', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '封面', + `describes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '描述', + `star_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'star状态', + `open_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '公开状态', + `open_version` int NOT NULL DEFAULT 0 COMMENT '公开版本', + `pv` int NOT NULL DEFAULT 0 COMMENT '页面的查看数', + `uv` int NOT NULL DEFAULT 0 COMMENT '独立的访问次数,每日IP重置', + `likes` int NOT NULL DEFAULT 0 COMMENT '点赞数', + `words` int NOT NULL DEFAULT 0 COMMENT '文章字数', + `version` int NOT NULL DEFAULT 0 COMMENT '版本', + `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色', + `toc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '目录解析', + `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Markdown 内容', + `html` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Html内容', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章,Article' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_article +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_article_log +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_article_log`; +CREATE TABLE `blossom_article_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `article_id` bigint NOT NULL COMMENT '文章ID', + `version` int NOT NULL DEFAULT 0 COMMENT '版本', + `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文章内容', + `cre_time` datetime NOT NULL COMMENT '修改日期', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章记录,ArticleLog' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_article_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_article_open +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_article_open`; +CREATE TABLE `blossom_article_open` ( + `id` bigint NOT NULL COMMENT '文章ID', + `pid` bigint NOT NULL COMMENT '文件夹ID', + `words` int NOT NULL COMMENT '字数', + `open_version` int NOT NULL DEFAULT 1 COMMENT '版本', + `open_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '公开时间', + `sync_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '同步时间', + `toc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '目录', + `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Markdown 内容', + `html` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Html内容', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '公开文章,ArticleOpen' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_article_open +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_article_reference +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_article_reference`; +CREATE TABLE `blossom_article_reference` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `source_id` bigint NOT NULL COMMENT '文章ID', + `source_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文章名称', + `target_Id` bigint NOT NULL COMMENT '引用文章ID', + `target_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '引用名称', + `target_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '图片链接', + `type` tinyint NOT NULL COMMENT '引用类型: 10:图片; 11:文章; 21:外部文章', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9148 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_article_reference +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_article_view +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_article_view`; +CREATE TABLE `blossom_article_view` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID', + `article_id` bigint NOT NULL COMMENT '文章ID', + `type` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '事件类型 1:uv; 2:like', + `ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '地址,IPV4', + `user_agent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '设备', + `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '省', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '市', + `cre_day` date NOT NULL COMMENT '日期 yyyy-MM-dd', + `cre_time` datetime NOT NULL COMMENT '日期', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_view_articleid`(`article_id`) USING BTREE COMMENT '文章ID', + INDEX `idx_view_ip`(`ip`) USING BTREE COMMENT 'IP', + INDEX `idx_view_creday`(`cre_day`) USING BTREE COMMENT '日期' +) ENGINE = InnoDB AUTO_INCREMENT = 49 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章访问记录,ArticleView' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_article_view +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_folder +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_folder`; +CREATE TABLE `blossom_folder` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id', + `pid` bigint UNSIGNED NOT NULL COMMENT '父id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件夹名称', + `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图标', + `tags` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标签', + `open_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '开放状态', + `sort` int UNSIGNED NOT NULL DEFAULT 1 COMMENT '排序', + `cover` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '封面图片', + `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色', + `describes` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '备注', + `store_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '/' COMMENT '存储地址', + `subject_words` int NOT NULL DEFAULT 0 COMMENT '专题字数', + `subject_upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '专题的最后修改时间', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1:文章;2:图片', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12035 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文件夹,Folder' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_folder +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_note +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_note`; +CREATE TABLE `blossom_note` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '内容', + `top` tinyint(1) NOT NULL DEFAULT 0 COMMENT '置顶', + `top_time` datetime NULL DEFAULT NULL COMMENT '置顶时间', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_note +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_picture +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_picture`; +CREATE TABLE `blossom_picture` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `pid` bigint NOT NULL DEFAULT -1 COMMENT '文件夹ID', + `source_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '原文件名', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件名', + `path_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件路径', + `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件访问url', + `rate` tinyint NOT NULL DEFAULT 0 COMMENT '评分 {0,1,2,3,4,5}', + `star_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '收藏 0:否,1:是', + `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件后缀', + `size` bigint NOT NULL DEFAULT 0 COMMENT '文件大小', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建日期', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `UNI_FILE_UID_FNAME`(`url`) USING BTREE COMMENT '相同路径的图片不能有两条', + UNIQUE INDEX `unq_pic_pathname`(`path_name`) USING BTREE COMMENT '路径唯一' +) ENGINE = InnoDB AUTO_INCREMENT = 305774931235323969 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '图片,Picture' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_picture +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_plan +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_plan`; +CREATE TABLE `blossom_plan` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `group_id` bigint NOT NULL COMMENT '分组ID', + `type` tinyint(1) NOT NULL COMMENT '计划类型: daily, day', + `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标题', + `content` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '内容', + `plan_month` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '日期所在月份', + `plan_date` date NULL DEFAULT NULL COMMENT '日期: day', + `plan_start_time` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '开始时间: daily, day', + `plan_end_time` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '结束时间', + `color` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色', + `position` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '该计划在该组计划的位置 head, tail, all', + `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图片名称, 或图片地址', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 142 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '计划,Plan' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_plan +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_stat +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_stat`; +CREATE TABLE `blossom_stat` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` tinyint(1) NOT NULL COMMENT '统计类型: 1:每日编辑文章数; 2:每月总字数;', + `stat_date` date NOT NULL COMMENT '统计日期', + `stat_value` int NOT NULL DEFAULT 0 COMMENT '统计数值', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 218 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_stat +-- ---------------------------- + +-- ---------------------------- +-- Table structure for blossom_web +-- ---------------------------- +DROP TABLE IF EXISTS `blossom_web`; +CREATE TABLE `blossom_web` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '网页名称', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '网页url', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '图标', + `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图片, 图片的优先级高于图标', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '类型 ', + `sort` int NOT NULL DEFAULT 1 COMMENT '排序', + `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 292 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '[FS] 网站收藏' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of blossom_web +-- ---------------------------- + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/blossom-backend/backend/pom.xml b/blossom-backend/backend/pom.xml new file mode 100644 index 0000000..5a7750c --- /dev/null +++ b/blossom-backend/backend/pom.xml @@ -0,0 +1,139 @@ + + + + blossom-backend + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + backend + + + 4.3.0 + + + + + + com.blossom + common-base + + + + com.blossom + common-cache + + + + com.blossom + common-db + + + + com.blossom + common-iaas + + + + + com.blossom + expand-tracker-core + + + + + com.blossom + expand-sentinel-springmvc + + + + + com.blossom + expand-sentinel-metric + + + + + com.auth0 + java-jwt + ${java-jwt.version} + + + + + net.coobird + thumbnailator + 0.4.20 + + + + + + backend-blossom + + + src/main/resources + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + ${project.build.finalName} + + + + + repackage + + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + + maven-compiler-plugin + 3.6.1 + + ${maven.compiler.target} + ${maven.compiler.source} + ${project.build.sourceEncoding} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.blossom.backend.APP + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java b/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java new file mode 100644 index 0000000..b6731b1 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java @@ -0,0 +1,28 @@ +package com.blossom.backend; + +import com.blossom.common.base.BaseConstants; +import com.blossom.expand.tracker.core.Tracker; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * BLOSSOM + * + * @author xzzz + */ +@EnableAsync +@EnableScheduling +@SpringBootApplication +public class APP { + + public static void main(String[] args) { + Tracker.start("APPLICATION_START", TrackerConstants.SPAN_TYPE_APPLICATION_RUN); + SpringApplication.run(APP.class, args); + BaseConstants.desc(); + Tracker.end(); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java new file mode 100644 index 0000000..56c733d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java @@ -0,0 +1,134 @@ +package com.blossom.backend.base.auth; + +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.pojo.LoginDTO; +import com.blossom.backend.base.auth.pojo.LoginEvent; +import com.blossom.backend.base.auth.pojo.LoginReq; +import com.blossom.backend.base.auth.repo.TokenRepository; +import com.blossom.backend.base.auth.security.PasswordEncoder; +import com.blossom.backend.base.auth.token.TokenEncoder; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.ServletUtil; +import com.blossom.common.base.util.json.JsonUtil; +import org.springframework.context.ApplicationContext; + +import javax.servlet.http.HttpServletRequest; +import java.util.TreeMap; + +/** + * 授权处理抽象类 + * + * @author xzzz + */ +public abstract class AbstractAuthService { + protected AuthProperties properties; + protected TokenRepository tokenRepository; + protected TokenEncoder tokenEncoder; + protected PasswordEncoder passwordEncoder; + private ApplicationContext applicationContext; + + public AbstractAuthService(AuthProperties properties, + TokenRepository tokenRepository, + TokenEncoder tokenEncoder, + PasswordEncoder passwordEncoder, + ApplicationContext applicationContext) { + this.properties = properties; + this.tokenRepository = tokenRepository; + this.tokenEncoder = tokenEncoder; + this.passwordEncoder = passwordEncoder; + this.applicationContext = applicationContext; + } + + /** + * 登录 + * + * @param request 请求 + * @param req 登录请求参数 + * @return 令牌对象 + */ + public AccessToken login(HttpServletRequest request, LoginReq req) { + LoginDTO login = req.to(LoginDTO.class); + AccessToken accessToken = req.to(AccessToken.class); + accessToken.setMetadata(new TreeMap<>()); + loginByPassword(accessToken, login); + fillConfig(accessToken); + genToken(accessToken); + saveToken(accessToken); + publishEvent(request, accessToken, login); + return accessToken; + } + + /** + * 为AccessToken填充配置信息, 如过期日期等 + */ + private void fillConfig(AccessToken accessToken) { + AuthProperties.Client client = properties.getClientMap().get(accessToken.getClientId()); + if (client == null) { + throw new AuthException(AuthRCode.INVALID_CLIENT_ID); + } + accessToken.setDuration(client.getDuration()); + accessToken.setExpire(System.currentTimeMillis() + (accessToken.getDuration() * 1000)); + accessToken.setRequestRefresh(client.getRequestRefresh()); + accessToken.setMultiPlaceLogin(client.getMultiPlaceLogin()); + accessToken.setLoginTime(DateUtils.now()); + } + + /** + * 登录时生成TOKEN令牌 + */ + private void genToken(AccessToken accessToken) { + accessToken.setToken(tokenEncoder.encode(accessToken)); + } + + /** + * 保存 accessToken 到存储介质中 + */ + private void saveToken(AccessToken accessToken) { + tokenRepository.saveToken(accessToken); + tokenRepository.saveUniqueToken(accessToken); + } + + /** + * 发布登录事件 + * + * @param request request + * @param accessToken token + * @param login 登录信息 + */ + private void publishEvent(HttpServletRequest request, AccessToken accessToken, LoginDTO login) { + String ip = ServletUtil.getIP(request); + String userAgent = ServletUtil.getUserAgent(request); + login.setUserId(accessToken.getUserId()); + login.setIp(ip); + login.setUserAgent(userAgent); + login.setLoginTime(accessToken.getLoginTime()); + login.setAccessTokenSnapshot(JsonUtil.toJson(accessToken)); + applicationContext.publishEvent(new LoginEvent(login)); + } + + /** + * 根据用户名密码登录 + * + * @param accessToken token + * @param req 请求参数, 包含用户名和密码 + */ + protected abstract void loginByPassword(AccessToken accessToken, LoginDTO req); + + /** + * 填充 metadata 内容 + * + * @param accessToken token + * @param user 用户信息 + */ + protected void fillUserDetail(AccessToken accessToken, UserEntity user) { + if (user == null) { + throw new AuthException(AuthRCode.USER_NOT_EXIST); + } + accessToken.setUserId(user.getId()); + accessToken.getMetadata().put("userId", String.valueOf(user.getId())); + accessToken.getMetadata().put("username", user.getUsername()); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java new file mode 100644 index 0000000..cf55f16 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java @@ -0,0 +1,70 @@ +package com.blossom.backend.base.auth; + +import java.util.ArrayList; +import java.util.List; + +/** + * 授权模块静态参数 + * + * @author xzzz + * @since 0.0.1 + */ +public class AuthConstant { + + /** + * token 所在的请求头名称 + */ + public static final String HEADER_AUTHORIZATION = "Authorization"; + + /** + * token 前缀, 遵循 Oauth2.0 规范 (RFC6750: https://tools.ietf.org/html/rfc6750) + *

+ * 参考文档: + * https://tools.ietf.org/html/RFC6750 + * https://learning.postman.com/docs/sending-requests/authorization/ + */ + public static final String HEADER_TOKEN_PREFIX = "Bearer "; + + /** + * Client 配置不踢出其他前一个 Token 时, token_unique 中记录的值 + */ + public static final String UNIQUE_TOKEN_EVERY_WHERE = "Client允许多处登录"; + + /** + * 标识请求时白名单, 为后续过滤器判断使用 + */ + public static final String WHITE_LIST_ATTRIBUTE_KEY = "IS_WHITE_LIST"; + + /** + * 默认忽略的请求 + */ + public static final List DEFAULT_WHITE_LIST = new ArrayList() { + private static final long serialVersionUID = -1; + { + // 登录接口 + this.add("/login"); + // 获取图片验证码接口 + this.add("/captcha/image"); + // 获取手机验证码接口 + this.add("/captcha/phone"); + // 一些默认的本地静态资源, 一些非前后端分离的静态文件, 如某些框架自带的操作界面(如 swagger)等 + this.add("/favicon.ico"); + this.add("/**/**.js"); + this.add("/**/**.css"); + // swagger 默认请求路径 + this.add("/doc.html"); + this.add("/webjars/**"); + this.add("/swagger-resources"); + this.add("/v2/**"); + } + }; + + /** + * 请求体包装过滤器的顺序 + */ + public static final int AUTH_FILTER_WRAPPER_ORDER = -101; + /** + * 代理过滤器的顺序 + */ + public static final int AUTH_FILTER_PROXY = -100; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java new file mode 100644 index 0000000..092920d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java @@ -0,0 +1,104 @@ +package com.blossom.backend.base.auth; + +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.redis.RedisTokenValidateFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * 授权上下文 + *

+ * 设置: {@link RedisTokenValidateFilter} + * 删除: {@link RedisTokenAuthFilterProxy#doFilter(ServletRequest, ServletResponse, FilterChain)} + * + * @author xzzz + * @since 0.0.1 + */ +public class AuthContext { + + private static final ThreadLocal TOKEN_CONTEXT = new ThreadLocal<>(); + + // region + + /** + * 获取授权上下文 + * + * @return 授权信息 + */ + public static AccessToken getContext() { + return TOKEN_CONTEXT.get(); + } + + /** + * 设置上下文信息 + * + * @param accessToken 授权信息 + */ + public static void setContext(AccessToken accessToken) { + TOKEN_CONTEXT.set(accessToken); + } + + /** + * 删除上下文 + */ + public static void removeContext() { + TOKEN_CONTEXT.remove(); + } + + /** + * 获取token 信息 + * + * @return token + */ + public static String getToken() { + return TOKEN_CONTEXT.get().getToken(); + } + + // endregion + + /** + * 获取UID + * + * @return 用户ID + */ + public static Long getUserId() { + AccessToken accessToken = getContext(); + if (accessToken == null) { + return null; + } + return getContext().getUserId(); + } + + /** + * 获取BID + * + * @return 商铺 ID + */ + public static Map getUserMetadata() { + AccessToken accessToken = getContext(); + if (accessToken == null) { + return new HashMap<>(0); + } + return accessToken.getMetadata(); + } + + /** + * 用户用户ID及用户名 + */ + public static String getIdAndName() { + String nameAndId = "0,Null"; + try { + AccessToken accessToken = getContext(); + if (accessToken != null) { + nameAndId = getUserId() + "," + getUserMetadata().get("username"); + } + } catch (Exception ignored) { + // 空的上下文时忽略 + } + return nameAndId; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java new file mode 100644 index 0000000..62af39e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java @@ -0,0 +1,70 @@ +package com.blossom.backend.base.auth; + +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.pojo.LoginReq; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * 登录授权 + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +public class AuthController { + + private final AuthService authService; + + /** + * 登录[OP] + * + * @param req 请求对象 + * @return token信息 + */ + @AuthIgnore + @PostMapping("login") + public R login(HttpServletRequest request, @Validated @RequestBody LoginReq req) { + XzException400.throwBy(StrUtil.isBlank(req.getClientId()), "客户端ID[ClientId]为必填项"); + XzException400.throwBy(StrUtil.isBlank(req.getGrantType()), "授权方式[GrantType]为必填项"); + return R.ok(authService.login(request, req)); + } + + /** + * 用户退出 + * + * @apiNote 如果为 JWT 授权方式, 则退出功能无效, 调用该接口后 Token 仍然可以正常使用。 + * 客户端可以正常调用该接口以校验 Token 是否有效, 然后自行从 Storage 或 Cookie 中删除 Token 即可。 + */ + @PostMapping("logout") + public R logout() { + authService.logout(AuthContext.getToken()); + return R.ok(); + } + + /** + * 用户主动注册 + */ + @PostMapping("register") + public void register() { + + } + + /** + * 检查 token 状态 + */ + @GetMapping("check") + public R check() { + return R.ok(authService.check()); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java new file mode 100644 index 0000000..791e989 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java @@ -0,0 +1,275 @@ +package com.blossom.backend.base.auth; + + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.base.auth.enums.AuthTypeEnum; +import com.blossom.backend.base.auth.enums.GrantTypeEnum; +import com.blossom.backend.base.auth.enums.LogTypeEnum; +import com.blossom.backend.base.auth.enums.PasswordEncoderEnum; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 授权配置项 + * + * @author xzzz + */ +@Slf4j +@Data +@Configuration +@ConfigurationProperties(prefix = "project.auth") +public class AuthProperties implements EnvironmentAware, ApplicationContextAware { + + /** + * 是否启用授权功能 + */ + private Boolean enabled = Boolean.TRUE; + /** + * 使用JWT + */ + private AuthTypeEnum type = AuthTypeEnum.jwt; + /** + * 默认密码 + */ + private String defaultPassword = "123456"; + /** + * 日志类型 + */ + private LogTypeEnum logType = LogTypeEnum.none; + /** + * 默认加密方式 + */ + private PasswordEncoderEnum passwordEncoder = PasswordEncoderEnum.bcrypt; + /** + * 授权平台配置,不同的平台类型生成的 token 时效是不同的 + *

+ * 该类创建完成后会检查是否配置平台数据, 未自定义平台配置会创建默认配置 + * + * @see AuthProperties#initAfterProcessorClient() + */ + private List clients; + /** + * platforms 转为 map, 方便查询 + */ + private Map clientMap; + /** + * 白名单列表 + * 配置时不需要增加context-path,会自动拼接 + */ + private List whiteList; + + @PostConstruct + public void init() { + // 检查配置的授权类型是否正确 + initAfterProcessorTypeCheck(); + // 白名单后置处理 + initAfterProcessorWhiteList(); + // 平台配置后置处理 + initAfterProcessorClient(); + } + + /** + * 白名单后置处理 + */ + private void initAfterProcessorWhiteList() { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map methodMap = mapping.getHandlerMethods(); + methodMap.forEach((key, value) -> { + if (null != AnnotationUtil.getAnnotation(value.getMethod(), AuthIgnore.class)) { + if (key.getPatternsCondition() != null) { + Set urls = key.getPatternsCondition().getPatterns(); + whiteList.addAll(urls); + } + } + }); + + String contentPath = env.getProperty(SpringUtil.SERVLET_CONTEXT_PATH); + // 设置默认登陆页白名单 + if (CollUtil.isEmpty(whiteList)) { + this.whiteList = new ArrayList<>(); + } + whiteList.addAll(AuthConstant.DEFAULT_WHITE_LIST); + this.whiteList = this.whiteList.stream() + .map(whiteUrl -> Optional.ofNullable(contentPath).orElse("") + whiteUrl) + .collect(Collectors.toList()); + } + + /** + * 平台配置后置处理,设置默认平台类型 + */ + private void initAfterProcessorClient() { + // 初始化客户端列表 + if (CollUtil.isEmpty(this.clients)) { + this.clientMap = new HashMap<>(1); + this.clients = new ArrayList<>(1); + } + + final Client defaultClient = Client.getDefault(); + + log.info("[AUTHORIZ] 授权类型:{}, Client:客户端, Expire(h):授权时间(小时), Refresh:请求刷新授权, Unique:用户Token唯一", type); + log.info("[AUTHORIZ] ┌──────────┬───────────┬─────────┬────────────┐"); + log.info("[AUTHORIZ] | ClientId | Expire(h) | Refresh | MultiPlace |"); + log.info("[AUTHORIZ] ├──────────┼───────────┼─────────┼────────────┤"); + for (Client client : this.clients) { + // 配置文件有配置,但配置不全,则未配置的参数使用默认配置 + if (client.getDuration() == null || client.getDuration() == 0) { + client.setDuration(defaultClient.getDuration()); + } + if (client.getRequestRefresh() == null) { + client.setRequestRefresh(defaultClient.getRequestRefresh()); + } + if (client.getMultiPlaceLogin() == null) { + client.setMultiPlaceLogin(defaultClient.getMultiPlaceLogin()); + } + if (CollUtil.isEmpty(client.getGrantType())) { + client.setGrantType(defaultClient.getGrantType()); + } + if (AuthTypeEnum.jwt.equals(type)) { + client.setRequestRefresh(false); + client.setMultiPlaceLogin(true); + } + log.info("[AUTHORIZ] | {}| {}| {}| {}| {}", + StrUtil.fillAfter(client.getClientId(), StrUtil.C_SPACE, 9), + StrUtil.fillAfter(client.getDuration() / 3600L + "", StrUtil.C_SPACE, 10), + StrUtil.fillAfter(String.valueOf(client.getRequestRefresh()), StrUtil.C_SPACE, 8), + StrUtil.fillAfter(String.valueOf(client.getMultiPlaceLogin()), StrUtil.C_SPACE, 11), + client.getGrantType() + ); + } + // 转换为 map, 有相同的 type, 后者会覆盖前者 + this.clientMap = this.clients.stream().collect(Collectors.toMap(Client::getClientId, Function.identity(), (key1, key2) -> key2)); + log.info("[AUTHORIZ] └──────────┴───────────┴─────────┴────────────┘"); + } + + private void initAfterProcessorTypeCheck() { + if (type.equals(AuthTypeEnum.redis) && !ClassLoaderUtil.isPresent("org.springframework.data.redis.core.RedisTemplate")) { + String msg = "授权方式 [project.auth.type = redis] 配置错误, 当前项目未使用 Redis, 无法使用 Redis 授权方式."; + log.error(msg); + throw new XzException500(msg); + } + } + + /** + * 授权客户端的配置 + */ + public static class Client { + /** + * 编码 + */ + private String clientId; + /** + * 默认过期时间, 单位为秒 + */ + private Integer duration; + /** + * 每次请求刷新 + */ + private Boolean requestRefresh; + /** + * 登录时是否踢出上一个登录的 token,注意,该配置修改后,并不会改变在之前下发的 Token 的校验逻辑 + *

为 true 则每次登录 token 会替换前一个 token。 + *

为 false 则每次登录返回的 token 是一样的。 + */ + private Boolean multiPlaceLogin; + /** + * 该平台允许的登录方式 + */ + private List grantType; + + /** + * 获取默认平台配置 + * + * @return 平台配置 + * @see AuthProperties#initAfterProcessorClient + */ + public static Client getDefault() { + Client client = new Client(); + client.setDuration(60 * 60 * 4); + client.setRequestRefresh(Boolean.TRUE); + client.setMultiPlaceLogin(Boolean.TRUE); + client.setGrantType(CollUtil.newArrayList(GrantTypeEnum.PASSWORD.getType())); + return client; + } + + public Client() { + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public Integer getDuration() { + return duration; + } + + public void setDuration(Integer duration) { + this.duration = duration; + } + + public Boolean getRequestRefresh() { + return requestRefresh; + } + + public void setRequestRefresh(Boolean requestRefresh) { + this.requestRefresh = requestRefresh; + } + + public Boolean getMultiPlaceLogin() { + return multiPlaceLogin; + } + + public void setMultiPlaceLogin(Boolean multiPlaceLogin) { + this.multiPlaceLogin = multiPlaceLogin; + } + + public List getGrantType() { + return grantType; + } + + public void setGrantType(List grantType) { + this.grantType = grantType; + } + } + + /** + * 环境配置 + */ + private Environment env; + + @Override + public void setEnvironment(Environment environment) { + this.env = environment; + } + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java new file mode 100644 index 0000000..c08ccdd --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java @@ -0,0 +1,76 @@ +package com.blossom.backend.base.auth; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.backend.base.auth.pojo.LoginDTO; +import com.blossom.backend.base.auth.security.PasswordEncoder; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.repo.TokenRepository; +import com.blossom.backend.base.auth.token.TokenEncoder; +import com.blossom.backend.base.user.UserService; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.common.base.enums.YesNo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * 授权服务 + * + * @author xzzz + */ +@Slf4j +@Service +public class AuthService extends AbstractAuthService { + + @Autowired + private UserService userService; + + public AuthService(AuthProperties properties, + TokenRepository tokenRepository, + TokenEncoder tokenEncoder, + PasswordEncoder passwordEncoder, + ApplicationContext applicationContext) { + super(properties, tokenRepository, tokenEncoder, passwordEncoder, applicationContext); + } + + /** + * 根据用户名密码登录 + */ + @Override + protected void loginByPassword(AccessToken accessToken, LoginDTO login) { + AuthException.throwBy(StrUtil.isBlank(login.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT); + UserEntity user = userService.selectByUsername(login.getUsername()); + AuthException.throwBy(ObjUtil.isNull(user), AuthRCode.USERNAME_OR_PWD_FAULT); + AuthException.throwBy(!passwordEncoder.matches(login.getPassword() + user.getSalt(), user.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT); + fillUserDetail(accessToken, user); + } + + /** + * 退出 + * + * @param token token 令牌 + */ + public void logout(String token) { + tokenRepository.remove(token); + } + + /** + * 用户注册 + */ + public void register() { + + } + + /** + * 检查 AccessToken 信息 + * + * @return + */ + public AccessToken check() { + return AuthContext.getContext(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java new file mode 100644 index 0000000..8114f81 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java @@ -0,0 +1,40 @@ +package com.blossom.backend.base.auth; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.util.spring.SpringUtil; + + +/** + * @author xzzz + * @since 0.0.1 + */ +public class TokenUtil { + + private static final String TOKEN_KEY = "auth:token"; + private static final String TOKEN_UNIQUE_KEY = "auth:token_unique"; + + public static String buildTokenKey(String token) { + return SpringUtil.getAppName() + ":" + TOKEN_KEY + ":" + token; + } + + public static String buildUniqueTokenKey(String token) { + return SpringUtil.getAppName() + ":" + TOKEN_UNIQUE_KEY + ":" + token; + } + + /** + * 截取 token 前缀(Bearer), 返回 token + * + * @param tokenStr token 字符串 + * @return token + */ + public static String cutPrefix(String tokenStr) { + if (tokenStr == null || tokenStr.length() == 0) { + return null; + } + if (StrUtil.startWith(tokenStr, AuthConstant.HEADER_TOKEN_PREFIX)) { + return StrUtil.replace(tokenStr, AuthConstant.HEADER_TOKEN_PREFIX, ""); + } + return tokenStr; + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java new file mode 100644 index 0000000..83f73d6 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java @@ -0,0 +1,12 @@ +package com.blossom.backend.base.auth.annotation; + +import java.lang.annotation.*; + +/** + * 拦截器中为接口参数添加用户ID + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AssignUser { +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java new file mode 100644 index 0000000..010a062 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java @@ -0,0 +1,14 @@ +package com.blossom.backend.base.auth.annotation; + +import java.lang.annotation.*; + +/** + * 白名单注解标识 + * + * @author xzzz + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthIgnore { +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java new file mode 100644 index 0000000..65036c3 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java @@ -0,0 +1,21 @@ +package com.blossom.backend.base.auth.annotation; + +import java.lang.annotation.*; + +/** + * 权限校验 + * + * @author xzzz + * @since 0.0.1 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PermCheck { + + /* + 所需要的授权 + */ + String value(); +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java new file mode 100644 index 0000000..1fd130c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java @@ -0,0 +1,18 @@ +package com.blossom.backend.base.auth.enums; + +/** + * 授权类型 + * + * @author xzzz + */ +public enum AuthTypeEnum { + /** + * 有状态的授权方式, Token 按自定义逻辑生成, 并存储在 Redis 中, 该种授权可以退出和主动删除 + */ + redis, + + /** + * 无状态的授权方式, Token 使用 JWT + */ + jwt +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java new file mode 100644 index 0000000..3c43c6e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java @@ -0,0 +1,27 @@ +package com.blossom.backend.base.auth.enums; + +import lombok.Getter; + +/** + * 授权方式 + * + * @author xzzz + */ +public enum GrantTypeEnum { + + /** + * 暂时只支持 password + */ + PASSWORD("password", "密码登录, 需要传入用户名和密码"); + + @Getter + private final String type; + + @Getter + private final String desc; + + GrantTypeEnum(String type, String desc) { + this.type = type; + this.desc = desc; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java new file mode 100644 index 0000000..0f8409c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java @@ -0,0 +1,21 @@ +package com.blossom.backend.base.auth.enums; + +/** + * 打印请求日志的类型 + * + * @author xzzz + */ +public enum LogTypeEnum { + /** + * 不打印请求日志 + */ + none, + /** + * 打印简单的请求日志, 只有请求地址 + */ + simple, + /** + * 答应详细的请求日志, 包含请求头, 请求参数, application/json 类型的请求体 + */ + detail; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java new file mode 100644 index 0000000..1bda661 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java @@ -0,0 +1,21 @@ +package com.blossom.backend.base.auth.enums; + +/** + * 密码加密方式 + * + * @author xzzz + */ +public enum PasswordEncoderEnum { + /** + * md5 与 sha256 为匹配 hash 值. + */ + md5, + /** + * md5 与 sha256 为匹配 hash 值. + */ + sha256, + /** + * bcrypt 安全性最高, 但加解密最慢. + */ + bcrypt; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java new file mode 100644 index 0000000..e06920c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java @@ -0,0 +1,39 @@ +package com.blossom.backend.base.auth.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzAbstractException; +import com.blossom.common.base.pojo.IRCode; +import com.blossom.common.base.pojo.RCode; + +/** + * 授权相关异常 + * + * @author xzzz + */ +public class AuthException extends XzAbstractException { + + public AuthException(AuthRCode authRCode) { + super(authRCode); + } + + public AuthException(String code, String message) { + super(code, message); + } + + public static void throwBy(boolean expression, IRCode authRCode) { + if (authRCode == null) { + authRCode = RCode.BAD_REQUEST; + } + throwBy(expression, authRCode.getCode(), authRCode.getMsg()); + } + + public static void throwBy(boolean expression, String code, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = RCode.BAD_REQUEST.getMsg(); + } + throw new AuthException(code, msg); + } + } + +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java new file mode 100644 index 0000000..b84a226 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java @@ -0,0 +1,77 @@ +package com.blossom.backend.base.auth.exception; + + +import com.blossom.common.base.pojo.IRCode; +import lombok.Getter; + +/** + * 授权响应码 + * + * @author xzzz + */ +public enum AuthRCode implements IRCode { + + /** + * 400: 请求路径不合法 + */ + REQUEST_REJECTED ("AUTH-40001", "请求不合法","请求不合法:请求路径中可能包含不规范的内容。"), + + /** + * 400: 登录参数请求参数不合法 + */ + INVALID_GRANT_TYPE ("AUTH-40002", "无效的授权方式","无效的授权方式:[GrantType] 字段错误, 请查看传入值是否在数据字典[GrantType]中。"), + INVALID_CLIENT_ID ("AUTH-40003", "无效的客户端","无效的客户端:[ClientId] 字段错误, 请求的客户端没有在服务器配置。"), + + /** + * 400: 登录时发生错误 + */ + USERNAME_OR_PWD_FAULT ("AUTH-40004", "用户名或密码错误","用户名或密码错误, 或用户名不存在。"), + CAPTCHA_FAULT ("AUTH-40005", "验证码错误","验证码错误, 或手机号不存在。"), + USER_NOT_ENABLED ("AUTH-40010", "用户已禁用, 暂时无法登录","用户已禁用, 暂时无法登录。"), + + /** + * 401: 未经过认证 + *

指身份验证是必需的, 没有提供身份验证或身份验证失败。如果请求已经包含授权凭据, 那么401状态码表示不接受这些凭据。 + */ + INVALID_TOKEN ("AUTH-40101", "无效的授权信息", + "无效的授权信息\n请求时的令牌错误, 可以通过 AUTH-40101 来判断登录超时来跳转至登录页等。"), + + ANOTHER_DEVICE_LOGIN ("AUTH-40102", "账号已在其他设备登录", + "账号已在其他设备登录。\n本账号在其他设备登录时, 本设备下次请求接口时会出现该错误, 该错误出现之后。\n再次使用该令牌访问时会响应 \"AUTH-40101\", 出现该错误通常需要提示用户后跳转至登录页。"), + + /** + * 403: 被禁止 + *

指示尽管请求有效, 但服务器拒绝响应它。与401状态码不同, 提供身份验证不会改变结果。 + */ + PERMISSION_DENIED ("AUTH-40302", "你没有权限访问该资源","你没有权限访问该资源。\n没有权限访问对应 API 接口或服务器资源。"), + + /** + * 404:找不到请求 + * + */ + USER_NOT_EXIST ("AUTH-40401", "用户不存在","用户不存在。\n通常不会返回该信息, 而是根据 [GrantType] 提示相应参数错误"), + ; + + @Getter + final String code; + @Getter + final String msg; + @Getter + final String desc; + + AuthRCode(String code, String msg, String desc) { + this.code = code; + this.msg = msg; + this.desc = desc; + } + + public static AuthRCode getByCode(String code) { + for (AuthRCode value : AuthRCode.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return null; + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java new file mode 100644 index 0000000..379134a --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java @@ -0,0 +1,33 @@ +package com.blossom.backend.base.auth.exception; + +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.exception.AbstractExceptionAdvice; +import com.blossom.common.base.exception.XzAbstractException; +import com.blossom.common.base.pojo.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 授权异常处理 + * + *

并不是所有授权阶段发生的异常都在此捕获, 例如有些异常在过滤器无法通过 Advice 捕获, 该种异常不需要继承 {@link XzAbstractException} + * + * @author xzzz + */ +@Slf4j +@Order(-1) +@RestControllerAdvice +public class ExceptionAdviceByAuth extends AbstractExceptionAdvice { + + public ExceptionAdviceByAuth(BaseProperties baseProperties) { + super(baseProperties); + } + + @ExceptionHandler(AuthException.class) + public R authExceptionHandler(XzAbstractException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(exception.getCode(), exception.getMessage()); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java new file mode 100644 index 0000000..b3579f1 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java @@ -0,0 +1,128 @@ +package com.blossom.backend.base.auth.filters; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.AuthProperties; +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.enums.ExFormat; +import com.blossom.common.base.enums.ExStackTrace; +import com.blossom.common.base.pojo.IRCode; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.pojo.RCode; +import com.blossom.common.base.util.ExceptionUtil; +import com.blossom.common.base.util.ServletUtil; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Optional; + +/** + * 授权过滤器抽象类, 授权有 JWT 和 有状态TOKEN 两种实现方式, 所以具体的过滤器逻辑需要 + * 子类重写 {@link AuthFilterProxy#doFilterInternal } 方法 + * + * @author xzzz + */ +@Slf4j +public abstract class AuthFilterProxy extends GenericFilterBean { + /** + * 授权配置 + */ + protected final AuthProperties properties; + + public AuthFilterProxy(AuthProperties properties) { + this.properties = properties; + } + + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!properties.getEnabled()) { + chain.doFilter(request, response); + } else { + log.debug("[AUTHORIZ] ================================ 代理过滤器 [开始] ================================"); + try { + this.doFilterInternal(request, response, chain); + } catch (Exception e) { + log.debug("[AUTHORIZ] **Proxy** >> 代理过滤器执行异常: {}", e.getMessage()); + onAuthenticationFailure(request, response, e); + } finally { + // 无论执行成功与否都需要清空上下文 + AuthContext.removeContext(); + log.debug("[AUTHORIZ] **Proxy** << 代理过滤器: response 清空上下文: {}", JsonUtil.toJson(AuthContext.getContext())); + } + log.debug("[AUTHORIZ] ================================ 代理过滤器 [结束] ================================"); + } + } + + /** + * 执行过滤器, 由不同的子过滤器实现具体的过滤器内容 + * + * @param request + * @param response + * @param chain + * @throws IOException + * @throws ServletException + */ + protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; + + /** + * 执行时出现异常, 在此捕获并统一处理 + * + * @param request request + * @param response response + * @param exception ex + */ + protected void onAuthenticationFailure(ServletRequest request, ServletResponse response, Exception exception) throws IOException { + HttpServletResponse resp = (HttpServletResponse) response; + + resp.setContentType("application/json;charset=utf-8"); + // 要求所有响应都为 200 + resp.setStatus(200); + + PrintWriter out = resp.getWriter(); + + // 自定义响应码 + IRCode authCode = RCode.INTERNAL_SERVER_ERROR; + if (exception instanceof AuthException) { + authCode = AuthRCode.getByCode(((AuthException) exception).getCode()); + } + + printStackTrace(authCode, exception); + + R apiResult = R.fault( + authCode.getCode(), + authCode.getMsg(), + exception.getMessage()); + String resultJson = JsonUtil.toJson(apiResult); + out.write(resultJson); + out.flush(); + out.close(); + } + + protected void printStackTrace(IRCode authCode, Exception exception) { + boolean filterStackTrace = false; + boolean onLine = false; + + try { + BaseProperties properties = SpringUtil.getBean(BaseProperties.class); + filterStackTrace = properties.getEx().getStackTrace().equals(ExStackTrace.project); + onLine = properties.getEx().getFormat().equals(ExFormat.line); + } catch (Exception e) { + e.printStackTrace(); + } + + log.error("{} | {}({})", ServletUtil.getUri(), exception.getClass().getSimpleName(), + Optional.ofNullable(authCode.getMsg()).orElse("")); + log.error(ExceptionUtil.printStackTrace(exception, filterStackTrace, onLine)); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/HttpFirewall.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/HttpFirewall.java new file mode 100644 index 0000000..e7a03d2 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/HttpFirewall.java @@ -0,0 +1,251 @@ +package com.blossom.backend.base.auth.filters; + +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import org.springframework.http.HttpMethod; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * 请求链接防火墙 + * + * @author xzzz + * @since 0.0.1 + */ +public class HttpFirewall { + + /** + * 请求地址中不允许出现「.」的 URL 编码 %2e + */ + private static final List FORBIDDEN_ENCODED_PERIOD = + Collections.unmodifiableList(Arrays.asList("%2e", "%2E")); + + /** + * 请求地址中不允许出现「;」以及「;」的 URL 编码 %3b + */ + private static final List FORBIDDEN_SEMICOLON = + Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B")); + + /** + * 请求地址中不允许出现「/」的 URL 编码 %2f + */ + private static final List FORBIDDEN_FORWARDSLASH = + Collections.unmodifiableList(Arrays.asList("%2f", "%2F")); + + /** + * 请求地址中不允许出现「//」以及「//」的 URL 编码 %2f%2f + */ + private static final List FORBIDDEN_DOUBLE_FORWARDSLASH = + Collections.unmodifiableList(Arrays.asList("//", "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F")); + + /** + * 请求地址中不允许出现「\」以及「\」的 URL 编码 %5c + */ + private static final List FORBIDDEN_BACKSLASH = + Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C")); + + /** + * 请求地址中不允许出现「」以及「」的 URL 编码 %00 + * (不是 null 而是空) 可转码 %00 查看, 转码后什么都没有 + */ + private static final List FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00")); + + /** + * 请求地址中不允许出现「%」以及「%」的 URL 编码 %25 + */ + private static final String PERCENT = "%"; + private static final String ENCODED_PERCENT = "%25"; + + private final Set encodedUrlBlockList = new HashSet<>(); + private final Set decodedUrlBlockList = new HashSet<>(); + + public HttpFirewall() { + // 请求地址中不允许出现「.」的 URL 编码 %2e + this.encodedUrlBlockList.addAll(FORBIDDEN_ENCODED_PERIOD); + this.decodedUrlBlockList.addAll(FORBIDDEN_ENCODED_PERIOD); + // 请求地址中不允许出现「;」以及「;」的 URL 编码 %3b + this.encodedUrlBlockList.addAll(FORBIDDEN_SEMICOLON); + this.decodedUrlBlockList.addAll(FORBIDDEN_SEMICOLON); + // 请求地址中不允许出现「/」的 URL 编码 %2f + this.encodedUrlBlockList.addAll(FORBIDDEN_FORWARDSLASH); + this.decodedUrlBlockList.addAll(FORBIDDEN_FORWARDSLASH); + // 请求地址中不允许出现「//」以及「//」的 URL 编码 %2f%2f + this.encodedUrlBlockList.addAll(FORBIDDEN_DOUBLE_FORWARDSLASH); + this.decodedUrlBlockList.addAll(FORBIDDEN_DOUBLE_FORWARDSLASH); + // 请求地址中不允许出现「\」以及「\」的 URL 编码 %5c + this.encodedUrlBlockList.addAll(FORBIDDEN_BACKSLASH); + this.decodedUrlBlockList.addAll(FORBIDDEN_BACKSLASH); + // 请求地址中不允许出现「」以及「」的 URL 编码 %00 + this.encodedUrlBlockList.addAll(FORBIDDEN_NULL); + this.decodedUrlBlockList.addAll(FORBIDDEN_NULL); + // 请求地址中不允许出现 %, 以及 % 的 URL 编码 %25 + this.encodedUrlBlockList.add(ENCODED_PERCENT); + this.decodedUrlBlockList.add(PERCENT); + } + + /** + * 允许的请求方式 + */ + private final Set allowedHttpMethods = createDefaultAllowedHttpMethods(); + private static Set createDefaultAllowedHttpMethods() { + Set result = new HashSet<>(); + result.add(HttpMethod.DELETE.name()); + result.add(HttpMethod.GET.name()); + result.add(HttpMethod.HEAD.name()); + result.add(HttpMethod.OPTIONS.name()); + result.add(HttpMethod.PATCH.name()); + result.add(HttpMethod.POST.name()); + result.add(HttpMethod.PUT.name()); + return result; + } + + /** + * 执行防火墙 + * @param servletRequest request + */ + public void wall(ServletRequest servletRequest) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + + // 判断请求方式 + boolean isForbiddenMethod = rejectForbiddenHttpMethod(request); + if (!isForbiddenMethod) { + throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求方式错误 [" + request.getMethod() + "], 只允许如下请求: " + this.allowedHttpMethods); + } + + // 是否是无效的 url 请求 + boolean isNormalized = isNormalized(request); + if (!isNormalized) { + throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求链接格式错误, 请检查!"); + } + + // 判断请求连接中的非法字符 + rejectedBlockListedUrls(request); + + // 如果请求地址中包含不可打印的 ASCII 字符,请求则会被拒绝 + boolean isContainsOnlyPrintableAsciiCharacters = containsOnlyPrintableAsciiCharacters(request); + if (!isContainsOnlyPrintableAsciiCharacters) { + throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求链接包含非法字符, 请检查!"); + } + } + + /** + * 判断请求方式 + * @param request request + */ + private boolean rejectForbiddenHttpMethod(HttpServletRequest request) { + return this.allowedHttpMethods.contains(request.getMethod()); + } + + /** + * 标准化 URL 请求, 路径中不能包含 + * ./ + * /../ + * /. + * + * @param request 请求 + * @return 是否 + */ + private boolean isNormalized(HttpServletRequest request) { + if (!isNormalized(request.getRequestURI())) { + return false; + } + if (!isNormalized(request.getContextPath())) { + return false; + } + if (!isNormalized(request.getServletPath())) { + return false; + } + if (!isNormalized(request.getPathInfo())) { + return false; + } + return true; + } + + /** + * 标准化 URL 请求, 路径中不能包含 + * ./ + * /../ + * /. + * + * @param path 请求路径 + * @return 是否 + */ + private boolean isNormalized(String path) { + if (path == null) { + return true; + } + for (int i = path.length(); i > 0;) { + int slashIndex = path.lastIndexOf('/', i - 1); + int gap = i - slashIndex; + if (gap == 2 && path.charAt(slashIndex + 1) == '.') { + // ".", "/./" or "/." + return false; + } + if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') { + return false; + } + i = slashIndex; + } + return true; + } + + /** + * 如果请求地址中包含不可打印的 ASCII 字符,请求则会被拒绝 + * @param request request + * @return 是否 + */ + private static boolean containsOnlyPrintableAsciiCharacters(HttpServletRequest request) { + String requestUri = request.getRequestURI(); + int length = requestUri.length(); + for (int i = 0; i < length; i++) { + char c = requestUri.charAt(i); + if (c < '\u0020' || c > '\u007e') { + return false; + } + } + return true; + } + + /** + * 请求中不能包含 // 双斜杠 + * @param request request + */ + private void rejectedBlockListedUrls(HttpServletRequest request) { + for (String forbidden : this.encodedUrlBlockList) { + if (encodedUrlContains(request, forbidden)) { + throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求被拒绝, 请求路径中包含非法字符 [" + forbidden + "]"); + } + } + for (String forbidden : this.decodedUrlBlockList) { + if (decodedUrlContains(request, forbidden)) { + throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求被拒绝, 请求路径中包含非法字符 [" + forbidden + "]"); + } + } + } + + + + private boolean encodedUrlContains(HttpServletRequest request, String value) { + if (valueContains(request.getContextPath(), value)) { + return true; + } + return valueContains(request.getRequestURI(), value); + } + + private boolean decodedUrlContains(HttpServletRequest request, String value) { + if (valueContains(request.getServletPath(), value)) { + return true; + } + if (valueContains(request.getPathInfo(), value)) { + return true; + } + return false; + } + + private boolean valueContains(String value, String contains) { + return value != null && value.contains(contains); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestBodyReaderWrapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestBodyReaderWrapper.java new file mode 100644 index 0000000..84c9640 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestBodyReaderWrapper.java @@ -0,0 +1,67 @@ +package com.blossom.backend.base.auth.filters; + +import cn.hutool.core.io.IoUtil; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * request 封装类,用于实现多次读取 requet body + * + * @author xzzz + * @since 0.0.1 + */ +public class RequestBodyReaderWrapper extends HttpServletRequestWrapper { + + private byte[] body; + + public RequestBodyReaderWrapper(HttpServletRequest request) throws IOException { + super(request); + body = IoUtil.readBytes(request.getInputStream(), false); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() throws IOException { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + + }; + } + + public void setInputStream(byte[] body) { + this.body = body; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestLogFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestLogFilter.java new file mode 100644 index 0000000..ae77374 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestLogFilter.java @@ -0,0 +1,84 @@ +package com.blossom.backend.base.auth.filters; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.blossom.backend.base.auth.AuthProperties; +import com.blossom.backend.base.auth.enums.LogTypeEnum; +import com.blossom.common.base.util.SystemUtil; +import com.blossom.common.base.util.json.JsonUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author wangyf + * @since 0.0.1 + */ +@Slf4j +public class RequestLogFilter { + /** + * 配置文件内容 + */ + private AuthProperties properties; + + public RequestLogFilter(AuthProperties properties) { + this.properties = properties; + } + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (properties.getLogType().equals(LogTypeEnum.none)) { + return; + } + + // 显示请求路径 + try { + if (properties.getLogType().equals(LogTypeEnum.simple)) { + HttpServletRequest req = ((HttpServletRequest) request); + String uri = req.getRequestURI(); + log.info("[AUTHORIZ] IP:{} > {}", StrUtil.fillAfter(SystemUtil.getRemoteIp(req), ' ', 15), uri); + } + + // 显示请求详情,此配置会覆盖显示请求路径 + if (properties.getLogType().equals(LogTypeEnum.detail)) { + HttpServletRequest req = ((HttpServletRequest) request); + + Enumeration headerNames = req.getHeaderNames(); + Map headers = new HashMap<>(); + while (headerNames.hasMoreElements()) { + String key = headerNames.nextElement(); + String value = req.getHeader(key); + headers.put(key, value); + } + + String requestBody = ""; + if (request.getContentType() != null && request.getContentType().contains("application/json")) { + try { + requestBody = req.getReader().lines().collect(Collectors.joining(System.lineSeparator())); + } catch (Exception e) { + log.warn("请求体转换错误"); + } + } + + log.warn("[AUTHORIZ] 请求详情 [{}:{}] From [{}]" + + "\n请求参数: [{}]" + + "\n请求体 : [{}]" + + "\n请求头 : [{}]", + req.getMethod(), req.getRequestURL().toString(), SystemUtil.getRemoteIp(req), + StrUtil.isBlank(req.getQueryString()) ? "" : URLUtil.decode(req.getQueryString()), + requestBody, + JsonUtil.toPrettyJson(headers) + ); + } + } catch (Exception e) { + log.warn("输出日志错误:" + e.getMessage()); + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestWrapperFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestWrapperFilter.java new file mode 100644 index 0000000..7be7189 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/RequestWrapperFilter.java @@ -0,0 +1,38 @@ +package com.blossom.backend.base.auth.filters; + +import com.blossom.backend.base.auth.AuthConstant; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 包装request, 提供多次读取请求体的机会 + * + * @author wangyf + * @since 0.0.1 + */ +@Slf4j +@Component +@Order(AuthConstant.AUTH_FILTER_WRAPPER_ORDER) +public class RequestWrapperFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // ContentType 为 "application/json" 格式才使用包装类 + // 这样是为了过滤上传文件等类型的请求 + if (request.getContentType() != null && request.getContentType().contains("application/json")) { + ServletRequest requestWrapper = new RequestBodyReaderWrapper(request); + filterChain.doFilter(requestWrapper, response); + } else { + filterChain.doFilter(request, response); + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/WhiteListFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/WhiteListFilter.java new file mode 100644 index 0000000..9cba110 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/WhiteListFilter.java @@ -0,0 +1,57 @@ +package com.blossom.backend.base.auth.filters; + +import com.blossom.backend.base.auth.AuthConstant; +import com.blossom.backend.base.auth.AuthProperties; +import com.blossom.common.base.util.spring.AntPathMatcherUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * [0] + * + * 白名单拦截器, 不一定作为拦截器链的入口 + * 通过获取配置文件中的白名单列表, 对请求(URI)进行判断 + * 支持 Ant 风格, 使用 {@link org.springframework.util.AntPathMatcher} 类进行匹配 + * 在白名单中的请求不会执行后续过滤器链, 即无法获取授权上下文 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class WhiteListFilter { + + /** + * 配置文件内容 + */ + private final AuthProperties properties; + + public WhiteListFilter(AuthProperties properties) { + this.properties = properties; + } + + /** + * 如果不在白名单 isWhiteRequest = false, 则需要走授权校验逻辑, 即继续代理过滤器链流程 + * 如果在白名单中 isWhiteRequest = true, 则中断过滤器链, 代理过滤器会调用后续原生过滤器 + * + * @param request request + * @param response response + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + log.debug("[AUTHORIZ] filter(000) 白名单校验"); + + String uri = ((HttpServletRequest)request).getRequestURI(); + // 如果请求路径在白名单中, 则允许略过请求 + for (String pattern : properties.getWhiteList()) { + if (AntPathMatcherUtil.match(pattern, uri)) { + request.setAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY,true); + break; + } + } + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/interceptor/UserTypeInterceptor.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/interceptor/UserTypeInterceptor.java new file mode 100644 index 0000000..ca331b0 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/interceptor/UserTypeInterceptor.java @@ -0,0 +1,56 @@ +package com.blossom.backend.base.auth.interceptor; + +import cn.hutool.core.util.ObjUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.base.user.UserService; +import com.blossom.backend.base.user.UserTypeEnum; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException404; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Slf4j +public class UserTypeInterceptor implements HandlerInterceptor { + + private UserService userService; + + @Autowired + public void setUserService(UserService userService) { + this.userService = userService; + } + + /** + * @param request request + * @param response response + * @return 通过与否 + * @throws Exception 异常处理 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + //必须强转为HandlerMethod + HandlerMethod handlerMethod = (HandlerMethod) handler; + + // 白名单不校验 + boolean isIgnore = handlerMethod.hasMethodAnnotation(AuthIgnore.class); + if (isIgnore) { + return true; + } + // GET请求校验 + if (HttpMethod.GET.name().equals(request.getMethod())) { + return true; + } + + UserEntity user = userService.selectById(AuthContext.getUserId()); + XzException404.throwBy(ObjUtil.isNull(user), "未查询到你的账户信息"); + XzException400.throwBy(UserTypeEnum.READONLY.getType().equals(user.getType()), "您的账号为只读账号, 无法使用该功能"); + return true; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTAuthFilterProxy.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTAuthFilterProxy.java new file mode 100644 index 0000000..5c6425a --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTAuthFilterProxy.java @@ -0,0 +1,87 @@ +package com.blossom.backend.base.auth.jwt; + +import com.blossom.backend.base.auth.AuthConstant; +import com.blossom.backend.base.auth.AuthProperties; +import com.blossom.backend.base.auth.filters.AuthFilterProxy; +import com.blossom.backend.base.auth.filters.HttpFirewall; +import com.blossom.backend.base.auth.filters.RequestLogFilter; +import com.blossom.backend.base.auth.filters.WhiteListFilter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * JWT 过滤器实现 + * + * @author xzzz + */ +@Slf4j +@Component +@Order(AuthConstant.AUTH_FILTER_PROXY) +@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt", matchIfMissing = true) +public class JWTAuthFilterProxy extends AuthFilterProxy { + + /** + * 防火墙对象 + */ + private final HttpFirewall httpFirewall = new HttpFirewall(); + /** + * 日志过滤器 + */ + private final RequestLogFilter logFilter; + /** + * 白名单过滤器 + */ + private final WhiteListFilter whiteListFilter; + /** + * JWT校验过滤器 + */ + private final JWTValidateFilter validateFilter; + + public JWTAuthFilterProxy(AuthProperties properties, JWTTokenEncoder tokenEncoder) { + super(properties); + this.logFilter = new RequestLogFilter(properties); + this.whiteListFilter = new WhiteListFilter(properties); + this.validateFilter = new JWTValidateFilter(tokenEncoder); + } + + + /** + *

1. 防火墙校验 + *

2. 授权校验 + *

    + *
  1. 日志
  2. + *
  3. 白名单
  4. + *
  5. 授权校验
  6. + *
+ * + * @param request request + * @param response response + * @param chain chain + */ + @Override + protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + /* + 1. 执行防火墙 + */ + httpFirewall.wall(request); + + /* + 2. 授权校验 + */ + // 2.1 日志 + logFilter.doFilter(request, response); + // 2.2 白名单 + whiteListFilter.doFilter(request, response); + // 2.3 token 校验 + validateFilter.doFilter(request, response); + chain.doFilter(request, response); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenEncoder.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenEncoder.java new file mode 100644 index 0000000..e58ed63 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenEncoder.java @@ -0,0 +1,128 @@ +package com.blossom.backend.base.auth.jwt; + +import cn.hutool.core.util.StrUtil; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.blossom.backend.base.auth.token.TokenEncoder; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.base.util.spring.SpringUtil; +import com.blossom.backend.base.auth.pojo.AccessToken; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +/** + * jwt 在线解密解码:https://www.box3.cn/tools/jwt.html + * + * @author xzzz + */ +@Slf4j +@Component +@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt") +public class JWTTokenEncoder implements TokenEncoder, EnvironmentAware { + /** + * 加密字符串 + */ + private final String secret = "T1h22WstOUaStOiUnAuCYif3Kw7DeLUciE8iWVAReqdnJTnJ7n4WYtE6x0UW"; + /** + * 生成签名 + */ + private final Algorithm signer = Algorithm.HMAC256(secret); + private Environment environment; + private String appName; + + @Override + public void setEnvironment(@NotNull Environment environment) { + this.environment = environment; + } + + @PostConstruct + public void init() { + this.appName = environment.getProperty(SpringUtil.APP_NAME); + } + + /** + * token 对象转为 jwt + * + * @param accessToken token + * @return jwt 字符串 + */ + @Override + public String encode(AccessToken accessToken) { + try { + return JWT.create() + .withIssuer(appName) + // 主题,科目 + .withSubject(accessToken.getClientId()) + // 自定义字段 + .withClaim("expire", accessToken.getExpire()) + .withClaim("duration", accessToken.getDuration()) + .withClaim("loginTime", accessToken.getLoginTime()) + .withClaim("userId", accessToken.getUserId()) + .withClaim("metadata", JsonUtil.toJson(accessToken.getMetadata())) + // 过期时间 + .withExpiresAt(DateUtils.date(accessToken.getExpire())) + .sign(signer); + } catch (Exception e) { + log.error("授权发生异常:", e); + throw new XzException500("JWT授权异常"); + } + } + + /** + * 解码 jwt + * + * @param token jwt + * @return token 对象 + */ + @Override + public AccessToken decode(String token) { + try { + JWTVerifier verifier = JWT.require(signer).build(); + DecodedJWT jwt = verifier.verify(token); + AccessToken accessToken = new AccessToken(); + accessToken.setToken(token); + + Long userId = jwt.getClaim("userId").asLong(); + if (userId == null || StrUtil.isBlank(jwt.getClaim("metadata").asString())) { + return null; + } + + accessToken.setExpire(jwt.getClaim("expire").asLong()); + accessToken.setDuration(jwt.getClaim("duration").asInt()); + accessToken.setLoginTime(jwt.getClaim("loginTime").asString()); + accessToken.setUserId(jwt.getClaim("userId").asLong()); + accessToken.setMetadata(JsonUtil.toMap(jwt.getClaim("metadata").asString())); + return accessToken; + } catch (SignatureVerificationException e) { + log.error("token 解码时 Signature 错误"); + throw e; + } catch (JWTDecodeException e) { + log.error("token 解码错误"); + throw e; + } catch (TokenExpiredException e) { + log.error("token 解码超时"); + throw e; + } catch (IllegalArgumentException e) { + log.error("token 解码参数错误"); + throw e; + } catch (JWTVerificationException e) { + log.error("token 解码失败"); + throw e; + } + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenRepository.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenRepository.java new file mode 100644 index 0000000..5535b53 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTTokenRepository.java @@ -0,0 +1,43 @@ +package com.blossom.backend.base.auth.jwt; + +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.repo.TokenRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * JWT 存储, JWT 无状态无需存储 + * + * @author xzzz + */ +@Slf4j +@Component +@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt") +public class JWTTokenRepository implements TokenRepository { + + @Override + public void saveToken(AccessToken accessToken) { + + } + + @Override + public AccessToken getToken(String token) { + return null; + } + + @Override + public void remove(String token) { + + } + + @Override + public void saveUniqueToken(AccessToken accessToken) { + + } + + @Override + public String getUniqueToken(String userId) { + return null; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTValidateFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTValidateFilter.java new file mode 100644 index 0000000..841bd8d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/jwt/JWTValidateFilter.java @@ -0,0 +1,153 @@ +package com.blossom.backend.base.auth.jwt; + + +import cn.hutool.core.util.StrUtil; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.blossom.backend.base.auth.AuthConstant; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.TokenUtil; +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.common.base.util.ServletUtil; +import com.blossom.backend.base.auth.pojo.AccessToken; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 请求验证, 校验请求是否携带 token, 未携带的进行拦截跑出错误 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class JWTValidateFilter { + + private final JWTTokenEncoder jwtTokenEncoder; + + public JWTValidateFilter(JWTTokenEncoder jwtTokenEncoder) { + this.jwtTokenEncoder = jwtTokenEncoder; + } + + /** + * 执行过滤器 + * + * @param request request + * @param response response + * @throws IOException io + * @throws ServletException servlet + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + log.debug("[AUTHORIZ] filter(100) 授权校验过滤器"); + + boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY)); + + // 解析请求头中的 token + String token = getHeaderToken(request); + + // 如果白名单且无token, 则直接放行 + if (isWhiteList && (StrUtil.isBlank(token) || AuthConstant.HEADER_TOKEN_PREFIX.trim().equals(token))) { + return; + } + + AccessToken accessToken = null; + try { + try { + accessToken = jwtTokenEncoder.decode(token); + } catch (SignatureVerificationException e) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } catch (JWTDecodeException e) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } catch (TokenExpiredException e) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } catch (IllegalArgumentException e) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } catch (JWTVerificationException e) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } + } catch (AuthException e) { + if (!isWhiteList) { + throw e; + } + } + + // 如果是白名单请求没有授权信息,则构造空的授权信息到上下文,异常由调用方捕获 + if (isWhiteList && null == accessToken) { + AccessToken accessTokenNull = new AccessToken(); + accessTokenNull.setToken(""); + accessTokenNull.setUserId(0L); + setAuthContext(accessTokenNull); + return; + } + + // 如果白名单请求,且获得了token,则说明是登录状态请求白名单的接口,正常执行下一过滤器 + if (isWhiteList && null != accessToken) { + // 将授权数据存放到上下文中,不执行后续过滤器 + setAuthContext(accessToken); + return; + } + // 如果不是白名单请求,则抛出错误 + if (!isWhiteList && null == accessToken) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } + + // 将授权数据存放到上下文中,并执行下一过滤器 + setAuthContext(accessToken); + } + + /** + * 1. 从请求头中获取 Token + * + * @param servletRequest request + * @return Token + */ + private String getHeaderToken(ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + // 从请求头获取 + String token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION); + + // 从请求头获取全小写 + if (StrUtil.isBlank(token)) { + token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toLowerCase()); + } + + // 从请求头获取全大写 + if (StrUtil.isBlank(token)) { + token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toUpperCase()); + } + + // 从 cookie 获取 + if (StrUtil.isBlank(token)) { + Cookie cookie = ServletUtil.getCookie(httpServletRequest, AuthConstant.HEADER_AUTHORIZATION); + if (cookie != null) { + token = cookie.getValue(); + } + } + + // 以 Bearer 开头 && 除此外长度 > 0 + token = TokenUtil.cutPrefix(token); + if (null != token && token.length() > 0) { + return token; + } + + return null; + } + + /** + * 3. 保存到上下文 + * + * @param accessToken 授权主体 + */ + private void setAuthContext(AccessToken accessToken) { + AuthContext.setContext(accessToken); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/AccessToken.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/AccessToken.java new file mode 100644 index 0000000..a0e22bc --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/AccessToken.java @@ -0,0 +1,76 @@ +package com.blossom.backend.base.auth.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * token 对象 + * + * @author xzzz + */ +@Data +public class AccessToken implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 登录令牌 + */ + private String token; + + /** + * 到期时间 + */ + private Long expire; + + /** + * 授权方式 + */ + private String grantType; + + /** + * 登录平台 + */ + private String clientId; + + /** + * 请求是否刷新Token + */ + private Boolean requestRefresh; + + /** + * 用户 Token 唯一: + *

为 false 则每次登录返回的 token 是一样的; + *

为 true 则每次登录 token 会替换前一个 token + */ + private Boolean multiPlaceLogin; + + /** + * 授权时长, 单位为秒 + */ + private Integer duration; + + /** + * 用户ID + */ + private Long userId; + + /** + * 登录日期 + */ + private String loginTime; + + /** + * 权限列表 + */ + private List permissions; + + /** + * 用户元信息, 由用户自定义各类信息 + */ + private Map metadata; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginDTO.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginDTO.java new file mode 100644 index 0000000..1fb9846 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginDTO.java @@ -0,0 +1,35 @@ +package com.blossom.backend.base.auth.pojo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 登录信息 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginDTO extends LoginReq { + + /** + * 用户ID + */ + private Long userId; + /** + * userAgent + */ + private String userAgent; + /** + * ip + */ + private String ip; + /** + * 登录时间 + */ + private String loginTime; + /** + * token 快照 + */ + private String accessTokenSnapshot; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginEvent.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginEvent.java new file mode 100644 index 0000000..f843b9e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginEvent.java @@ -0,0 +1,15 @@ +package com.blossom.backend.base.auth.pojo; + +import org.springframework.context.ApplicationEvent; + +/** + * 登录事件 + * + * @author xzzz + */ +public class LoginEvent extends ApplicationEvent { + + public LoginEvent(LoginDTO source) { + super(source); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginReq.java new file mode 100644 index 0000000..668581f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/pojo/LoginReq.java @@ -0,0 +1,40 @@ +package com.blossom.backend.base.auth.pojo; + +import com.blossom.backend.base.auth.enums.GrantTypeEnum; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotEmpty; + +/** + * 登录请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginReq extends AbstractPOJO { + + /** + * 登录方式,可见数据字典中[GrantType]部分, 必须为小写 + * + * @see GrantTypeEnum + */ + @NotEmpty(message = "授权方式[GrantType]为必填项") + private String grantType; + /** + * 客户端ID + */ + @NotEmpty(message = "客户端ID[ClientId]为必填项") + private String clientId; + + /** + * 用户名, grantType = password 时必填 + */ + private String username; + /** + * 密码, grantType = password 时必填 + */ + private String password; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenAuthFilterProxy.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenAuthFilterProxy.java new file mode 100644 index 0000000..7e587e8 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenAuthFilterProxy.java @@ -0,0 +1,111 @@ +//package com.blossom.backend.base.auth.redis; +// +//import com.blossom.backend.base.auth.AuthConstant; +//import com.blossom.backend.base.auth.AuthProperties; +//import com.blossom.backend.base.auth.repo.TokenRepository; +//import com.blossom.backend.base.auth.filters.AuthFilterProxy; +//import com.blossom.backend.base.auth.filters.HttpFirewall; +//import com.blossom.backend.base.auth.filters.RequestLogFilter; +//import com.blossom.backend.base.auth.filters.WhiteListFilter; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.core.annotation.Order; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.stereotype.Component; +// +//import javax.servlet.FilterChain; +//import javax.servlet.ServletException; +//import javax.servlet.ServletRequest; +//import javax.servlet.ServletResponse; +//import java.io.IOException; +// +///** +// * redis token 拦截器实现 +// * +// * @author xzzz +// */ +//@Slf4j +//@Component +//@Order(AuthConstant.AUTH_FILTER_PROXY) +//@ConditionalOnClass(RedisTemplate.class) +//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis") +//public class RedisTokenAuthFilterProxy extends AuthFilterProxy { +// /** +// * 防火墙对象 +// */ +// protected final HttpFirewall httpFirewall = new HttpFirewall(); +// /** +// * 日期过滤器 +// */ +// protected final RequestLogFilter logFilter; +// /** +// * 白名单过滤器 +// */ +// protected final WhiteListFilter whiteListFilter; +// /** +// * 授权检查过滤器 +// */ +// private final RedisTokenValidateFilter redisTokenValidateFilter; +// /** +// * 唯一授权检查过滤器 +// */ +// private final RedisTokenUniqueFilter redisTokenUniqueFilter; +// /** +// * 授权重置过滤器 +// */ +// private final RedisTokenExpireResetFilter redisTokenExpireResetFilter; +// +// public RedisTokenAuthFilterProxy(AuthProperties properties, TokenRepository tokenRepository) { +// super(properties); +// this.logFilter = new RequestLogFilter(properties); +// this.whiteListFilter = new WhiteListFilter(properties); +// this.redisTokenValidateFilter = new RedisTokenValidateFilter(tokenRepository); +// this.redisTokenUniqueFilter = new RedisTokenUniqueFilter(tokenRepository); +// this.redisTokenExpireResetFilter = new RedisTokenExpireResetFilter(tokenRepository); +// } +// +// /** +// *

1. 防火墙校验 +// *

2. 授权校验 +// *

    +// *
  1. 日志
  2. +// *
  3. 白名单
  4. +// *
  5. 授权校验
  6. +// *
  7. 授权续期
  8. +// *
+// * +// * @param request request +// * @param response response +// * @param chain chain +// */ +// @Override +// protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { +// /* +// 1. 执行防火墙 +// */ +// httpFirewall.wall(request); +// +// /* +// 2. 授权校验 +// */ +// // 2.1 日志 +// logFilter.doFilter(request, response); +// // 2.2 白名单 +// whiteListFilter.doFilter(request, response); +// // 2.3 token 校验 +// redisTokenValidateFilter.doFilter(request, response); +// // 2.4 token 唯一校验 +// redisTokenUniqueFilter.doFilter(request, response); +// // 2.5 过期时间刷新 +// redisTokenExpireResetFilter.doFilter(request, response); +// +// /* +// 虚拟过滤器链执行完毕后交由原生过滤器继续执行 +// */ +// chain.doFilter(request, response); +// +// log.debug("[AUTHORIZ] **Proxy** >> 原生过滤器: request 请求结束"); +// } +// +//} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenEncoder.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenEncoder.java new file mode 100644 index 0000000..51e0505 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenEncoder.java @@ -0,0 +1,31 @@ +//package com.blossom.backend.base.auth.redis; +// +//import cn.hutool.core.lang.UUID; +//import com.blossom.backend.base.auth.pojo.AccessToken; +//import com.blossom.backend.base.auth.token.TokenEncoder; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.stereotype.Component; +// +///** +// * token 编解码器, 简单生成一个 UUID token, 与 token 信息无加解密, 编解码关系 +// * +// * @author xzzz +// */ +//@Component +//@ConditionalOnClass(RedisTemplate.class) +//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis") +//public class RedisTokenEncoder implements TokenEncoder { +// +// /** +// * 生成一个 uuid 作为 token +// * +// * @param accessToken token +// * @return uuid +// */ +// @Override +// public String encode(AccessToken accessToken) { +// return UUID.randomUUID().toString(true); +// } +//} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenExpireResetFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenExpireResetFilter.java new file mode 100644 index 0000000..7a186be --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenExpireResetFilter.java @@ -0,0 +1,60 @@ +package com.blossom.backend.base.auth.redis; + +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.repo.TokenRepository; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * [200] + * + * 令牌刷新, 对令牌进行刷新. + * 不同类型的令牌刷新不同 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class RedisTokenExpireResetFilter { + + /** + * 配置文件内容 + */ + private final TokenRepository tokenRepository; + + public RedisTokenExpireResetFilter(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + /** + * 执行过滤器 + * @param request request + * @param response response + * @throws IOException io + * @throws ServletException servlet + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + log.debug("[AUTHORIZ] filter(200) 令牌续期"); + + // 1. 上下文获取授权主体 + AccessToken accessToken = AuthContext.getContext(); + + // 2. 如果配置客户端有效 && 客户端允许刷新令牌 + if (accessToken != null && StrUtil.isNotBlank(accessToken.getToken()) && accessToken.getRequestRefresh()) { + // 3.1 刷新存储中的授权信息 + accessToken.setExpire(System.currentTimeMillis() + (accessToken.getDuration() * 1000)); + tokenRepository.saveToken(accessToken); + tokenRepository.saveUniqueToken(accessToken); + // 3.2 刷新上下文授权信息 + AuthContext.setContext(accessToken); + } else { + + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenRepository.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenRepository.java new file mode 100644 index 0000000..de3ec56 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenRepository.java @@ -0,0 +1,85 @@ +//package com.blossom.backend.base.auth.redis; +// +//import cn.hutool.core.util.StrUtil; +//import com.blossom.backend.base.auth.TokenUtil; +//import com.blossom.backend.base.auth.pojo.AccessToken; +//import com.blossom.backend.base.auth.repo.TokenRepository; +//import com.blossom.common.base.exception.XzException500; +//import com.blossom.common.base.util.json.JsonUtil; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.scheduling.annotation.Async; +//import org.springframework.stereotype.Component; +// +//import java.util.concurrent.TimeUnit; +// +///** +// * redis token 存储 +// * +// * @author xzzz +// */ +//@Slf4j +//@Component +//@ConditionalOnClass(RedisTemplate.class) +//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis") +//public class RedisTokenRepository implements TokenRepository { +// +// @Autowired +// private StringRedisTemplate redisTemplate; +// +// @Override +// public void saveToken(AccessToken accessToken) { +// if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) { +// throw new XzException500("无法保存空的AccessToken"); +// } +// redisTemplate.opsForValue().set( +// TokenUtil.buildTokenKey(accessToken.getToken()), +// JsonUtil.toJson(accessToken), +// accessToken.getDuration(), +// TimeUnit.SECONDS); +// } +// +// @Override +// public AccessToken getToken(String token) { +// if (StrUtil.isBlank(token)) { +// return null; +// } +// return JsonUtil.toObj(redisTemplate.opsForValue().get(TokenUtil.buildTokenKey(token)), AccessToken.class); +// } +// +// @Async +// @Override +// public void remove(String token) { +// log.info("异步删除 token"); +// redisTemplate.delete(TokenUtil.buildTokenKey(token)); +// } +// +// @Override +// public void saveUniqueToken(AccessToken accessToken) { +// if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) { +// throw new XzException500("无法保存空的AccessToken"); +// } +// +// if (accessToken.getMultiPlaceLogin()) { +// return; +// } +// +// redisTemplate.opsForValue().set( +// TokenUtil.buildUniqueTokenKey(String.valueOf(accessToken.getUserId())), +// accessToken.getToken(), +// accessToken.getDuration(), +// TimeUnit.SECONDS); +// } +// +// @Override +// public String getUniqueToken(String userId) { +// if (StrUtil.isBlank(userId)) { +// return null; +// } +// return redisTemplate.opsForValue().get(TokenUtil.buildUniqueTokenKey(userId)); +// } +//} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenUniqueFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenUniqueFilter.java new file mode 100644 index 0000000..ee0b0d1 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenUniqueFilter.java @@ -0,0 +1,63 @@ +package com.blossom.backend.base.auth.redis; + +import com.blossom.backend.base.auth.AuthConstant; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.repo.TokenRepository; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * redis 唯一token 拦截器, 用于处理用户多设备登录时的 唯一有效token 判断逻辑 + * + * @author xzzz + */ +@Slf4j +public class RedisTokenUniqueFilter { + + /** + * 配置文件内容 + */ + private final TokenRepository tokenRepository; + + public RedisTokenUniqueFilter(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + /** + * 执行过滤器 + * + * @param request request + * @param response response + * @throws IOException io + * @throws ServletException servlet + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY)); + + if (isWhiteList) { + return; + } + + AccessToken accessToken = AuthContext.getContext(); + + /* + * 如果不允许多地登录, 则要判断当前用户是否是唯一的用户 + */ + if (!accessToken.getMultiPlaceLogin()) { + String uniqueToken = tokenRepository.getUniqueToken(String.valueOf(accessToken.getUserId())); + if (!accessToken.getToken().equals(uniqueToken)) { + tokenRepository.remove(accessToken.getToken()); + throw new AuthException(AuthRCode.ANOTHER_DEVICE_LOGIN); + } + } + } + + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenValidateFilter.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenValidateFilter.java new file mode 100644 index 0000000..de84b64 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/redis/RedisTokenValidateFilter.java @@ -0,0 +1,131 @@ +package com.blossom.backend.base.auth.redis; + + +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.AuthConstant; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.TokenUtil; +import com.blossom.backend.base.auth.exception.AuthException; +import com.blossom.backend.base.auth.exception.AuthRCode; +import com.blossom.backend.base.auth.pojo.AccessToken; +import com.blossom.backend.base.auth.repo.TokenRepository; +import com.blossom.common.base.util.ServletUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 请求验证, 校验请求是否携带 token, 未携带的进行拦截跑出错误 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class RedisTokenValidateFilter { + + private final TokenRepository tokenRepository; + + public RedisTokenValidateFilter(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + /** + * 执行过滤器 + * + * @param request request + * @param response response + * @throws IOException io + * @throws ServletException servlet + */ + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + log.debug("[AUTHORIZ] filter(100) 授权校验过滤器"); + + boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY)); + + // 解析请求头中的 token + String token = getHeaderToken(request); + + // 如果白名单且无token, 则直接放行 + if (isWhiteList && StrUtil.isBlank(token)) { + return; + } + + // 根据 token 取出 Redis 中的授权数据 + AccessToken accessToken = tokenRepository.getToken(token); + + // 如果是白名单请求没有授权信息,则构造空的授权信息到上下文,异常由调用方捕获 + if (isWhiteList && null == accessToken) { + AccessToken accessTokenNull = new AccessToken(); + accessTokenNull.setToken(""); + accessTokenNull.setUserId(0L); + setAuthContext(accessTokenNull); + return; + } + + // 如果白名单请求,且获得了token,则说明是登录状态请求白名单的接口,正常执行下一过滤器 + if (isWhiteList && null != accessToken) { + // 将授权数据存放到上下文中,不执行后续过滤器 + setAuthContext(accessToken); + return; + } + // 如果不是白名单请求,则抛出错误 + if (!isWhiteList && null == accessToken) { + throw new AuthException(AuthRCode.INVALID_TOKEN); + } + // 将授权数据存放到上下文中,并执行下一过滤器 + setAuthContext(accessToken); + } + + /** + * 1. 从请求头中获取 Token + * + * @param servletRequest request + * @return Token + */ + private String getHeaderToken(ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + // 从请求头获取 + String token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION); + + // 从请求头获取全小写 + if (StrUtil.isBlank(token)) { + token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toLowerCase()); + } + + // 从请求头获取全大写 + if (StrUtil.isBlank(token)) { + token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toUpperCase()); + } + + // 从 cookie 获取 + if (StrUtil.isBlank(token)) { + Cookie cookie = ServletUtil.getCookie(httpServletRequest, AuthConstant.HEADER_AUTHORIZATION); + if (cookie != null) { + token = cookie.getValue(); + } + } + + // 以 Bearer 开头 && 除此外长度 > 0 + token = TokenUtil.cutPrefix(token); + if (null != token && token.length() > 0) { + return token; + } + + return null; + } + + /** + * 3. 保存到上下文 + * + * @param accessToken 授权主体 + */ + private void setAuthContext(AccessToken accessToken) { + AuthContext.setContext(accessToken); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/repo/TokenRepository.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/repo/TokenRepository.java new file mode 100644 index 0000000..e8caad9 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/repo/TokenRepository.java @@ -0,0 +1,49 @@ +package com.blossom.backend.base.auth.repo; + +import com.blossom.backend.base.auth.pojo.AccessToken; + +/** + * token 存储 + * + * @author xzzz + */ +public interface TokenRepository { + + /** + * 保存 token + * + * @param accessToken token 对象信息 + */ + void saveToken(AccessToken accessToken); + + /** + * 获取 token 对象信息 + * + * @param token token 令牌 + * @return token + */ + AccessToken getToken(String token); + + /** + * 删除 Token 信息 + * + * @param token token 令牌 + */ + void remove(String token); + + /** + * 保存唯一生效的 token 对象 + * + * @param accessToken 唯一生效的 token 信息 + */ + void saveUniqueToken(AccessToken accessToken); + + /** + * 获取唯一生效的 token 令牌 + * + * @param userId 用户ID + * @return 该用户有效的 token + */ + String getUniqueToken(String userId); + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoder.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoder.java new file mode 100644 index 0000000..ad72674 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoder.java @@ -0,0 +1,42 @@ +package com.blossom.backend.base.auth.security; + +/** + * 密码加密方式接口 + * + * @author xzzz + * @since 0.0.1 + */ +public interface PasswordEncoder { + + /** + * 明文加密 + *

+ * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or + * greater hash combined with an 8-byte or greater randomly generated salt. + * + * @param rawPassword 密码原文 + * @return 密文 + */ + String encode(CharSequence rawPassword); + + /** + * 密码判断,用未加密的密码与已加密的密码进行判断 + * + * @param rawPassword 原始密码,未加密的密码 + * @param encodedPassword 已经加密过的密码 + * @return true 为密码正确 + */ + boolean matches(CharSequence rawPassword, String encodedPassword); + + /** + * Returns true if the encoded password should be encoded again for better security, + * else false. The default implementation always returns false. + * + * @param encodedPassword the encoded password to check + * @return true if the encoded password should be encoded again for better security, + * else false. + */ + default boolean upgradeEncoding(String encodedPassword) { + return false; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderBCrypt.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderBCrypt.java new file mode 100644 index 0000000..f71cfde --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderBCrypt.java @@ -0,0 +1,32 @@ +package com.blossom.backend.base.auth.security; + +import com.blossom.common.base.util.security.BCryptUtil; +import com.blossom.common.base.util.security.SaltUtil; + +/** + * bcrypt 加解密 + * + * @author xzzz + */ +public class PasswordEncoderBCrypt implements PasswordEncoder { + + @Override + public String encode(CharSequence rawPassword) { + return BCryptUtil.encode(String.valueOf(rawPassword)); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return BCryptUtil.matches(rawPassword, encodedPassword); + } + + public static void main(String[] args) { + System.out.println(SaltUtil.randomSalt()); + + String salt = "UVeESP5NgXwb8JmjCHUK"; + String password = "blos"; + PasswordEncoderBCrypt uuid = new PasswordEncoderBCrypt(); + System.out.println(uuid.encode(password + salt)); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderMD5.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderMD5.java new file mode 100644 index 0000000..04bdf10 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderMD5.java @@ -0,0 +1,21 @@ +package com.blossom.backend.base.auth.security; + +import cn.hutool.crypto.SecureUtil; + +/** + * md5 加解密匹配 + * + * @author xzzz + */ +public class PasswordEncoderMD5 implements PasswordEncoder { + + @Override + public String encode(CharSequence rawPassword) { + return SecureUtil.md5(rawPassword.toString()); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return SecureUtil.md5(rawPassword.toString()).equals(encodedPassword); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderSHA256.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderSHA256.java new file mode 100644 index 0000000..16e4867 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordEncoderSHA256.java @@ -0,0 +1,21 @@ +package com.blossom.backend.base.auth.security; + +import com.blossom.common.base.util.security.SHA256Util; + +/** + * sha256 加解密匹配 + * + * @author xzzz + */ +public class PasswordEncoderSHA256 implements PasswordEncoder{ + + @Override + public String encode(CharSequence rawPassword) { + return SHA256Util.encode(String.valueOf(rawPassword)); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return SHA256Util.encode(String.valueOf(rawPassword)).equals(encodedPassword); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordManager.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordManager.java new file mode 100644 index 0000000..08ff940 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/security/PasswordManager.java @@ -0,0 +1,57 @@ +package com.blossom.backend.base.auth.security; + +import com.blossom.backend.base.auth.AuthProperties; +import com.blossom.backend.base.auth.enums.PasswordEncoderEnum; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * 密码处理 + * + * @author xzzz + */ +@Primary +@Component +public class PasswordManager implements PasswordEncoder { + + private final AuthProperties authProperties; + private final PasswordEncoder passwordEncoder; + + public PasswordManager(AuthProperties authProperties) { + this.authProperties = authProperties; + if (authProperties.getPasswordEncoder() == PasswordEncoderEnum.md5) { + passwordEncoder = new PasswordEncoderMD5(); + } else if (authProperties.getPasswordEncoder() == PasswordEncoderEnum.sha256) { + passwordEncoder = new PasswordEncoderSHA256(); + } else { + passwordEncoder = new PasswordEncoderBCrypt(); + } + } + + public String getDefaultPassword() { + return passwordEncoder.encode(authProperties.getDefaultPassword()); + } + + /** + * 明文加密, 如果密码需要加盐, 需要加盐后传入 + * + * @param rawPassword 密码明文 + * @return 密码密文 + */ + @Override + public String encode(CharSequence rawPassword) { + return passwordEncoder.encode(rawPassword); + } + + /** + * 密码判断,用未加密的密码与已加密的密码进行判断 + * + * @param rawPassword 原始密码,未加密的密码 + * @param encodedPassword 已经加密过的密码 + * @return true 为密码正确 + */ + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/token/TokenEncoder.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/token/TokenEncoder.java new file mode 100644 index 0000000..d795fb2 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/token/TokenEncoder.java @@ -0,0 +1,29 @@ +package com.blossom.backend.base.auth.token; + +import com.blossom.backend.base.auth.pojo.AccessToken; + +/** + * token 编解码器 + * + * @author xzzz + */ +public interface TokenEncoder { + + /** + * token 编码成字符串 + * + * @param accessToken token + * @return 字符串 + */ + String encode(AccessToken accessToken); + + /** + * token 解码 + * + * @param string token 字符串 + * @return token + */ + default AccessToken decode(String string) { + return null; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamEnum.java new file mode 100644 index 0000000..6c8ce93 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamEnum.java @@ -0,0 +1,42 @@ +package com.blossom.backend.base.param; + +import lombok.Getter; + +/** + * 参数枚举 + * + * @author xzzz + */ +public enum ParamEnum { + + /** + * 文章的 web 端访问路径 + */ + WEB_ARTICLE_URL(false), + + /** + * 文章日志过期天数 + */ + ARTICLE_LOG_EXP_DAYS(false), + + /** + * 和风天气KEY + */ + HEFENG_KEY(true), + + /** + * GITEE key + */ + GITEE_ACCESS_TOKEN(true), + ; + + /** + * 是否脱敏 + */ + @Getter + private final Boolean masking; + + ParamEnum(Boolean masking) { + this.masking = masking; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamMapper.java new file mode 100644 index 0000000..d45683f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamMapper.java @@ -0,0 +1,17 @@ +package com.blossom.backend.base.param; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.base.param.pojo.ParamEntity; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 系统参数信息的持久化层 + * + * @author xzzz + */ +@Mapper +public interface ParamMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamService.java new file mode 100644 index 0000000..311aaf3 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/ParamService.java @@ -0,0 +1,79 @@ +package com.blossom.backend.base.param; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.base.param.pojo.ParamEntity; +import com.blossom.backend.base.param.pojo.ParamReq; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.util.BeanUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.*; + +/** + * 系统参数信息业务接口 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ParamService extends ServiceImpl { + + private static final Map CACHE = new HashMap<>(20); + + @PostConstruct + public void refresh() { + log.info("[ BASE] 初始化系统参数缓存"); + CACHE.clear(); + List params = baseMapper.selectList(new QueryWrapper<>()); + if (CollUtil.isEmpty(params)) { + return; + } + for (ParamEntity param : params) { + CACHE.put(param.getParamName(), param); + } + } + + public ParamEntity getValue(ParamEnum name) { + return CACHE.get(name.name()); + } + + /** + * 根据多个名称查询 + * + * @param masking 返回数据是否脱敏 + * @param names 参数名称 + * @return 返回参数 map + */ + public Map selectMap(boolean masking, ParamEnum... names) { + if (ArrayUtil.isEmpty(names)) { + return new HashMap<>(0); + } + Map result = new HashMap<>(names.length); + for (ParamEnum name : names) { + ParamEntity param = BeanUtil.toObj(CACHE.get(name.name()), ParamEntity.class); + if (masking && name.getMasking()) { + param.setParamValue(StrUtil.hide(param.getParamValue(), 0, Math.min(param.getParamValue().length(), 20))); + } + result.put(name.name(), param.getParamValue()); + } + return result; + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public Long update(ParamReq req) { + XzException404.throwBy(req.getId() == null, "ID不得为空"); + return req.getId(); + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamEntity.java new file mode 100644 index 0000000..9a4fccd --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamEntity.java @@ -0,0 +1,37 @@ +package com.blossom.backend.base.param.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 系统参数信息的实体类 + * + * @author xzzz + */ +@Data +@TableName("base_sys_param") +@EqualsAndHashCode(callSuper = true) +public class ParamEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 参数ID */ + @TableId + private Long id; + /** 参数名称 */ + private String paramName; + /** 参数值 */ + private String paramValue; + /** 参数说明 */ + private String paramDesc; + /** 创建时间 */ + private Date creTime; + /** 修改时间 */ + private Date updTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamPageReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamPageReq.java new file mode 100644 index 0000000..a1a7eb7 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamPageReq.java @@ -0,0 +1,33 @@ +package com.blossom.backend.base.param.pojo; + +import com.blossom.common.db.pojo.PageReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 系统参数分页请求类 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ParamPageReq extends PageReq implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 参数ID */ + private Long id; + /** 参数名称 */ + private String paramName; + /** 参数值 */ + private String paramValue; + /** 参数说明 */ + private String paramDesc; + /** 创建时间 */ + private Date creTime; + /** 修改时间 */ + private Date updTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamReq.java new file mode 100644 index 0000000..e89d328 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamReq.java @@ -0,0 +1,33 @@ +package com.blossom.backend.base.param.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 系统参数请求实体 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ParamReq extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 参数ID */ + private Long id; + /** 参数名称 */ + private String paramName; + /** 参数值 */ + private String paramValue; + /** 参数说明 */ + private String paramDesc; + /** 创建时间 */ + private Date creTime; + /** 修改时间 */ + private Date updTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamRes.java new file mode 100644 index 0000000..7d8e6d5 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/param/pojo/ParamRes.java @@ -0,0 +1,24 @@ +package com.blossom.backend.base.param.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 系统参数响应实体,会排除敏感字段 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ParamRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 参数名称 */ + private String paramName; + /** 参数值 */ + private String paramValue; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysController.java new file mode 100644 index 0000000..38d938c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysController.java @@ -0,0 +1,41 @@ +package com.blossom.backend.base.sys; + +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.base.sys.os.OSRes; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 系统功能 + * + * @apiNote 服务在线检查, 文件上传 + * @author xzzz + */ +@Slf4j +@RestController +@RequestMapping("/sys") +public class SysController { + + @Autowired + private SysService sysService; + + /** + * 服务在线检查 + */ + @AuthIgnore + @GetMapping("/alive") + public R checkAlive() { + return R.ok(String.format("这里是 [%s] 服务器 [%s] 环境", SpringUtil.getAppName(), SpringUtil.getProfileAction())); + } + + /** + * 对象存储的配置信息 + */ + @GetMapping("/osconfig") + public R getOsConfig() { + return R.ok(sysService.getOsConfig()); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysService.java new file mode 100644 index 0000000..e4f5de4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/SysService.java @@ -0,0 +1,33 @@ +package com.blossom.backend.base.sys; + +import com.blossom.backend.base.sys.os.OSRes; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 系统 + * + * @author xzzz + */ +@Service +public class SysService { + + @Autowired + private OSManager osManager; + + /** + * 对象存储的配置信息 + */ + public OSRes getOsConfig() { + IaasProperties props = osManager.getProp(); + OSRes res = new OSRes(); + res.setOsType(props.getOsType()); + // TODO 目前只有 BLOS 一种 + res.setBucketName(""); + res.setDefaultPath(props.getBlos().getDefaultPath()); + res.setDomain(props.getBlos().getDomain()); + return res; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/os/OSRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/os/OSRes.java new file mode 100644 index 0000000..363a6c0 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/sys/os/OSRes.java @@ -0,0 +1,29 @@ +package com.blossom.backend.base.sys.os; + +import lombok.Data; + +/** + * 对象存储配置 + * + * @author xzzz + */ +@Data +public class OSRes { + + /** + * 对象存储类型 + */ + private String osType; + /** + * bucket 名称 + */ + private String bucketName; + /** + * 请求域名 + */ + private String domain; + /** + * 保存路径 + */ + private String defaultPath; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserController.java new file mode 100644 index 0000000..ad6568a --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserController.java @@ -0,0 +1,96 @@ +package com.blossom.backend.base.user; + +import com.blossom.backend.base.param.ParamEnum; +import com.blossom.backend.base.param.ParamService; +import com.blossom.backend.base.sys.SysService; +import com.blossom.backend.base.user.pojo.*; +import com.blossom.backend.config.BlConstants; +import com.blossom.backend.server.article.draft.pojo.ArticleStatRes; +import com.blossom.backend.server.article.stat.ArticleStatService; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Map; + +/** + * 用户 [User] + * + * @author xzzz + */ +@RestController +@RequestMapping("/user") +@AllArgsConstructor +public class UserController { + private final UserService userService; + private final ArticleStatService articleService; + private final SysService sysService; + private final ParamService paramService; + + /** + * 查询 blossom 用户 + * + * @return 用户信息 + * @apiNote blossom 当前只支持一个用户登录, 数据并为做区分. 当登录状态调用该接口时, 会返回服务器相关配置信息. + */ + @GetMapping("/info") + public R user() { + BlossomUserRes user = userService.selectById(AuthContext.getUserId()).to(BlossomUserRes.class); + user.setOsRes(sysService.getOsConfig()); + Map paramMap = paramService.selectMap(true, ParamEnum.values()); + user.setParams(paramMap); + return R.ok(user); + } + + @AuthIgnore + @GetMapping("/info/open") + public R userOpen(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) { + if (userId == null) { + return R.ok(new BlossomUserRes()); + } + BlossomUserRes user = userService.selectById(userId).to(BlossomUserRes.class); + ArticleStatRes stat = articleService.statCount(null, null, userId); + user.setArticleWords(stat.getArticleWords()); + user.setArticleCount(stat.getArticleCount()); + return R.ok(user); + } + + /** + * 修改用户 + */ + @PostMapping("/upd") + public R update(@Validated @RequestBody UserUpdReq req) { + UserEntity user = req.to(UserEntity.class); + user.setId(AuthContext.getUserId()); + userService.updById(user); + return R.ok(); + } + + /** + * 修改密码 + */ + @PostMapping("/upd/pwd") + public R updatePassword(@Validated @RequestBody UserUpdPwdReq req) { + req.setUserId(AuthContext.getUserId()); + userService.updPassword(req); + return R.ok(); + } + + /** + * 新增用户 + */ + @PostMapping("/add") + public R add(@Validated @RequestBody UserAddReq req) { + UserEntity curUser = userService.getById(AuthContext.getUserId()); + if (curUser == null || !UserTypeEnum.ADMIN.getType().equals(curUser.getType())) { + throw new XzException400("您没有权限添加用户"); + } + userService.insert(req); + return R.ok(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserMapper.java new file mode 100644 index 0000000..9886cef --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserMapper.java @@ -0,0 +1,28 @@ +package com.blossom.backend.base.user; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.base.user.pojo.UserEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 用户 + * + * @author xzzz + */ +@Mapper +public interface UserMapper extends BaseMapper { + + /** + * 修改用户信息 + */ + void updById(UserEntity user); + + /** + * 修改密码 + * + * @param userId 用户ID + * @param password 新密码 + */ + void updPwd(@Param("userId") Long userId, @Param("password") String password); +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserService.java new file mode 100644 index 0000000..04fb219 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserService.java @@ -0,0 +1,100 @@ +package com.blossom.backend.base.user; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.security.PasswordEncoder; +import com.blossom.backend.base.user.pojo.UserAddReq; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.backend.base.user.pojo.UserUpdPwdReq; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.util.security.SaltUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.omg.PortableInterceptor.USER_EXCEPTION; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 用户 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class UserService extends ServiceImpl { + + private static final Map userCache = new HashMap<>(); + + private final PasswordEncoder passwordEncoder; + + /** + * 查询全部用户 + */ + public List listAll() { + return baseMapper.selectList(new QueryWrapper<>()); + } + + /** + * 根据ID查询 + */ + public UserEntity selectById(Long id) { + return baseMapper.selectOne(new LambdaQueryWrapper().eq(UserEntity::getId, id)); + } + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户信息 + */ + public UserEntity selectByUsername(String username) { + return baseMapper.selectOne(new LambdaQueryWrapper().eq(UserEntity::getUsername, username)); + } + + /** + * 新增用户 + */ + @Transactional(rollbackFor = Exception.class) + public void insert(UserAddReq req) { + UserEntity user = req.to(UserEntity.class); + user.setNickName(req.getUsername()); + user.setRealName(req.getUsername()); + user.setType(req.getType()); + user.setSalt(SaltUtil.randomSalt()); + user.setPassword(passwordEncoder.encode(req.getPassword() + user.getSalt())); + baseMapper.insert(user); + } + + /** + * 修改用户信息 + */ + @Transactional(rollbackFor = Exception.class) + public void updById(UserEntity user) { + baseMapper.updById(user); + } + + /** + * 修改密码 + */ + @Transactional(rollbackFor = Exception.class) + public void updPassword(UserUpdPwdReq req) { + XzException400.throwBy(!req.getNewPassword().equals(req.getConfirmPassword()), "两次输入密码不匹配"); + UserEntity user = selectById(req.getUserId()); + if (!passwordEncoder.matches(req.getPassword() + user.getSalt(), user.getPassword())) { + throw new XzException400("密码错误"); + } + + String newPwd = passwordEncoder.encode(req.getNewPassword() + user.getSalt()); + baseMapper.updPwd(req.getUserId(), newPwd); + } + + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserTypeEnum.java new file mode 100644 index 0000000..72e1e11 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/UserTypeEnum.java @@ -0,0 +1,31 @@ +package com.blossom.backend.base.user; + +import lombok.Getter; + +/** + * 用户类型 + * + * @author xzzz + */ +public enum UserTypeEnum { + /** + * 管理员 + */ + ADMIN(1), + /** + * 普通用户 + */ + NORMAL(2), + /** + * 只读用户 + */ + READONLY(3), + ; + + @Getter + private final Integer type; + + UserTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/BlossomUserRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/BlossomUserRes.java new file mode 100644 index 0000000..e2e2e3e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/BlossomUserRes.java @@ -0,0 +1,66 @@ +package com.blossom.backend.base.user.pojo; + +import com.blossom.backend.base.sys.os.OSRes; +import com.blossom.common.base.pojo.AbstractPOJO; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Map; + +/** + * 用户响应 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BlossomUserRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long id; + /** + * 用户名 + */ + private String username; + /** + * 昵称 + */ + private String nickName; + /** + * 用户头像 + */ + private String avatar; + /** + * 备注 + */ + private String remark; + /** + * 位置 + */ + private String location; + /** + * 文章数 + */ + private Integer articleCount; + /** + * 文章字数 + */ + private Integer articleWords; + + /** + * 对象存储信息, 非登录状态不返回该字段 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private OSRes osRes; + + /** + * 系统参数, paramName: paramValue + */ + private Map params; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserAddReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserAddReq.java new file mode 100644 index 0000000..3b6cd7f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserAddReq.java @@ -0,0 +1,22 @@ +package com.blossom.backend.base.user.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +@EqualsAndHashCode(callSuper = true) +public class UserAddReq extends AbstractPOJO { + + @NotBlank(message = "用户名为必填项") + private String username; + + @NotBlank(message = "密码为必填项") + private String password; + + @NotNull(message = "用户类型为必填项") + private Integer type; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserEntity.java new file mode 100644 index 0000000..d779db9 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserEntity.java @@ -0,0 +1,69 @@ +package com.blossom.backend.base.user.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 用户 + * + * @author xzzz + */ +@Data +@TableName("base_user") +@EqualsAndHashCode(callSuper = true) +public class UserEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 用户类型 + */ + private Integer type; + /** + * 用户名 + */ + private String username; + /** + * 用户手机号 + */ + private String phone; + /** + * 用户密码 + */ + private String password; + /** + * 密码加盐 + */ + private String salt; + /** + * 昵称 + */ + private String nickName; + /** + * 真实姓名 + */ + private String realName; + /** + * 用户头像 + */ + private String avatar; + /** + * 备注 + */ + private String remark; + /** + * 用户的位置 + */ + private String location; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdPwdReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdPwdReq.java new file mode 100644 index 0000000..4d3cba5 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdPwdReq.java @@ -0,0 +1,28 @@ +package com.blossom.backend.base.user.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * 修改密码 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserUpdPwdReq extends AbstractPOJO { + + private Long userId; + + @NotBlank(message = "旧密码为必填项") + private String password; + + @NotBlank(message = "新密码为必填项") + private String newPassword; + + @NotBlank(message = "确认密码为必填项") + private String confirmPassword; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdReq.java new file mode 100644 index 0000000..4b68141 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/user/pojo/UserUpdReq.java @@ -0,0 +1,43 @@ +package com.blossom.backend.base.user.pojo; + +import com.blossom.common.base.pojo.AbstractEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 用户请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserUpdReq extends AbstractEntity { + + /** + * 用户名 + */ + @NotBlank(message = "用户名为必填项") + private String username; + /** + * 昵称 + */ + @NotBlank(message = "昵称为必填项") + private String nickName; + /** + * 说明 + */ + @NotBlank(message = "说明为必填项") + private String remark; + /** + * 位置 + */ + private String location; + /** + * 用户头像 + */ + @NotBlank(message = "用户头像为必填项") + private String avatar; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/config/BlConstants.java b/blossom-backend/backend/src/main/java/com/blossom/backend/config/BlConstants.java new file mode 100644 index 0000000..18539ce --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/config/BlConstants.java @@ -0,0 +1,6 @@ +package com.blossom.backend.config; + +public class BlConstants { + + public static final String REQ_HEADER_USERID = "Blossom-User-Id"; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/config/SpringTaskExecutorConfig.java b/blossom-backend/backend/src/main/java/com/blossom/backend/config/SpringTaskExecutorConfig.java new file mode 100644 index 0000000..c1b3e5c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/config/SpringTaskExecutorConfig.java @@ -0,0 +1,78 @@ +package com.blossom.backend.config; + +import cn.hutool.core.thread.ThreadFactoryBuilder; +import com.blossom.common.base.util.spring.SpringUtil; +import com.blossom.expand.tracker.core.adapter.spring.TrackerTaskDecorator; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +@Configuration +public class SpringTaskExecutorConfig { + + @Bean("taskExecutor") + public Executor taskExecutor(Environment env) { + log.info("[TRACKERS] 已经适配框架 : Spring Task"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // 核心线程池10 + executor.setCorePoolSize(10); + // 最大线程池20 + executor.setMaxPoolSize(20); + // 队列容量 + executor.setQueueCapacity(200); + // 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。 + executor.setKeepAliveSeconds(60); + // 线程名称前缀 + executor.setThreadNamePrefix(env.getProperty(SpringUtil.APP_NAME) + "-task-"); + // 用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean + executor.setWaitForTasksToCompleteOnShutdown(true); + // 等待时间 + executor.setAwaitTerminationSeconds(60); + // 增加 TaskDecorator 属性的配置 + executor.setTaskDecorator(new TrackerTaskDecorator( + "SPRING_ASYNC_TASK", TrackerConstants.SPAN_TYPE_SPRING_ASYNC)); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 处理未捕获异常日志打印 + final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> + log.error("异步线程执行失败。异常信息 => {} : ", e.getMessage(), e); + ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder(); + threadFactoryBuilder.setNamePrefix(env.getProperty(SpringUtil.APP_NAME) + "-task-"); + threadFactoryBuilder.setUncaughtExceptionHandler(uncaughtExceptionHandler); + executor.setThreadFactory(threadFactoryBuilder.build()); + executor.initialize(); + return executor; + } + + /** + * 定时任务线程池 + */ + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setPoolSize(10); + taskScheduler.setThreadNamePrefix("bl-sche-"); + final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> log.error("定时任务执行失败。异常信息 => {} : ", e.getMessage(), e); + ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder(); + threadFactoryBuilder.setNamePrefix("bl-sche-"); + threadFactoryBuilder.setUncaughtExceptionHandler(uncaughtExceptionHandler); + taskScheduler.setThreadFactory(threadFactoryBuilder.build()); + taskScheduler.initialize(); + return taskScheduler; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/config/Test.java b/blossom-backend/backend/src/main/java/com/blossom/backend/config/Test.java new file mode 100644 index 0000000..84f9e27 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/config/Test.java @@ -0,0 +1,16 @@ +package com.blossom.backend.config; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Test { + public static void main(String[] args) { + + String s = + "**JSR (JavaSpecification Requests)**\\n\\nJSR的意思是“Java 规范提案”。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。简单的就是jsr是java开发者以及授权者指定的标准,而java开发者以及授权者形成一个jcp国际组织。职能是指定java标准。\\n\\n目前已经有超过300个JSR,常见的JSR有:\\n\\n| JSR | Specification or Technology |\\n|-----|-------------------------------------------------------------------------------------------------------------------------------------|\\n| 1 | Real-Time Specification for Java (RTSJ) 1.0 |\\n| 3 | Java Management Extensions (JMX) 1.0, 1.1, & 1.2[1] |\\n| 5 | Java API for XML Processing (JAXP) 1.0 |\\n| 8 | OSGI Open Services Gateway Specification |\\n| 9 | Jiro (Federated Management Architecture Specification) 1.0 |\\n| 12 | Java Data Objects (JDO) 1.0 |\\n| 13 | Improved BigDecimal (Java Platform, Standard Edition#java.math) |\\n| 14 | Add Generic Types To The Java Programming Language (as of J2SE 5.0) |\\n| 16 | J2EE连接器架构 (Java EE Connector Architecture, JCA) 1.0 |\\n| 19 | Enterprise JavaBeans (EJB) 2.0 |\\n| 22 | JAIN SLEE API Specification (JSLEE) 1.0 |\\n| 30 | Connected Limited Device Configuration (CLDC) 1.0 for Java ME |\\n| 31 | Java Architecture for XML Binding (JAXB) 1.0 |\\n| 32 | JAIN SIP API Specification (JSIP) 1.0, 1.1 and 1.2 for Java ME |\\n| 36 | Connected Device Configuration (CDC) 1.0 for Java ME |\\n| 37 | 移动信息设备描述 (Mobile Information Device Profile, MIDP) 1.0 for Java ME |\\n| 40 | Java Metadata Interface (JMI) 1.0 |\\n| 41 | A Simple Assertion Facility (as of J2SE 1.4) |\\n| 47 | Logging API Specification (as of J2SE 1.4) |\\n| 48 | WBEM Services Specification (as of J2SE 1.4) |\\n| 51 | New I/O APIs for the Java Platform (NIO) (as of J2SE 1.4) |\\n| 52 | JavaServer Pages Standard Tag Library (JSTL) 1.0 and 1.1[2] |\\n| 53 | Java Servlet 2.3 and JavaServer Pages (JSP) 1.2 Specifications |\\n| 54 | Java Database Connectivity (JDBC) 3.0 |\\n| 56 | Java Network Launching Protocol and API (JNLP) 1.0, 1.5 and 6.0[3] (Java Web Start) |\\n| 58 | Java 2 Platform, Enterprise Edition (J2EE) 1.3 |\\n| 59 | Java 2 Platform, Standard Edition (J2SE) 1.4 (Merlin) |\\n| 63 | Java API for XML Processing (JAXP) 1.1 and 1.2[4] |\\n| 68 | Java Platform, Micro Edition (Java ME) 1.0 |\\n| 73 | Java Data Mining API (JDM) 1.0 |\\n| 75 | PDA Optional Packages for the J2ME Platform |\\n| 80 | Java USB API |\\n| 82 | Java APIs for Bluetooth |\\n| 88 | Java EE Application Deployment |\\n| 93 | Java API for XML Registries (JAXR) 1.0 |\\n| 94 | Java Rules Engine API |\\n| 102 | Java Document Object Model (JDOM) 1.0 |\\n| 110 | Java APIs for WSDL (WSDL4J) 1.0 |\\n| 112 | J2EE连接器架构 (Java EE Connector Architecture, JCA) 1.5 |\\n| 114 | Java Database Connectivity (JDBC) Rowset Implementations |\\n| 116 | SIP Servlet API 1.0 |\\n| 118 | Mobile Information Device Profile (MIDP) 2.0 for Java ME |\\n| 120 | Wireless Messaging API (WMA) |\\n| 121 | Application Isolation API |\\n| 127 | JavaServer Faces (JSF) 1.0 and 1.1[5] |\\n| 133 | Java Memory Model and Thread Specification Revision |\\n| 135 | Java Mobile Media API (MMAPI) for Java ME |\\n| 139 | Connected Limited Device Configuration (CLDC) 1.1 for Java ME |\\n| 140 | Service Location Protocol (SLP) API for Java |\\n| 141 | 会话描述协议 (Session Description Protocol, SDP) API for Java |\\n| 151 | Java 2 Platform, Enterprise Edition (J2EE) 1.4 |\\n| 152 | JavaServer Pages (JSP) 2.0 |\\n| 153 | Enterprise JavaBeans (EJB) 2.1 |\\n| 154 | Java Servlet 2.4 and 2.5 Specifications[6] |\\n| 160 | Java Management Extensions (JMX) Remote API 1.0 |\\n| 166 | Concurrency Utilities (as of J2SE 5.0 java.util.concurrent, java.util.concurrent.atomic and java.util.concurrent.locks) |\\n| 168 | Portlet specification 1.0 |\\n| 170 | Content repository API for Java (JCR) 1.0 |\\n| 172 | Web Services Specification for Java ME |\\n| 173 | StAX (Streaming API for XML) |\\n| 175 | A Metadata Facility for the Java Programming Language |\\n| 176 | Java 2 Platform, Standard Edition (J2SE) 5.0 (Tiger) |\\n| 177 | Security and Trust Services API for J2ME (SATSA) |\\n| 179 | Location API 1.0 for Java ME |\\n| 180 | Session Initiation Protocol (SIP) API for Java ME |\\n| 181 | Web Services Metadata for the Java Platform |\\n| 184 | Mobile 3D Graphics API for Java ME 1.0 and 1.1 |\\n| 185 | Java Technology for the Wireless Industry (JTWI) |\\n| 187 | 即时通讯(Instant messaging) (Java ME and Java SE) |\\n| 198 | 集成开发环境的标准扩展API |\\n| 199 | Java Compiler API |\\n| 201 | Extending the Java Programming Language with Enumerations, Autoboxing, Enhanced for loops and Static Import (as of J2SE 5.0) |\\n| 202 | Java Class File Specification Update |\\n| 203 | More New I/O APIs for the Java Platform (NIO2) |\\n| 204 | Unicode Supplementary Character Support (as of J2SE 5.0 adds support for Unicode 3.1) |\\n| 205 | Wireless Messaging API 2.0 (WMA) 2.0 |\\n| 206 | Java API for XML Processing (JAXP) 1.3 |\\n| 208 | Java Business Integration (JBI) 1.0 |\\n| 215 | Java Community Process (JCP) 2.6 |\\n| 218 | Connected Device Configuration (CDC) 1.1 for Java ME |\\n| 220 | Enterprise JavaBeans (EJB) 3.0 |\\n| 221 | Java Database Connectivity (JDBC) 4.0 |\\n| 222 | Java Architecture for XML Binding (JAXB) 2.0 |\\n| 223 | Scripting for the Java Platform for Java SE 6 |\\n| 224 | Java API for XML Web Services (JAX-WS), successor of JAX-RPC |\\n| 225 | XQuery API for Java (XQJ) |\\n| 226 | Scalable 2D Vector Graphics API for Java ME |\\n| 229 | Payment API (PAPI) |\\n| 231 | Java Bindings for OpenGL |\\n| 234 | Advanced Multimedia Supplements API for Java ME |\\n| 235 | Service Data Objects (SDO) |\\n| 239 | Java Bindings for OpenGL ES |\\n| 240 | JAIN SLEE API Specification (JSLEE) 1.1 |\\n| 241 | The Groovy Programming Language |\\n| 243 | Java Data Objects (JDO) 2.0 |\\n| 244 | Java Platform, Enterprise Edition (Java EE) 5 |\\n| 245 | JavaServer Pages (JSP) 2.1 |\\n| 247 | Java Data Mining API (JDM) 2.0 |\\n| 248 | Mobile Service Architecture |\\n| 249 | Mobile Service Architecture 2 |\\n| 250 | Common Annotations for the Java Platform (for the Metadata facility for Java) |\\n| 252 | JavaServer Faces (JSF) 1.2 |\\n| 253 | Mobile Telephony API (MTA) |\\n| 255 | Java Management Extensions (JMX) 2.0 |\\n| 256 | Mobile Sensor API |\\n| 257 | Contactless Communication API (NFC) |\\n| 260 | Javadoc Tag Technology Update |\\n| 269 | Pluggable Annotations Processing API (for the Metadata facility for Java) |\\n| 270 | Java Platform, Standard Edition (Java SE) 6 (Mustang) |\\n| 271 | Mobile Information Device Profile (MIDP) 3.0 for Java ME |\\n| 274 | The BeanShell Scripting Language |\\n| 275 | Units specification (see Units of Measurement) |\\n| 276 | Design-time Metadata for JavaServer Faces Components |\\n| 277 | Java Module System |\\n| 280 | XML API for Java ME |\\n| 281 | IMS Services API (See IMS) |\\n| 282 | Real-Time Specification for Java (RTSJ) 1.1 |\\n| 283 | Content Repository for Java Technology 2.0 |\\n| 286 | Portlet Specification 2.0 |\\n| 289 | SIP Servlet API 1.1 |\\n| 290 | Java Language & XML User Interface Markup Integration (XML-UI) |\\n| 291 | Dynamic Component Support for Java SE (see OSGi) |\\n| 292 | Supporting Dynamically Typed Languages on the JavaTM Platform |\\n| 293 | Location API 2.0 for Java ME |\\n| 294 | Improved Modularity Support in the Java Programming Language |\\n| 296 | Swing Application Framework (Java SE 7) |\\n| 299 | Java Contexts and Dependency Injection (Weld) |\\n| 301 | JSF Portlet Bridge |\\n| 303 | Bean Validation |\\n| 307 | Network Mobility and Mobile Data API (not official as of July, 20th, 2007 but official release is planned for 2. Q. 2008 |\\n| 308 | Annotations on Java Types (Java SE 8) |\\n| 311 | Java API for RESTful Web Services (JAX-RS) 1.0 and 1.1 |\\n| 314 | JavaServer Faces (JSF) 2.0 |\\n| 316 | Java Platform, Enterprise Edition (Java EE) 6 |\\n| 317 | Java Persistence API (JPA) 2.0 |\\n| 325 | IMS Communication Enablers (ICE) (See IMS) |\\n| 901 | The Java Language Specification, Third Edition (JLS) (as of J2SE 5.0 incorporates changes from JSRs 14, 41, 133, 175, 201, and 204) |\\n| 907 | Java Transaction API (JTA) 1.0 and 1.1 |\\n| 912 | Java 3D API 1.3 |\\n| 913 | Java Community Process (JCP) 2.0, 2.1 & 2.5[7]. |\\n| 914 | Java Message Service (JMS) API 1.0 and 1.1 |\\n| 924 | Java Virtual Machine Specification, Second Edition (JVM) (as of J2SE 5.0)[8]. |\\n| 926 | Java 3D API 1.5 |\\n\\n\\n\\n## JSR 250\\n\\nJSR-250 注释,它包括\\n\\n- javax.annotation.@PostConstruct\\n- javax.annotation.@PreDestroy\\n- javax.annotation.@Resource\\n\\n### @PostConstruct 和 @PreDestroy 注释:\\n\\n为了定义一个 bean 的安装和卸载,我们使用 init-method 和/或 destroy-method 参数简单的声明一下 。\\ninit-method 属性指定了一个方法,该方法在 bean 的实例化阶段会立即被调用。同样地,destroy-method 指定了一个方法,该方法只在一个 bean 从容器中删除之前被调用。\\n\\n你可以使用 @PostConstruct 注释作为初始化回调函数的一个替代,@PreDestroy 注释作为销毁回调函数的一个替代\\n\\n## JSR 292\\n\\n添加一个字节码 **invokedynamic** 。用于提供在缺乏静态类型信息时高效和灵活的执行方法调用。\\n\\n**invokedynamic** 是针对 JVM 的,用于更好的支持动态 JVM 语言的函数调用; **JSR292** 实现则提供在 Java 语言层面上的 **invokedynamic** 调用。\\n\\n主要用法:一些比反射更快速,更安全的动态方法调用支持,在很多框架中,创建接口的动态代理对象后,会通过这种方法来调用动态对象方法,如`Feign`\\n\\n```java\\n└─java\\n └─lang\\n ├─invoke\\n | ├─CallSite\\n | ├─ConstantCallSite\\n | ├─MethodHandle\\n | ├─MethodHandleProxies\\n | ├─MethodHandles\\n | ├─MethodHandles.Lookup\\n | ├─MethodType\\n | ├─MutableCallSite\\n | ├─SwitchPoint\\n | └─VolatileCallSite\\n ├─ClassValue\\n └─BootstrapMethodError\\n```\\n\\n"; + + System.out.println(s.replaceAll("\\\\n","\n")); + + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/config/WebConfigurer.java b/blossom-backend/backend/src/main/java/com/blossom/backend/config/WebConfigurer.java new file mode 100644 index 0000000..ff3b160 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/config/WebConfigurer.java @@ -0,0 +1,29 @@ +package com.blossom.backend.config; + +import com.blossom.backend.base.auth.interceptor.UserTypeInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +/** + * @author jasmineXz + */ +@Slf4j +@Configuration +public class WebConfigurer implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 拦截所有请求,通过判断是否有 @myInterceptor 注解 决定是否需要登录 + registry.addInterceptor(myInterceptor()).addPathPatterns("/**"); + } + + @Bean + public UserTypeInterceptor myInterceptor() { + log.info("123123213"); + return new UserTypeInterceptor(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/FolderTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/FolderTypeEnum.java new file mode 100644 index 0000000..077c577 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/FolderTypeEnum.java @@ -0,0 +1,27 @@ +package com.blossom.backend.server; + +import lombok.Getter; + +/** + * 文件夹类型, 不同的文件夹具有不同的字段要素, 例如图片文件夹不需要公开 + * + * @author xzzz + */ +public enum FolderTypeEnum { + /** + * 文章文件夹 + */ + ARTICLE(1), + + /** + * 图片文件夹 + */ + PICTURE(2); + + @Getter + private final Integer type; + + FolderTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/TagEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/TagEnum.java new file mode 100644 index 0000000..ea3f0e4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/TagEnum.java @@ -0,0 +1,34 @@ +package com.blossom.backend.server; + +import cn.hutool.core.util.StrUtil; + +/** + * 文档中有特殊意义的标签 + * + * @author xzzz + */ +public enum TagEnum { + /** + * 标识该文件夹是个专题 + */ + subject, + /** + * 标识该文章是个目录 + */ + toc, + ; + + public static boolean isSubject(String tags) { + if (StrUtil.isBlank(tags)) { + return false; + } + return tags.toLowerCase().contains(TagEnum.subject.name()); + } + + public static boolean isToc(String tags) { + if (StrUtil.isBlank(tags)) { + return false; + } + return tags.toLowerCase().contains(TagEnum.toc.name()); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleController.java new file mode 100644 index 0000000..e67838d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleController.java @@ -0,0 +1,171 @@ +package com.blossom.backend.server.article.draft; + +import cn.hutool.core.net.URLEncodeUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.article.draft.pojo.*; +import com.blossom.backend.server.article.open.ArticleOpenService; +import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity; +import com.blossom.backend.server.doc.DocTypeEnum; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.common.base.enums.YesNo; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.List; + + +/** + * 文章 [Article] + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +@RequestMapping("/article") +public class ArticleController { + + private final ArticleService baseService; + private final ArticleOpenService openService; + + /** + * 查询列表 + * + * @apiNote 返回简单字段 + */ + @GetMapping("/list") + public R> listAll(ArticleQueryReq req) { + req.setUserId(AuthContext.getUserId()); + return R.ok(baseService.listAll(req), ArticleInfoSimpleRes.class); + } + + /** + * 通过ID查询文章 + * + * @param id 文章ID + * @param showToc 返回目录 + * @param showMarkdown 返回 markdown 内容 + * @param showHtml 返回 html 内容 + * @return 文章信息 + */ + @GetMapping("/info") + public R info(@RequestParam("id") Long id, + @RequestParam("showToc") Boolean showToc, + @RequestParam("showMarkdown") Boolean showMarkdown, + @RequestParam("showHtml") Boolean showHtml) { + if (showToc == null) { + showToc = false; + } + if (showMarkdown == null) { + showMarkdown = false; + } + if (showHtml == null) { + showHtml = false; + } + ArticleEntity article = baseService.selectById(id, showToc, showMarkdown, showHtml); + XzException400.throwBy(ObjUtil.isNull(article), "文章不存在"); + ArticleInfoRes res = article.to(ArticleInfoRes.class); + res.setTags(DocUtil.toTagList(article.getTags())); + res.setType(DocTypeEnum.A.getType()); + + if (YesNo.YES.getValue().equals(article.getOpenStatus())) { + ArticleOpenEntity open = openService.selectById(id, false, false, false); + if (open != null) { + res.setOpenTime(open.getOpenTime()); + res.setOpenVersion(open.getOpenVersion()); + res.setSyncTime(open.getSyncTime()); + } + } + + return R.ok(res); + } + + /** + * 新增文章 + * + * @param req 文章对象 + * @return 保存结果 + */ + @PostMapping("/add") + public R insert(@Validated @RequestBody ArticleAddReq req) { + ArticleEntity article = req.to(ArticleEntity.class); + article.setTags(DocUtil.toTagStr(req.getTags())); + article.setUserId(AuthContext.getUserId()); + return R.ok(baseService.insert(article)); + } + + /** + * 修改文章 + * + * @param req 文章对象 + * @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 {@link ArticleService#updateContentById(ArticleEntity)} + */ + @PostMapping("/upd") + public R insert(@Validated @RequestBody ArticleUpdReq req) { + ArticleEntity article = req.to(ArticleEntity.class); + article.setTags(DocUtil.toTagStr(req.getTags())); + return R.ok(baseService.update(article)); + } + + /** + * 保存正文内容 + */ + @PostMapping("/upd/content") + public R updateContent(@Validated @RequestBody ArticleUpdContentReq content) { + ArticleEntity upd = content.to(ArticleEntity.class); + upd.setReferences(content.getReferences()); + upd.setUserId(AuthContext.getUserId()); + int words = baseService.updateContentById(upd); + ArticleUpdContentRes res = new ArticleUpdContentRes(); + res.setWords(words); + res.setUpdTime(DateUtils.date()); + return R.ok(res); + } + + /** + * 星标文章 + * + * @param req 文章对象 + */ + @PostMapping("/star") + public R star(@Validated @RequestBody ArticleStarReq req) { + return R.ok(baseService.update(req.to(ArticleEntity.class))); + } + + /** + * 下载文章 + * + * @param id 文章ID + * @param response 文章流 + */ + @GetMapping("/download") + public void download(@RequestParam("id") Long id, HttpServletResponse response) throws IOException { + ArticleEntity article = baseService.selectById(id, false, true, false); + if (StrUtil.isBlank(article.getMarkdown())) { + throw new IllegalArgumentException("文章内容为空,无法下载"); + } + try (InputStream is = new ByteArrayInputStream(article.getMarkdown().getBytes(StandardCharsets.UTF_8)); BufferedInputStream bis = new BufferedInputStream(is)) { + OutputStream os = response.getOutputStream(); + String filename = URLEncodeUtil.encode(article.getName() + ".md"); + // 设置强制下载不打开 + response.setContentType("application/force-download"); + // 将请求头暴露给前端 + response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.addHeader("Content-Disposition", "attachment;filename=" + filename); + byte[] buffer = new byte[1024]; + int i = bis.read(buffer); + while (i != -1) { + os.write(buffer, 0, i); + i = bis.read(buffer); + } + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleMapper.java new file mode 100644 index 0000000..70f014e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleMapper.java @@ -0,0 +1,60 @@ +package com.blossom.backend.server.article.draft; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.article.draft.pojo.ArticleStatRes; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文章 + * + * @author xzzz + */ +@Mapper +public interface ArticleMapper extends BaseMapper { + + /** + * 查询全部 + */ + List listAll(ArticleEntity entity); + + /** + * 根据ID修改 + */ + void updById(ArticleEntity entity); + + /** + * 修改文章内容 + */ + void updContentById(ArticleEntity entity); + + /** + * 修改某段日期内修改的文章数据 + * + * @param beginUpdTime 开始修改日期 + * @param endUpdTime 结束修改日期 + */ + ArticleStatRes statCount(@Param("beginUpdTime") String beginUpdTime, + @Param("endUpdTime") String endUpdTime, + @Param("userId") Long userId); + + /** + * 同步版本号, 将文章的 version 同步到 openVersion, 只有 open_status 为 1 才会修改成功 + * + * @param articleId 文章ID + */ + void sync(@Param("articleId") Long articleId); + + /** + * 递增 UV, PV + * + * @param article 文章ID + * @param pv pv + * @param uv uv + */ + void uvAndPv(@Param("articleId") Long article, @Param("pv") Integer pv, @Param("uv") Integer uv); + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleService.java new file mode 100644 index 0000000..2a3e142 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/ArticleService.java @@ -0,0 +1,148 @@ +package com.blossom.backend.server.article.draft; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.TagEnum; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq; +import com.blossom.backend.server.article.log.ArticleLogService; +import com.blossom.backend.server.article.reference.ArticleReferenceService; +import com.blossom.backend.server.article.view.ArticleViewService; +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import com.blossom.backend.server.utils.ArticleUtil; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.common.base.exception.XzException404; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * 文章 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleService extends ServiceImpl { + + private final ArticleReferenceService referenceService; + private final ArticleViewService viewService; + private final ArticleLogService logService; + + /** + * 查询列表 + */ + public List listAll(ArticleQueryReq req) { + List articles = baseMapper.listAll(req.to(ArticleEntity.class)); + if (CollUtil.isEmpty(articles)) { + return new ArrayList<>(); + } + return articles; + } + + /** + * 树状列表 + * + * @return DocTreeRes + */ + public List listTree(ArticleQueryReq req) { + List articles = baseMapper.listAll(req.to(ArticleEntity.class)); + List articleTrees = new ArrayList<>(articles.size()); + for (ArticleEntity article : articles) { + articleTrees.add(DocUtil.toDocTree(article)); + } + return articleTrees; + } + + /** + * 根据ID查询 + */ + public ArticleEntity selectById(Long id, boolean showToc, boolean showMarkdown, boolean showHtml) { + QueryWrapper where = new QueryWrapper<>(); + List column = CollUtil.newArrayList("id", "pid", "name", "icon", "tags", "sort", "cover", "describes", "star_status", + "open_status", "pv", "uv", "likes", "words", "version", "cre_time", "upd_time"); + if (showToc) { + column.add(TagEnum.toc.name()); + } + if (showMarkdown) { + column.add("markdown"); + } + if (showHtml) { + column.add("html"); + } + where.select(column); + where.eq("id", id).last("limit 1"); + return baseMapper.selectOne(where); + } + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public Long insert(ArticleEntity req) { + baseMapper.insert(req); + return req.getId(); + } + + /** + * 修改, 该接口只能修改文章的基本信息, 正文及版本修改请使用 {@link ArticleService#updateContentById(ArticleEntity)} + */ + @Transactional(rollbackFor = Exception.class) + public Long update(ArticleEntity req) { + XzException404.throwBy(req.getId() == null, "ID不得为空"); + baseMapper.updById(req); + return req.getId(); + } + + /** + * 修改文章内容, 并统计字数 + * + * @return 返回文章字数 + */ + @Transactional(rollbackFor = Exception.class) + public Integer updateContentById(ArticleEntity req) { + XzException404.throwBy(req.getId() == null, "ID不得为空"); + if (req.getMarkdown() != null) { + req.setWords(ArticleUtil.statWords(req.getMarkdown())); + } + if (req.getHtml() != null) { + req.setHtml(req.getHtml().replaceAll("


", "")); + } + baseMapper.updContentById(req); + referenceService.bind(req.getUserId(), req.getId(), req.getName(), req.getReferences()); + logService.insert(req.getId(), 0, req.getMarkdown()); + return req.getWords(); + } + + /** + * 同步版本号, 将文章的 version 同步到 openVersion, 只有 open_status 为 1 才会修改成功 + */ + public void sync(Long id) { + baseMapper.sync(id); + } + + /** + * 递增UV和PV数据 + * PV 接口每调用一次都会递增, UV数据每个IP每天只会递增一次 + * + * @param ip ip + * @param userAgent userAgent + * @param id 文章ID + */ + @Async + @Transactional(rollbackFor = Exception.class) + public void uvAndPv(String ip, String userAgent, Long id) { + int uv = 0; + if (viewService.uv(ip, userAgent, id)) { + uv = 1; + } + baseMapper.uvAndPv(id, 1, uv); + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleAddReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleAddReq.java new file mode 100644 index 0000000..bb44963 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleAddReq.java @@ -0,0 +1,59 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * 文章新增请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleAddReq extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 文件夹ID + */ + @Min(value = 0, message = "[上级菜单ID] 不能小于0") + @NotNull(message = "[上级菜单] 为必填项") + private Long pid; + /** + * 文章名称 + */ + @NotBlank(message = "[文章名称] 为必填项") + private String name; + /** + * 文章图标 + */ + private String icon; + /** + * 标签集合 + */ + private List tags; + /** + * 排序 + */ + private Integer sort; + /** + * 封面 + */ + private String cover; + /** + * 描述 + */ + private String describes; + /** + * 颜色 + */ + private String color; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleEntity.java new file mode 100644 index 0000000..c3d72ac --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleEntity.java @@ -0,0 +1,135 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 文章 + * + * @author xzzz + */ +@Data +@TableName("blossom_article") +@EqualsAndHashCode(callSuper = true) +public class ArticleEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 文章名称 + */ + private String name; + /** + * 文章图标 + */ + private String icon; + /** + * 标签集合 + */ + private String tags; + /** + * 排序 + */ + private Integer sort; + /** + * 封面 + */ + private String cover; + /** + * 描述 + */ + private String describes; + /** + * star状态 + */ + private Integer starStatus; + /** + * 公开状态 + */ + private Integer openStatus; + /** + * 公开版本 + */ + private Integer openVersion; + /** + * 页面的查看数 + */ + private Integer pv; + /** + * 独立的访问次数,每日IP重置 + */ + private Integer uv; + /** + * 点赞数 + */ + private Integer likes; + /** + * 文章字数 + */ + private Integer words; + /** + * 文章字数 + */ + private Integer version; + /** + * 颜色 + */ + private String color; + /** + * 目录 + */ + private String toc; + /** + * Markdown 内容 + */ + private String markdown; + /** + * Html 内容 + */ + private String html; + /** + * 版本 + */ + private Date creTime; + /** + * 修改时间 + */ + private Date updTime; + /** + * 用户ID + */ + private Long userId; + + //region ============================== 非数据库字段 ============================== + /** + * 引用集合 + */ + @TableField(exist = false) + private List references; + + /** + * 父ID集合 + */ + @TableField(exist = false) + private List pids; + + //endregion +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoRes.java new file mode 100644 index 0000000..a165f14 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoRes.java @@ -0,0 +1,122 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 文章响应 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleInfoRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 文章名称 + */ + private String name; + /** + * 文章图标 + */ + private String icon; + /** + * 标签集合 + */ + private List tags; + /** + * 排序 + */ + private Integer sort; + /** + * 封面 + */ + private String cover; + /** + * 描述 + */ + private String describes; + /** + * star状态 + */ + private Integer starStatus; + /** + * 页面的查看数 + */ + private Integer pv; + /** + * 独立的访问次数,每日IP重置 + */ + private Integer uv; + /** + * 点赞数 + */ + private Integer likes; + /** + * 文章字数 + */ + private Integer words; + /** + * 版本 + */ + private Integer version; + /** + * Markdown 内容 + */ + private String markdown; + /** + * Html 内容 + */ + private String html; + /** + * 版本 + */ + private Date creTime; + /** + * 修改时间 + */ + private Date updTime; + /** + * 颜色 + */ + private String color; + /** + * 目录 + */ + private String toc; + /** + * 类型 + */ + private Integer type; + /** + * 开放状态 + */ + private Integer openStatus; + /** + * 公开同步时间 + */ + private Date syncTime; + /** + * 公开时间 + */ + private Date openTime; + /** + * 公开版本 + */ + private Integer openVersion; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoSimpleRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoSimpleRes.java new file mode 100644 index 0000000..67db6e0 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleInfoSimpleRes.java @@ -0,0 +1,81 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * 文章响应 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleInfoSimpleRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 文章名称 + */ + private String name; + /** + * 文章图标 + */ + private String icon; + /** + * 标签集合 + */ + private List tags; + /** + * 封面 + */ + private String cover; + /** + * 描述 + */ + private String describes; + /** + * star状态 + */ + private Integer starStatus; + /** + * 开放状态 + */ + private Integer openStatus; + /** + * 页面的查看数 + */ + private Integer pv; + /** + * 独立的访问次数,每日IP重置 + */ + private Integer uv; + /** + * 点赞数 + */ + private Integer likes; + /** + * 文章字数 + */ + private Integer words; + /** + * 版本 + */ + private Integer version; + /** + * 颜色 + */ + private String color; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleQueryReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleQueryReq.java new file mode 100644 index 0000000..ab72272 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleQueryReq.java @@ -0,0 +1,37 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.common.db.pojo.PageReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * 文章查询请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleQueryReq extends PageReq implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 上级ID集合 + */ + private List pids; + /** + * star状态, YesNoEnum + */ + private Integer starStatus; + /** + * 公开状态 + */ + private Integer openStatus; + /** + * 用户ID + */ + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStarReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStarReq.java new file mode 100644 index 0000000..ef91aca --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStarReq.java @@ -0,0 +1,35 @@ +package com.blossom.backend.server.article.draft.pojo; + + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * 文章 star + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleStarReq extends AbstractPOJO { + + /** + * 文章ID + */ + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[文章ID] 为必填项") + private Long id; + + /** + * star 状态 + */ + @Min(value = 0, message = "[star 状态] 不能小于0") + @Max(value = 1, message = "[star 状态] 不能大于1") + @NotNull(message = "[star 状态] 为必填项") + private Integer starStatus; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStatRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStatRes.java new file mode 100644 index 0000000..b625643 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleStatRes.java @@ -0,0 +1,25 @@ +package com.blossom.backend.server.article.draft.pojo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 文章统计对象 + * + * @author xzzz + */ +@Data +public class ArticleStatRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 文章数 + */ + private Integer articleCount; + /** + * 字数 + */ + private Integer articleWords; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentReq.java new file mode 100644 index 0000000..0d70c52 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentReq.java @@ -0,0 +1,53 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.backend.server.article.draft.ArticleController; +import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 修改文章内容 + * + *

+ * {@link ArticleController#updateContent(ArticleUpdContentReq)} + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleUpdContentReq extends AbstractPOJO { + + /** + * ID + */ + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[文章ID] 为必填项") + private Long id; + /** + * 名称, 用于引用关系表中的名称冗余 + */ + @NotBlank(message = "文章名称为必填项") + private String name; + /** + * markdown 内容 + */ + private String markdown; + /** + * html 内容 + */ + private String html; + /** + * 目录 + */ + private String toc; + /** + * 引用链接集合 + */ + private List references; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentRes.java new file mode 100644 index 0000000..f3d513b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdContentRes.java @@ -0,0 +1,40 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.backend.server.article.draft.ArticleController; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 修改文章内容 + *

+ * {@link ArticleController#updateContent(ArticleUpdContentReq)} + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleUpdContentRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 文章ID + */ + private Long id; + /** + * 文章版本 + */ + private Integer version; + /** + * 字数 + */ + private Integer words; + /** + * 修改时间 + */ + private Date updTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdReq.java new file mode 100644 index 0000000..a83b538 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/draft/pojo/ArticleUpdReq.java @@ -0,0 +1,63 @@ +package com.blossom.backend.server.article.draft.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * 文章修改请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleUpdReq extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[文章ID] 为必填项") + private Long id; + /** + * 文件夹ID + */ + @Min(value = 0, message = "[上级菜单ID] 不能小于0") + @NotNull(message = "[上级菜单] 为必填项") + private Long pid; + /** + * 文章名称 + */ + @NotBlank(message = "[文章名称] 为必填项") + private String name; + /** + * 文章图标 + */ + private String icon; + /** + * 标签集合 + */ + private List tags; + /** + * 排序 + */ + private Integer sort; + /** + * 封面 + */ + private String cover; + /** + * 描述 + */ + private String describes; + /** 颜色 */ + private String color; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogMapper.java new file mode 100644 index 0000000..fbd0f1f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogMapper.java @@ -0,0 +1,14 @@ +package com.blossom.backend.server.article.log; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.log.pojo.ArticleLogEntity; +import org.apache.ibatis.annotations.Mapper; + + +/** + * 文章记录 + */ +@Mapper +public interface ArticleLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogService.java new file mode 100644 index 0000000..93d4242 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/ArticleLogService.java @@ -0,0 +1,66 @@ +package com.blossom.backend.server.article.log; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.base.param.ParamEnum; +import com.blossom.backend.base.param.ParamService; +import com.blossom.backend.base.param.pojo.ParamEntity; +import com.blossom.backend.server.article.log.pojo.ArticleLogEntity; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +/** + * 文章记录 + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleLogService extends ServiceImpl { + + private final ParamService paramService; + + /** + * 新增记录 + */ + @Async + @Transactional(rollbackFor = Exception.class) + public void insert(Long articleId, Integer version, String markdown) { + ArticleLogEntity log = new ArticleLogEntity(); + log.setArticleId(articleId); + log.setVersion(version); + log.setMarkdown(markdown); + log.setCreTime(DateUtils.date()); + baseMapper.insert(log); + } + + + /** + * 每天凌晨5点执行 + */ + @Scheduled(cron = "0 0 05 * * ?") + @Transactional(rollbackFor = Exception.class) + public void refreshWeather() { + ParamEntity param = paramService.getValue(ParamEnum.ARTICLE_LOG_EXP_DAYS); + int expireDay = -60; + if (param != null) { + expireDay = Integer.parseInt(param.getParamValue()); + } + if (expireDay > 0) { + expireDay = expireDay * -1; + } + + log.info("[BLOSSOM] 删除{}日前的编辑记录", Math.abs(expireDay)); + Date expireDate = DateUtils.offsetDay(DateUtils.date(), expireDay); + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.lt(ArticleLogEntity::getCreTime, DateUtils.toYMD(expireDate)); + baseMapper.delete(where); + } + +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/pojo/ArticleLogEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/pojo/ArticleLogEntity.java new file mode 100644 index 0000000..9378d74 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/log/pojo/ArticleLogEntity.java @@ -0,0 +1,33 @@ +package com.blossom.backend.server.article.log.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.*; +import java.io.Serializable; + +/** + * 文章记录 + */ +@Data +@TableName("blossom_article_log") +@EqualsAndHashCode(callSuper = true) +public class ArticleLogEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** ID */ + @TableId + private Long id; + /** 文章ID */ + private Long articleId; + /** 版本 */ + private Integer version; + /** 文章内容 */ + private String markdown; + /** 修改日期 */ + private Date creTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenController.java new file mode 100644 index 0000000..39f5bf7 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenController.java @@ -0,0 +1,97 @@ +package com.blossom.backend.server.article.open; + + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.article.draft.ArticleService; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.article.draft.pojo.ArticleInfoRes; +import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity; +import com.blossom.backend.server.article.open.pojo.ArticleOpenReq; +import com.blossom.backend.server.article.open.pojo.ArticleOpenRes; +import com.blossom.backend.server.article.open.pojo.ArticleOpenSyncReq; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.server.article.view.ArticleViewService; +import com.blossom.backend.server.article.view.pojo.ArticleViewEntity; +import com.blossom.backend.server.doc.DocTypeEnum; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.util.ServletUtil; +import lombok.AllArgsConstructor; +import org.checkerframework.checker.units.qual.A; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; + +/** + * 公开文章 [Article] + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +@RequestMapping("/article/open") +public class ArticleOpenController { + + private final ArticleService articleService; + private final ArticleOpenService openService; + + + /** + * 查询公开文章, 只返回 html 内容 + * + * @param id 文章ID + * @return + */ + @AuthIgnore + @GetMapping("/info") + public R infoOpen(@RequestParam("id") Long id, HttpServletRequest request) { + ArticleOpenEntity open = openService.selectById(id, true, false, true); + XzException404.throwBy(ObjUtil.isNull(open), "文章不存在"); + + ArticleInfoRes res = open.to(ArticleInfoRes.class); + res.setOpenTime(open.getOpenTime()); + res.setOpenVersion(open.getOpenVersion()); + res.setSyncTime(open.getSyncTime()); + res.setType(DocTypeEnum.A.getType()); + + ArticleEntity article = articleService.selectById(id, false, false, false); + if (article != null) { + res.setTags(DocUtil.toTagList(article.getTags())); + res.setName(article.getName()); + res.setUv(article.getUv()); + res.setLikes(article.getLikes()); + } + + articleService.uvAndPv(ServletUtil.getIP(request), ServletUtil.getUserAgent(request), id); + + return R.ok(res); + } + + /** + * 公开文章 + * + * @param req 文章对象 + */ + @PostMapping + public R open(@Validated @RequestBody ArticleOpenReq req) { + req.setUserId(AuthContext.getUserId()); + return R.ok(openService.open(req)); + } + + /** + * 同步公开文章 + * + * @param req 文章对象 + */ + @PostMapping("/sync") + public R sync(@Validated @RequestBody ArticleOpenSyncReq req) { + openService.sync(req.getId()); + ArticleOpenEntity openEntity = openService.selectById(req.getId(), false, false, false); + return R.ok(openEntity.to(ArticleOpenRes.class)); + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenMapper.java new file mode 100644 index 0000000..1acd245 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenMapper.java @@ -0,0 +1,35 @@ +package com.blossom.backend.server.article.open; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 公开文章 + * + * @author xzzz + */ +@Mapper +public interface ArticleOpenMapper extends BaseMapper { + + /** + * 公开文章 + * + * @param id 公开文章的ID + */ + void open(Long id); + + /** + * 关闭公开 + * + * @param id 关闭公开文章的ID + */ + void close(Long id); + + /** + * 同步公开 + * + * @param id 公开文章的ID + */ + void sync(Long id); +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenService.java new file mode 100644 index 0000000..9a92c49 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/ArticleOpenService.java @@ -0,0 +1,93 @@ +package com.blossom.backend.server.article.open; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.TagEnum; +import com.blossom.backend.server.article.draft.ArticleService; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity; +import com.blossom.backend.server.article.open.pojo.ArticleOpenReq; +import com.blossom.common.base.enums.YesNo; +import com.blossom.common.base.exception.XzException400; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 公开文章 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleOpenService extends ServiceImpl { + + private final ArticleService articleService; + + /** + * 根据ID查询 + */ + public ArticleOpenEntity selectById(Long id, boolean showToc, boolean showMarkdown, boolean showHtml) { + QueryWrapper where = new QueryWrapper<>(); + List column = CollUtil.newArrayList("id", "pid", "words", "open_version", "open_time", "sync_time"); + if (showToc) { + column.add(TagEnum.toc.name()); + } + if (showMarkdown) { + column.add("markdown"); + } + if (showHtml) { + column.add("html"); + } + where.select(column); + where.eq("id", id).last("limit 1"); + return baseMapper.selectOne(where); + } + + /** + * 公开或关闭公开访问 + */ + @Transactional(rollbackFor = Exception.class) + public Long open(ArticleOpenReq req) { + ArticleEntity article = articleService.getById(req.getId()); + ArticleEntity entity = req.to(ArticleEntity.class); + /* + * 公开文章 将 article 表插入到 article_open 表 + */ + if (YesNo.YES.getValue().equals(req.getOpenStatus())) { + XzException400.throwBy(article.getOpenStatus().equals(YesNo.YES.getValue()), "文章已[" + req.getId() + "]已允许公开访问, 若要同步最新文章内容, 请使用同步"); + entity.setOpenVersion(article.getVersion()); + baseMapper.open(req.getId()); + } + /* + * 取消公开 删除 article_open 表数据 + */ + else if (YesNo.NO.getValue().equals(req.getOpenStatus())) { + entity.setOpenVersion(0); + XzException400.throwBy(article.getOpenStatus().equals(YesNo.NO.getValue()), "文章[" + req.getId() + "]未公开, 无法取消公开访问"); + baseMapper.close(req.getId()); + } + + articleService.update(entity); + return req.getId(); + } + + + /** + * 同步公开访问的文章正文等信息 + */ + @Transactional(rollbackFor = Exception.class) + public void sync(Long id) { + ArticleOpenEntity article = baseMapper.selectById(id); + XzException400.throwBy(ObjUtil.isNull(article), "文章[" + id + "]未公开, 无法同步"); + baseMapper.sync(id); + articleService.sync(id); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenEntity.java new file mode 100644 index 0000000..c626b22 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenEntity.java @@ -0,0 +1,58 @@ +package com.blossom.backend.server.article.open.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 公开文章 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blossom_article_open") +public class ArticleOpenEntity extends AbstractPOJO { + + /** + * ID + */ + @TableId + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 公开版本的字数 + */ + private Integer words; + /** + * 公开版本 + */ + private Integer openVersion; + /** + * 公开时间 + */ + private Date openTime; + /** + * 同步时间 + */ + private Date syncTime; + /** + * 目录 + */ + private String toc; + /** + * markdown 内容 + */ + private String markdown; + /** + * html 内容 + */ + private String html; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenReq.java new file mode 100644 index 0000000..7241995 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenReq.java @@ -0,0 +1,40 @@ +package com.blossom.backend.server.article.open.pojo; + + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * 文章公开 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleOpenReq extends AbstractPOJO { + + /** + * 文章ID + */ + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[文章ID] 为必填项") + private Long id; + + /** + * 公开状态 + */ + @Min(value = 0, message = "[open 状态] 不能小于0") + @Max(value = 1, message = "[open 状态] 不能大于1") + @NotNull(message = "[open 状态] 为必填项") + private Integer openStatus; + + /** + * 用户ID + */ + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenRes.java new file mode 100644 index 0000000..6d99bd3 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenRes.java @@ -0,0 +1,52 @@ +package com.blossom.backend.server.article.open.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 文章公开响应 + * + * @author xzzz + */ +@Data +public class ArticleOpenRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 字数 + */ + private Integer words; + /** + * 公开文章版本 + */ + private Integer openVersion; + /** + * 初次公开时间 + */ + private Date openTime; + /** + * 同步时间 + */ + private Date syncTime; + /** + * markdown 正文 + */ + private String markdown; + /** + * html 正文 + */ + private String html; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenSyncReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenSyncReq.java new file mode 100644 index 0000000..5390532 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/open/pojo/ArticleOpenSyncReq.java @@ -0,0 +1,26 @@ +package com.blossom.backend.server.article.open.pojo; + + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * 公开文章同步 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleOpenSyncReq extends AbstractPOJO { + + /** + * 文章ID + */ + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[文章ID] 为必填项") + private Long id; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceController.java new file mode 100644 index 0000000..1ff6d14 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceController.java @@ -0,0 +1,39 @@ +package com.blossom.backend.server.article.reference; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 文章引用 + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +@RequestMapping("/article/ref") +public class ArticleReferenceController { + + private final ArticleReferenceService baseService; + + /** + * 文章引用关系 + * + * @param onlyInner 是否只查询内部文章之间的引用 + */ + @GetMapping("/list") + public R> listAll(@RequestParam("onlyInner") Boolean onlyInner) { + if (onlyInner == null) { + onlyInner = true; + } + return R.ok(baseService.listAll(onlyInner, AuthContext.getUserId())); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceMapper.java new file mode 100644 index 0000000..197e3e8 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceMapper.java @@ -0,0 +1,26 @@ +package com.blossom.backend.server.article.reference; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.reference.pojo.ArticleReferenceEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文章 + * + * @author xzzz + */ +@Mapper +public interface ArticleReferenceMapper extends BaseMapper { + + /** + * 新增图片和文章引用关系 + * + * @param articleId 文章ID + * @param articleName 文章名称 + * @param pictureUrls 图片列表 + */ + void insertList(@Param("references") List references); +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceService.java new file mode 100644 index 0000000..eec8e3a --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/ArticleReferenceService.java @@ -0,0 +1,172 @@ +package com.blossom.backend.server.article.reference; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.article.reference.pojo.ArticleReferenceEntity; +import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq; +import com.blossom.common.base.util.BeanUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文章相关引用 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleReferenceService extends ServiceImpl { + + /** + * 文章引用记录 + */ + @Transactional(rollbackFor = Exception.class) + public void bind(Long userId, Long sourceId, String sourceName, List references) { + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(ArticleReferenceEntity::getSourceId, sourceId); + baseMapper.delete(where); + + // 没有图片, 则不保存 + if (CollUtil.isEmpty(references)) { + return; + } + List refs = BeanUtil.toList(references, ArticleReferenceEntity.class); + for (ArticleReferenceEntity ref : refs) { + ref.setUserId(userId); + ref.setSourceId(sourceId); + ref.setSourceName(sourceName); + } + baseMapper.insertList(refs); + } + + /** + * 插件图片是否被引用 + * + * @param url 文章url + */ + public boolean check(String url) { + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(ArticleReferenceEntity::getTargetUrl, url); + return baseMapper.exists(where); + } + + /** + * 查询文章引用关系 + * + * @param onlyInner 是否只查询内部文章之间的引用 + * @param userId 用户ID + */ + public Map listAll(boolean onlyInner, Long userId) { + Map result = new HashMap<>(); + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(ArticleReferenceEntity::getUserId, userId); + if (onlyInner) { + where.in(ArticleReferenceEntity::getType, 11); + } else { + where.in(ArticleReferenceEntity::getType, 11, 21); + } + + List all = baseMapper.selectList(where); + if (CollUtil.isEmpty(all)) { + return result; + } + + // ======================================== node ======================================== + // sourceName + Map> source = all.stream().collect(Collectors.groupingBy(ArticleReferenceEntity::getSourceId)); + // targetName, 被引用的同一篇文章可能会有不同名称, 例如 github.com 分别被不同的引用时分别叫 A1,A2 + Map> target = all.stream().collect(Collectors.groupingBy(ArticleReferenceEntity::getTargetUrl)); + + Set nodes = new HashSet<>(); + source.forEach((id, list) -> { + Node node = new Node(list.get(0).getSourceName(), 11); + node.setInner(true); + node.setArtId(id); + nodes.add(node); + }); + target.forEach((name, list) -> { + Node node = new Node(list.get(0).getTargetName(), list.get(0).getType()); + if (list.get(0).getType().equals(11)) { + node.setInner(true); + node.setArtId(list.get(0).getTargetId()); + } else { + node.setInner(false); + node.setArtUrl(list.get(0).getTargetUrl()); + } + nodes.add(node); + }); + + result.put("nodes", nodes); + + // ======================================== link ======================================== + Set links = new HashSet<>(); + for (ArticleReferenceEntity ref : all) { + String sourceName = source.get(ref.getSourceId()).get(0).getSourceName(); + String targetName = target.get(ref.getTargetUrl()).get(0).getTargetName(); + links.add(new Link(sourceName, targetName)); + } + + result.put("links", links); + + return result; + } + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + private static class Node { + private String name; + private Integer artType; + private Boolean inner; + private Long artId; // inner = true + private String artUrl;// inner = false + + public Node(String name, Integer artType) { + this.name = name; + this.artType = artType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if ((null == o) || (this.getClass() != o.getClass())) { + return false; + } + + Node t = (Node) o; + + return this.name.equals(t.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + @Data + @EqualsAndHashCode + private static class Link { + private String source; + private String target; + + public Link(String source, String target) { + this.source = source; + this.target = target; + } + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceEntity.java new file mode 100644 index 0000000..5464e8a --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceEntity.java @@ -0,0 +1,33 @@ +package com.blossom.backend.server.article.reference.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 文章图片引用关系 + * + * @author xzzz + */ +@Data +@TableName("blossom_article_reference") +@EqualsAndHashCode(callSuper = true) +public class ArticleReferenceEntity extends AbstractPOJO implements Serializable { + + @TableId + private Long id; + + private Long sourceId; + private String sourceName; + private Long targetId; + private String targetName; + private String targetUrl; + private Integer type; + + private Long userId; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceReq.java new file mode 100644 index 0000000..ff9172d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/reference/pojo/ArticleReferenceReq.java @@ -0,0 +1,18 @@ +package com.blossom.backend.server.article.reference.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + + +/** + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleReferenceReq extends AbstractPOJO { + private Long targetId; + private String targetName; + private String targetUrl; + private Integer type; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatController.java new file mode 100644 index 0000000..3d7a1c9 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatController.java @@ -0,0 +1,81 @@ +package com.blossom.backend.server.article.stat; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.user.pojo.BlossomUserRes; +import com.blossom.backend.config.BlConstants; +import com.blossom.backend.server.article.draft.pojo.ArticleStatRes; +import com.blossom.backend.server.article.stat.pojo.ArticleHeatmapRes; +import com.blossom.backend.server.article.stat.pojo.ArticleLineRes; +import com.blossom.common.base.pojo.R; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + + +/** + * 文章统计 + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +@RequestMapping("/article/stat") +public class ArticleStatController { + + private final ArticleStatService statService; + + /** + * 文章每日编辑热力图 + * + * @param offsetMonth 向前查询的月数, 填写负数, 默认为 -2 + */ + @AuthIgnore + @GetMapping("/heatmap/open") + public R heatmapOpen( + @RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId, + @RequestParam(value = "offsetMonth", required = false) Integer offsetMonth) { + if (userId == null) { + return R.ok(new ArticleHeatmapRes()); + } + return R.ok(statService.statArticleCountByDay(offsetMonth, userId)); + } + + @GetMapping("/heatmap") + public R heatmap(@RequestParam(value = "offsetMonth", required = false) Integer offsetMonth) { + return R.ok(statService.statArticleCountByDay(offsetMonth, AuthContext.getUserId())); + } + + /** + * 当前文章数和文章字数 + */ + @AuthIgnore + @GetMapping("/words/open") + public R wordOpen(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) { + if (userId == null) { + return R.ok(new ArticleStatRes()); + } + return R.ok(statService.statCount(null, null, userId)); + } + + @GetMapping("/words") + public R word() { + return R.ok(statService.statCount(null, null, AuthContext.getUserId())); + } + + /** + * 文章字数折线图 + */ + @AuthIgnore + @GetMapping("/line/open") + public R lineOpen(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) { + if (userId == null) { + return R.ok(new ArticleLineRes()); + } + return R.ok(statService.statArticleWordsByMonth(userId)); + } + + @GetMapping("/line") + public R line() { + return R.ok(statService.statArticleWordsByMonth(AuthContext.getUserId())); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatJob.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatJob.java new file mode 100644 index 0000000..75ab3de --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatJob.java @@ -0,0 +1,54 @@ +package com.blossom.backend.server.article.stat; + +import cn.hutool.core.collection.CollUtil; +import com.blossom.backend.base.user.UserController; +import com.blossom.backend.base.user.UserService; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.backend.server.article.draft.pojo.ArticleStatRes; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 文章统计任务 + * + * @author xzzz + */ +@Slf4j +@Component +@AllArgsConstructor +public class ArticleStatJob { + + private final ArticleStatService statService; + private final UserService userService; + + /** + * 每10分钟刷新 + */ + @Scheduled(cron = "0 0/10 * * * ?") + public void update() { + log.info("[BLOSSOM] 刷新文章统计"); + // 修改本日编辑文章数 + String toDay = DateUtils.today(); + String toDayBegin = toDay + " 00:00:00"; + String toDayEnd = toDay + " 23:59:59"; + + List users = userService.listAll(); + if (CollUtil.isEmpty(users)) { + return; + } + + for (UserEntity user : users) { + ArticleStatRes statCount = statService.statCount(toDayBegin, toDayEnd, user.getId()); + statService.updByDate(1, toDay, statCount.getArticleCount(), user.getId()); + + String toMouth = DateUtils.format(DateUtils.beginOfMonth(DateUtils.date()), DateUtils.PATTERN_YYYYMMDD); + ArticleStatRes statWord = statService.statCount(null, null, user.getId()); + statService.updByDate(2, toMouth, statWord.getArticleWords(), user.getId()); + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatMapper.java new file mode 100644 index 0000000..8859458 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatMapper.java @@ -0,0 +1,14 @@ +package com.blossom.backend.server.article.stat; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.stat.pojo.ArticleStatEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 文章统计 + * + * @author xzzz + */ +@Mapper +public interface ArticleStatMapper extends BaseMapper { +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatService.java new file mode 100644 index 0000000..6ac8a4f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatService.java @@ -0,0 +1,157 @@ +package com.blossom.backend.server.article.stat; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.article.draft.ArticleMapper; +import com.blossom.backend.server.article.draft.pojo.ArticleStatRes; +import com.blossom.backend.server.article.stat.pojo.ArticleHeatmapRes; +import com.blossom.backend.server.article.stat.pojo.ArticleLineRes; +import com.blossom.backend.server.article.stat.pojo.ArticleStatEntity; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 文章统计 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleStatService extends ServiceImpl { + + private final ArticleMapper articleMapper; + + /** + * 文章编辑热力图, type = 1 + * + * @param offsetMonth 统计最近的月数, 传入负数 + */ + public ArticleHeatmapRes statArticleCountByDay(Integer offsetMonth, Long userId) { + // 默认1个季度 + if (offsetMonth == null) { + offsetMonth = -2; + } + + Date today = DateUtils.date(); + Date thisMonthEnd = DateUtils.endOfMonth(today); + Date before2Month = DateUtils.offsetMonth(thisMonthEnd, offsetMonth); + + String begin = DateUtils.format(before2Month, DateUtils.PATTERN_YYYYMMDD).substring(0, 7) + "-01"; + String end = DateUtils.format(thisMonthEnd, DateUtils.PATTERN_YYYYMMDD); + + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(ArticleStatEntity::getUserId, userId) + .eq(ArticleStatEntity::getType, ArticleStatTypeEnum.ARTICLE_HEATMAP.getType()) + .ge(ArticleStatEntity::getStatDate, begin) + .le(ArticleStatEntity::getStatDate, end); + + // 最近 @{offsetMonth} 个月, 每个月按最大31天计算 + List stats = baseMapper.selectList(where); + + ArticleHeatmapRes res = new ArticleHeatmapRes(); + res.setData(new ArrayList<>()); + res.setMaxStatValues(0); + res.setDateBegin(begin); + res.setDateEnd(end); + + if (CollUtil.isNotEmpty(stats)) { + res.setMaxStatValues(stats.stream().mapToInt(ArticleStatEntity::getStatValue).max().getAsInt()); + for (ArticleStatEntity stat : stats) { + Object[] data = new Object[]{ + DateUtils.format(stat.getStatDate(), DateUtils.PATTERN_YYYYMMDD), + stat.getStatValue() + }; + res.getData().add(data); + } + } + + return res; + + } + + /** + * 文章字数统计 + */ + public ArticleLineRes statArticleWordsByMonth(Long userId) { + Date today = DateUtils.date(); + Date thisMonthEnd = DateUtils.endOfMonth(today); + Date before36Month = DateUtils.offsetMonth(thisMonthEnd, -36); + + String begin = DateUtils.format(before36Month, DateUtils.PATTERN_YYYYMMDD).substring(0, 7) + "-01"; + String end = DateUtils.format(thisMonthEnd, DateUtils.PATTERN_YYYYMMDD); + + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(ArticleStatEntity::getUserId, userId).eq(ArticleStatEntity::getType, ArticleStatTypeEnum.ARTICLE_WORDS.getType()) + .ge(ArticleStatEntity::getStatDate, begin) + .le(ArticleStatEntity::getStatDate, end); + + List stats = baseMapper.selectList(where); + + ArticleLineRes res = new ArticleLineRes(); + res.setStatDates(new ArrayList<>()); + res.setStatValues(new ArrayList<>()); + if (CollUtil.isNotEmpty(stats)) { + for (ArticleStatEntity stat : stats) { + res.getStatDates().add(DateUtils.format(stat.getStatDate(), DateUtils.PATTERN_YYYYMMDD).substring(0, 7)); + res.getStatValues().add(stat.getStatValue()); + } + } + return res; + } + + /** + * 文章统计, 文章数, 总字数 + * + * @param beginUpdTime 修改日期的开始范围 + * @param endUpdTime 修改日期的结束范围 + */ + public ArticleStatRes statCount(String beginUpdTime, String endUpdTime, Long userId) { + return articleMapper.statCount(beginUpdTime, endUpdTime, userId); + } + + @Transactional(rollbackFor = Exception.class) + public void updByDate(Integer type, String date, Integer value, Long userId) { + if (value == null) { + value = 0; + } + log.info("{}:{}:{}", type, date, value); + + LambdaQueryWrapper existWhere = new LambdaQueryWrapper<>(); + existWhere + .eq(ArticleStatEntity::getUserId, userId) + .eq(ArticleStatEntity::getType, type) + .eq(ArticleStatEntity::getStatDate, date); + if (baseMapper.exists(existWhere)) { + LambdaQueryWrapper updWhere = new LambdaQueryWrapper<>(); + updWhere.eq(ArticleStatEntity::getUserId, userId) + .eq(ArticleStatEntity::getType, type) + .eq(ArticleStatEntity::getStatDate, date); + ArticleStatEntity upd = new ArticleStatEntity(); + upd.setStatValue(value); + baseMapper.update(upd, updWhere); + } else { + ArticleStatEntity ist = new ArticleStatEntity(); + ist.setType(type); + ist.setStatDate(DateUtils.parse(date, DateUtils.PATTERN_YYYYMMDD)); + ist.setStatValue(value); + ist.setUserId(userId); + baseMapper.insert(ist); + } + + } + + public static void main(String[] args) { + Date today = DateUtils.date(); + Date thisMonthEnd = DateUtils.endOfMonth(today); + System.out.println(DateUtils.format(thisMonthEnd, DateUtils.PATTERN_YYYYMMDD)); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatTypeEnum.java new file mode 100644 index 0000000..b337c67 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/ArticleStatTypeEnum.java @@ -0,0 +1,28 @@ +package com.blossom.backend.server.article.stat; + +import lombok.Getter; + +/** + * 文章统计类型 + * + * @author xzzz + */ +public enum ArticleStatTypeEnum { + + /** + * 文章每日编辑数, 按热力图显示 + */ + ARTICLE_HEATMAP(1), + + /** + * 截止当月文章总字数, 按折线图显示 + */ + ARTICLE_WORDS(2); + + @Getter + private final Integer type; + + ArticleStatTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleHeatmapRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleHeatmapRes.java new file mode 100644 index 0000000..5f9910b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleHeatmapRes.java @@ -0,0 +1,37 @@ +package com.blossom.backend.server.article.stat.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 文章编辑热力图统计 + * + * @author xzzz + */ +@Data +public class ArticleHeatmapRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * [ + * [日期,值], + * [日期,值] + * ] + */ + private List data; + /** + * 最大修改数据 + */ + private Integer maxStatValues; + /** + * 开始时间 + */ + private String dateBegin; + /** + * 结束时间 + */ + private String dateEnd; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleLineRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleLineRes.java new file mode 100644 index 0000000..df94aa0 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleLineRes.java @@ -0,0 +1,20 @@ +package com.blossom.backend.server.article.stat.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 文章字数折线图统计 + * + * @author xzzz + */ +@Data +public class ArticleLineRes implements Serializable { + + private static final long serialVersionUID = 1L; + + List statDates; + List statValues; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleStatEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleStatEntity.java new file mode 100644 index 0000000..9cbd51d --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/stat/pojo/ArticleStatEntity.java @@ -0,0 +1,40 @@ +package com.blossom.backend.server.article.stat.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 文章统计表 + * + * @author xzzz + */ +@Data +@TableName("blossom_stat") +@EqualsAndHashCode(callSuper = true) +public class ArticleStatEntity extends AbstractPOJO { + + @TableId + private Long id; + + /** + * 统计类型 + */ + private Integer type; + + /** + * 统计日期 + */ + private Date statDate; + + /** + * 统计值 + */ + private Integer statValue; + + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewMapper.java new file mode 100644 index 0000000..bf8f8d4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewMapper.java @@ -0,0 +1,25 @@ +package com.blossom.backend.server.article.view; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.article.view.pojo.ArticleViewEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + + +/** + * 文章访问记录 + * + * @author xzzz + */ +@Mapper +public interface ArticleViewMapper extends BaseMapper { + + /** + * 检查文章的UV + * + * @param today 日期 + * @param ip ip + * @param articleId 文章ID + */ + Long checkUv(@Param("today") String today, @Param("ip") String ip, @Param("articleId") Long articleId); +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewService.java new file mode 100644 index 0000000..afa55c4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewService.java @@ -0,0 +1,53 @@ +package com.blossom.backend.server.article.view; + +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.article.view.pojo.ArticleViewEntity; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +/** + * 文章访问记录 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class ArticleViewService extends ServiceImpl { + + /** + * 检查并新增文章今日UV记录, 注意:非全系统UV + * + * @return true: 今日第一次访问; false:今日已访问 + */ + public boolean uv(String ip, String userAgent, Long articleId) { + Date now = DateUtil.date(); + if (baseMapper.checkUv(DateUtils.toYMD(now), ip, articleId) > 0) { + return false; + } + ArticleViewEntity uv = new ArticleViewEntity(); + uv.setType(ArticleViewTypeEnum.UV.getType()); + uv.setArticleId(articleId); + uv.setIp(ip); + uv.setUserAgent(userAgent); + uv.setCreDay(now); + uv.setCreTime(now); + baseMapper.insert(uv); + return true; + } + + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public void like(ArticleViewEntity req) { + baseMapper.insert(req); + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewTypeEnum.java new file mode 100644 index 0000000..bcc4a0b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/ArticleViewTypeEnum.java @@ -0,0 +1,27 @@ +package com.blossom.backend.server.article.view; + +import lombok.Getter; + +/** + * 文章记录的类型 + * + * @author xzzz + */ +public enum ArticleViewTypeEnum { + + /** + * UV, 每个IP,每天,每个文章唯一 + */ + UV(1), + /** + * 点赞, 每个IP,每个文章唯一 + */ + LIKE(2); + + @Getter + private Integer type; + + ArticleViewTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/pojo/ArticleViewEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/pojo/ArticleViewEntity.java new file mode 100644 index 0000000..8cd4247 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/article/view/pojo/ArticleViewEntity.java @@ -0,0 +1,44 @@ +package com.blossom.backend.server.article.view.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + + +/** + * 文章访问记录 + * + * @author xzzz + */ +@Data +@TableName("blossom_article_view") +@EqualsAndHashCode(callSuper = true) +public class ArticleViewEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** ID */ + @TableId + private Integer id; + /** 文章ID */ + private Long articleId; + /** 事件类型 1:uv; 2:like */ + private Integer type; + /** 地址 */ + private String ip; + /** 设备 */ + private String userAgent; + /** 省 */ + private String province; + /** 市 */ + private String city; + /** 日期 yyyy-MM-dd */ + private Date creDay; + /** 日期 */ + private Date creTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocController.java new file mode 100644 index 0000000..0a35732 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocController.java @@ -0,0 +1,55 @@ +package com.blossom.backend.server.doc; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.config.BlConstants; +import com.blossom.backend.server.doc.pojo.DocTreeReq; +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * 文档 [Doc] + * + * @author xzzz + * @apiNote 包含文件夹和文章 + */ +@RestController +@AllArgsConstructor +@RequestMapping("/doc") +public class DocController { + + private final DocService docService; + + /** + * 全部列表, 编辑器使用 + * + * @return 文件夹列表 + */ + @GetMapping("/trees") + public R> trees(@ModelAttribute DocTreeReq req) { + req.setUserId(AuthContext.getUserId()); + return R.ok(docService.listTree(req)); + } + + /** + * 公开文章和列表, web 使用, 只查询公开的文章 + * + * @return 文件夹列表 + */ + @AuthIgnore + @GetMapping("/trees/open") + public R> trees(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) { + if (userId == null) { + return R.ok(new ArrayList<>()); + } + DocTreeReq open = new DocTreeReq(); + open.setOnlyOpen(true); + open.setUserId(userId); + return R.ok(docService.listTree(open)); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocService.java new file mode 100644 index 0000000..3412c85 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocService.java @@ -0,0 +1,131 @@ +package com.blossom.backend.server.doc; + +import cn.hutool.core.collection.CollUtil; +import com.blossom.backend.server.FolderTypeEnum; +import com.blossom.backend.server.TagEnum; +import com.blossom.backend.server.article.draft.ArticleService; +import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq; +import com.blossom.backend.server.doc.pojo.DocTreeReq; +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import com.blossom.backend.server.folder.FolderService; +import com.blossom.backend.server.folder.pojo.FolderQueryReq; +import com.blossom.backend.server.picture.PictureService; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.backend.server.utils.PictureUtil; +import com.blossom.common.base.enums.YesNo; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文档 + * + * @author xzzz + */ +@Service +@AllArgsConstructor +public class DocService { + private final FolderService folderService; + private final ArticleService articleService; + private final PictureService pictureService; + + /** + * 查询文档树状列表 + * + * @return 树状列表 + */ + public List listTree(DocTreeReq req) { + List all = new ArrayList<>(); + + /* =============================================================================================== + * 只查询文件夹 + * =============================================================================================== */ + if (req.getOnlyFolder()) { + FolderQueryReq where = req.to(FolderQueryReq.class); + List folder = folderService.listTree(where); + all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId()))); + all.addAll(folder); + } + /* =============================================================================================== + * 只查询有图片的文件夹, 包含有图片的文章文件夹 + * =============================================================================================== */ + else if (req.getOnlyPicture()) { + // 有图片的所有文件夹, 包含文章文件夹 + List pids = pictureService.listDistinctPid(req.getUserId()); + if (CollUtil.isNotEmpty(pids)) { + List articleTopFolder = folderService.recursiveToParentTree(pids); + all.addAll(articleTopFolder); + } + // 图片文件夹 + FolderQueryReq folder = req.to(FolderQueryReq.class); + folder.setType(FolderTypeEnum.PICTURE.getType()); + List picFolder = folderService.listTree(folder); + + all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId()))); + all.addAll(picFolder); + } + /* =============================================================================================== + * 只查询公开的的文章和文章文件夹 + * =============================================================================================== */ + else if (req.getOnlyOpen()) { + ArticleQueryReq articleWhere = req.to(ArticleQueryReq.class); + articleWhere.setOpenStatus(YesNo.YES.getValue()); + List articles = articleService.listTree(articleWhere); + all.addAll(articles); + if (CollUtil.isNotEmpty(articles)) { + List pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList()); + List folders = folderService.recursiveToParentTree(pidList); + all.addAll(folders); + } + } + /* =============================================================================================== + * 只查询专题的文章和文件夹 + * =============================================================================================== */ + else if (req.getOnlySubject()) { + FolderQueryReq folderWhere = req.to(FolderQueryReq.class); + folderWhere.setTags(TagEnum.subject.name()); + folderWhere.setType(FolderTypeEnum.ARTICLE.getType()); + List subjects = folderService.listTree(folderWhere); + if (CollUtil.isNotEmpty(subjects)) { + List subjectIds = subjects.stream().map(DocTreeRes::getI).collect(Collectors.toList()); + List foldersTop = folderService.recursiveToParentTree(subjectIds); + List foldersBottom = folderService.recursiveToChildrenTree(subjectIds); + all.addAll(foldersTop); + all.addAll(foldersBottom); + } + List articles = articleService.listTree(req.to(ArticleQueryReq.class)); + all.addAll(articles); + } + /* =============================================================================================== + * 只查询关注的 + * =============================================================================================== */ + else if (req.getOnlyStar()) { + ArticleQueryReq articleWhere = req.to(ArticleQueryReq.class); + articleWhere.setStarStatus(YesNo.YES.getValue()); + List articles = articleService.listTree(articleWhere); + all.addAll(articles); + if (CollUtil.isNotEmpty(articles)) { + List pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList()); + List folders = folderService.recursiveToParentTree(pidList); + all.addAll(folders); + } + } + /* =============================================================================================== + * 默认查询文章文件夹 + * =============================================================================================== */ + else { + FolderQueryReq folder = req.to(FolderQueryReq.class); + folder.setType(FolderTypeEnum.ARTICLE.getType()); + List folders = folderService.listTree(folder); + List articles = articleService.listTree(req.to(ArticleQueryReq.class)); + all.addAll(folders); + all.addAll(articles); + } + + return DocUtil.treeWrap(all.stream().distinct().collect(Collectors.toList())); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocTypeEnum.java new file mode 100644 index 0000000..becee96 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/DocTypeEnum.java @@ -0,0 +1,33 @@ +package com.blossom.backend.server.doc; + +import lombok.Getter; + +/** + * 文档的类型 + * + * @author xzzz + */ +public enum DocTypeEnum { + + /** + * folder article 文章文件夹 + */ + FA(1), + + /** + * folder picture 图片文件夹 + */ + FP(2), + + /** + * article 文章 + */ + A(3); + + @Getter + private final Integer type; + + DocTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeReq.java new file mode 100644 index 0000000..0c38293 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeReq.java @@ -0,0 +1,74 @@ +package com.blossom.backend.server.doc.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 文档树状列表查询请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DocTreeReq extends AbstractPOJO { + /** + * [Folder] 只查询文件夹, 包含文章文件夹和图片文件夹 + */ + private Boolean onlyFolder; + /** + * [Picture + Article]只查询图片文件夹, 以及含有图片的文章文件夹 + */ + private Boolean onlyPicture; + /** + * [Article]只查询公开的文章文件夹 + */ + private Boolean onlyOpen; + /** + * [Article]只查询专题文件夹 + */ + private Boolean onlySubject; + /** + * [Article]问查询有 star 文章的文件夹 + */ + private Boolean onlyStar; + /** + * 用户ID + */ + private Long userId; + + public Boolean getOnlyFolder() { + if (onlyFolder == null) { + return false; + } + return onlyFolder; + } + + public Boolean getOnlyPicture() { + if (onlyPicture == null) { + return false; + } + return onlyPicture; + } + + public Boolean getOnlyOpen() { + if (onlyOpen == null) { + return false; + } + return onlyOpen; + } + + public Boolean getOnlySubject() { + if (onlySubject == null) { + return false; + } + return onlySubject; + } + + public Boolean getOnlyStar() { + if (onlyStar == null) { + return false; + } + return onlyStar; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeRes.java new file mode 100644 index 0000000..bfef782 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/doc/pojo/DocTreeRes.java @@ -0,0 +1,71 @@ +package com.blossom.backend.server.doc.pojo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * 文档树状列表查询响应, 为减少网络消耗使用字段简称 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode +public class DocTreeRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private Long i; + /** + * 父id + */ + @JsonInclude + private Long p; + /** + * 名称, 文件夹名称或文章名称 + */ + private String n; + /** + * open, 公开状态 + */ + private Integer o; + /** + * 版本有差异 version different + */ + private Integer vd; + /** + * 图标 + */ + private String icon; + /** + * 标签 + */ + private List t; + /** + * 排序 + */ + private Integer s; + /** + * 是否收藏 + */ + private Integer star; + /** + * 存储地址 storage path + */ + private String sp; + /** + * 类型 + * @see com.blossom.backend.server.doc.DocTypeEnum + */ + private Integer ty; + /** + * 子菜单 + */ + private List children; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderController.java new file mode 100644 index 0000000..560d140 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderController.java @@ -0,0 +1,100 @@ +package com.blossom.backend.server.folder; + +import cn.hutool.core.util.ObjUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.config.BlConstants; +import com.blossom.backend.server.article.stat.pojo.ArticleLineRes; +import com.blossom.backend.server.folder.pojo.*; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.pojo.R; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + + +/** + * 文件夹 [Folder] + * + * @author xzzz + */ +@RestController +@AllArgsConstructor +@RequestMapping("/folder") +public class FolderController { + private final FolderService baseService; + + /** + * 查询专题列表 [OP] + */ + @AuthIgnore + @GetMapping("/subjects/open") + public R> listSubjectOpen(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) { + if (userId == null) { + return R.ok(new ArrayList<>()); + } + return R.ok(baseService.subjects(userId)); + } + + /** + * 查询专题列表 + */ + @GetMapping("/subjects") + public R> listSubject() { + return R.ok(baseService.subjects(AuthContext.getUserId())); + } + + /** + * 通过ID查询文件夹 + * + * @param id 文件夹ID + * @return 文件夹信息 + */ + @GetMapping("/info") + public R info(@RequestParam("id") Long id) { + FolderEntity entity = baseService.selectById(id); + XzException404.throwBy(ObjUtil.isNull(entity), "文件夹[" + id + "]信息不存在"); + FolderRes res = entity.to(FolderRes.class); + res.setTags(DocUtil.toTagList(entity.getTags())); + res.setType(entity.getType()); + return R.ok(res); + } + + /** + * 新增或修改文件夹 + * + * @param req 文件夹对象 + */ + @PostMapping("/add") + public R insert(@Validated @RequestBody FolderAddReq req) { + FolderEntity folder = req.to(FolderEntity.class); + folder.setTags(DocUtil.toTagStr(req.getTags())); + folder.setUserId(AuthContext.getUserId()); + return R.ok(baseService.insert(folder)); + } + + /** + * 修改文件夹 + * + * @param req 文件夹对象 + */ + @PostMapping("/upd") + public R update(@Validated @RequestBody FolderUpdReq req) { + FolderEntity folder = req.to(FolderEntity.class); + folder.setTags(DocUtil.toTagStr(req.getTags())); + return R.ok(baseService.update(folder)); + } + + /** + * 公开 + */ + @PostMapping("/open") + public R open(@Validated @RequestBody FolderOpenCloseReq req) { + FolderEntity folder = req.to(FolderEntity.class); + return R.ok(baseService.update(folder)); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderMapper.java new file mode 100644 index 0000000..8fa1871 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderMapper.java @@ -0,0 +1,42 @@ +package com.blossom.backend.server.folder; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.folder.pojo.FolderEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文件夹 + * + * @author xzzz + */ +@Mapper +public interface FolderMapper extends BaseMapper { + + /** + * 查询全部 + */ + List listAll(FolderEntity entity); + + /** + * 递归获取传入ID的所有的父文件夹 + */ + List recursiveToParent(@Param("ids") List ids); + + /** + * 递归获取传入ID的所有的子文件夹 + */ + List recursiveToChildren(@Param("ids") List ids); + + /** + * 根据ID修改 + */ + void updById(FolderEntity entity); + + /** + * 根据ID集合修改 + */ + void updByIds(FolderEntity entity); +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderService.java new file mode 100644 index 0000000..14f9fae --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/FolderService.java @@ -0,0 +1,234 @@ +package com.blossom.backend.server.folder; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.TagEnum; +import com.blossom.backend.server.article.draft.ArticleService; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq; +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import com.blossom.backend.server.folder.pojo.FolderEntity; +import com.blossom.backend.server.folder.pojo.FolderQueryReq; +import com.blossom.backend.server.folder.pojo.FolderSubjectRes; +import com.blossom.backend.server.utils.DocUtil; +import com.blossom.common.base.enums.YesNo; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.util.DateUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文件夹 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class FolderService extends ServiceImpl { + + private final ArticleService articleService; + + /** + * 专题列表 + *

1. 查询全部公开专题 + *

2. 由于专题可能会有子文件夹, 所以要查询所有子文件夹. + *

3. 查询出这些文件夹下的所有文章 + *

4. 相同专题的所有文件夹ID归为一组. + *

5. 通过文件夹ID获取到专题下的所有文章, 从而统计文章的总字数, 修改时间, 创建时间等. + *

6. 如果文章包含 TOC 标签, 则该文章为专题的目录, 专题的默认跳转会跳转至该目录 + */ + public List subjects(Long userId) { + // 1. 查询所有公开的专题 + FolderEntity where = new FolderEntity(); + where.setTags(TagEnum.subject.name()); + where.setOpenStatus(YesNo.YES.getValue()); + where.setUserId(userId); + List allOpenSubject = baseMapper.listAll(where); + if (CollUtil.isEmpty(allOpenSubject)) { + return new ArrayList<>(); + } + + // 专题的ID + List allOpenSubjectIds = allOpenSubject.stream().map(FolderEntity::getId).collect(Collectors.toList()); + + // 2. 查询全部专题的子文件夹 + List allOpenSubjectChildFolders = baseMapper.recursiveToChildren(CollUtil.newArrayList(allOpenSubjectIds)); + allOpenSubjectIds.addAll(allOpenSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList())); + + // 3. 查询这些文件夹下的所有文章 + ArticleQueryReq articleWhere = new ArticleQueryReq(); + articleWhere.setPids(allOpenSubjectIds); + articleWhere.setUserId(userId); + // 统计专题信息时, 会包含非公开文章 + // articleWhere.setOpenStatus(YesNo.YES.getValue()); + List articles = articleService.listAll(articleWhere); + + List results = new ArrayList<>(); + + for (FolderEntity subject : allOpenSubject) { + // 专题对象, 包含字数, 更新日期等信息 + FolderSubjectRes result = subject.to(FolderSubjectRes.class); + // 默认专题字数 + result.setSubjectWords(0); + // 默认专题修改时间 + result.setSubjectUpdTime(subject.getCreTime()); + // 4. 这个专题下的所有文件夹ID + List subjectAllId = DocUtil.getChildrenIds(subject.getId(), allOpenSubjectChildFolders); + // 5. 遍历文章, 将文章归属到某个专题下, 并统计相关字数, 日期等信息 + for (ArticleEntity article : articles) { + if (subjectAllId.contains(article.getPid())) { + result.setSubjectWords(result.getSubjectWords() + article.getWords()); + if (DateUtils.compare(result.getSubjectUpdTime(), article.getUpdTime()) < 0) { + result.setSubjectUpdTime(article.getUpdTime()); + } + // 6. 如果是TOC,则设置 TOC_ID + if (TagEnum.isToc(article.getTags())) { + result.setTocId(article.getId()); + } + } + } + + results.add(result); + } + + return results; + } + + /** + * 查询全部文件夹, 并转换成 {@link DocTreeRes} + */ + public List listTree(FolderQueryReq req) { + List folders = baseMapper.listAll(req.to(FolderEntity.class)); + return DocUtil.toTreeRes(folders); + } + + /** + * 递归获取传入ID的所有的父文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己 + * + * @param ids ID 集合 + */ + public List recursiveToParentTree(List ids) { + List folders = baseMapper.recursiveToParent(ids); + return DocUtil.toTreeRes(folders); + } + + /** + * 递归获取传入ID的所有的子文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己 + * + * @param ids ID 集合 + */ + public List recursiveToChildrenTree(List ids) { + List folders = baseMapper.recursiveToChildren(ids); + return DocUtil.toTreeRes(folders); + } + + /** + * 根据ID查询 + * + * @param id 文件夹ID + */ + public FolderEntity selectById(Long id) { + return baseMapper.selectOne(new LambdaQueryWrapper().eq(FolderEntity::getId, id)); + } + + /** + * 新增文件夹 + *

================================================== + *

StorePath 存储路径

+ *

1. 文件夹的 storePath 不允许为 "null". 默认值为 "/" + *

2. 新增文件夹时, 如果传入的 storePath 等于 "null" 或 "/" 时, 会使用父级文件夹的 storePath, + * 如果父级没有显式配置存储路径, 则本文件夹使用 "/". + *

3. 由于文件夹在常规操作下是逐层创建的, 所以只会寻找父级, 而不会递归向上直到顶级. + *

================================================== + *

Type 文件夹类型

+ *

1. 文件夹类型: 1:文章文件夹; 2:图片文件夹 {@link com.blossom.backend.server.FolderTypeEnum} + *

2. 文件夹类型一经创建, 无法修改. + */ + @Transactional(rollbackFor = Exception.class) + public Long insert(FolderEntity folder) { + // 如果是 + if (StrUtil.isBlank(folder.getStorePath()) || "/".equals(folder.getStorePath())) { + FolderEntity parentFolder = this.selectById(folder.getPid()); + // 无上级菜单, 使用默认值, 否则使用上级菜单的路径 + if (parentFolder == null) { + folder.setStorePath(formatStorePath("/")); + } else { + folder.setStorePath(parentFolder.getStorePath()); + } + } + baseMapper.insert(folder); + return folder.getId(); + } + + /** + * 修改文件夹 + *

StorePath 存储路径

+ *

1. 文件夹的 storePath 不允许为 "null". 默认值为 "/" + *

2. 修改文件夹时, 如果传入的 storePath 不为空, 会同时修改所有子文件夹的路径, 但如果子文件夹路径指定了其他路径, 则跳过该子文件夹. + *

{@code
+     * 仅保留与自己路径相同的子文件夹
+     * children.stream().filter(child -> child.getStorePath().equals(oldFolder.getStorePath()))
+     * }
+ */ + @Transactional(rollbackFor = Exception.class) + public Long update(FolderEntity folder) { + XzException404.throwBy(folder.getId() == null, "ID不得为空"); + XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己"); + // 如果 + if (StrUtil.isNotBlank(folder.getStorePath())) { + final FolderEntity oldFolder = selectById(folder.getId()); + // 获取所有子文件夹 + List children = baseMapper.recursiveToChildren(CollUtil.newArrayList(folder.getId())); + // 子文件夹中排除自己, 排除存储路径不等于自己的 + children = children.stream() + .filter(child -> !child.getId().equals(folder.getId())) + .filter(child -> child.getStorePath().equals(oldFolder.getStorePath())) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(children)) { + FolderEntity upd = new FolderEntity(); + upd.setStorePath(folder.getStorePath()); + upd.setIds(children.stream().map(FolderEntity::getId).collect(Collectors.toList())); + baseMapper.updByIds(upd); + } + } + folder.setStorePath(formatStorePath(folder.getStorePath())); + baseMapper.updById(folder); + return folder.getId(); + } + + /** + * 格式化 StorePath + *

1. 在前后增加 / + *

2. 在前后增加 / + * + * @param storePath storePath + */ + private String formatStorePath(String storePath) { + if (storePath == null) { + return null; + } + if ("".equals(storePath)) { + storePath = "/"; + } + if (!StrUtil.endWith(storePath, "/")) { + storePath = storePath + "/"; + } + if (!StrUtil.startWith(storePath, "/")) { + storePath = "/" + storePath; + } + while (storePath.contains("//")) { + storePath = storePath.replace("//", "/"); + } + return storePath; + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderAddReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderAddReq.java new file mode 100644 index 0000000..4149e02 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderAddReq.java @@ -0,0 +1,77 @@ +package com.blossom.backend.server.folder.pojo; + + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 文件夹新增参数 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FolderAddReq extends AbstractPOJO { + + private static final long serialVersionUID = 1L; + + /** + * 父ID + */ + @Min(value = 0, message = "[上级菜单ID] 不能小于0") + @NotNull(message = "[上级菜单] 为必填项") + private Long pid; + /** + * 文件夹名称 + */ + @NotBlank(message = "[文件夹名称] 为必填项") + private String name; + /** + * 图标 + */ + private String icon; + /** + * 标签 + */ + private List tags; + /** + * 排序 + */ + @NotNull(message = "排序为必填项") + @Min(value = 1, message = "排序不能小于0") + private Integer sort; + /** + * 封面图片 + */ + private String cover; + /** + * 备注 + */ + private String describes; + /** + * 存储地址 + */ + @NotBlank(message = "存储地址为必填项, 如不指定请使用 \"/\"") + private String storePath; + /** + * 颜色 + */ + private String color; + /** + * 文件夹类型, 1:文章文件夹; 2:图片文件夹; + *

文件夹类型确认后无法修改 + * + * @see com.blossom.backend.server.FolderTypeEnum + */ + @NotNull(message = "文件夹类型为必填项") + @Min(value = 1, message = "文件夹类型必须为[1/2]") + @Max(value = 2, message = "文件夹类型必须为[1/2]") + private Integer type; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderEntity.java new file mode 100644 index 0000000..c8844ac --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderEntity.java @@ -0,0 +1,104 @@ +package com.blossom.backend.server.folder.pojo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 文件夹 + * + * @author xzzz + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("blossom_folder") +public class FolderEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * id + */ + private Long id; + /** + * 父id + */ + private Long pid; + /** + * 文件夹名称 + */ + private String name; + /** + * 图标 + */ + private String icon; + /** + * 标签 + */ + private String tags; + /** + * 开放状态 + */ + private Integer openStatus; + /** + * 排序 + */ + private Integer sort; + /** + * 封面图片 + */ + private String cover; + /** + * 颜色 + */ + private String color; + /** + * 备注 + */ + private String describes; + /** + * 存储地址, 以 / 开头, 以 / 结尾, 保存时会进行格式校验 + */ + private String storePath; + /** + * 专题字数 + */ + private Integer subjectWords; + /** + * 专题的最后修改时间 + */ + private Date subjectUpdTime; + /** + * 文件夹类型, 1:文章文件夹; 2:图片文件夹; + * @see com.blossom.backend.server.FolderTypeEnum + */ + private Integer type; + /** + * 创建时间 + */ + private Date creTime; + /** + * 修改时间 + */ + private Date updTime; + /** + * 用户ID + */ + private Long userId; + + //region ============================== 非数据库字段 ============================== + + /** + * ID 集合 + */ + @TableField(exist = false) + private List ids; + + //endregion + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderOpenCloseReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderOpenCloseReq.java new file mode 100644 index 0000000..52ba3e5 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderOpenCloseReq.java @@ -0,0 +1,33 @@ +package com.blossom.backend.server.folder.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 文件夹公开关闭请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FolderOpenCloseReq extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + /** id */ + @NotNull(message = "[文件夹ID] 为必填项") + private Long id; + + /** + * open 状态 {@link com.blossom.common.base.enums.YesNo} + */ + @Min(value = 0, message = "[open 状态] 不能小于0") + @Max(value = 1, message = "[open 状态] 不能大于1") + @NotNull(message = "[open 状态] 为必填项") + private Integer openStatus; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderQueryReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderQueryReq.java new file mode 100644 index 0000000..8008767 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderQueryReq.java @@ -0,0 +1,25 @@ +package com.blossom.backend.server.folder.pojo; + +import com.blossom.common.db.pojo.PageReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 文件夹查询请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FolderQueryReq extends PageReq implements Serializable { + + private static final long serialVersionUID = 1L; + + private String tags; + + private Integer type; + + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderRes.java new file mode 100644 index 0000000..f4727f6 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderRes.java @@ -0,0 +1,55 @@ +package com.blossom.backend.server.folder.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 文件夹响应 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FolderRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + /** id */ + private Long id; + /** 父id */ + private Long pid; + /** 文件夹名称 */ + private String name; + /** 图标 */ + private String icon; + /** 标签 */ + private List tags; + /** 是否公开文件夹 [0:未公开,1:公开] */ + private Integer openStatus; + /** 排序 */ + private Integer sort; + /** 封面图片 */ + private String cover; + /** + * 颜色 + */ + private String color; + /** 备注 */ + private String describes; + /** 存储地址 */ + private String storePath; + /** 专题字数 */ + private Integer subjectWords; + /** 专题的最后修改时间 */ + private Date subjectUpdTime; + /** 1:文章;2:图片 */ + private Integer type; + /** 创建时间 */ + private Date creTime; + /** 修改时间 */ + private Date updTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderSubjectRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderSubjectRes.java new file mode 100644 index 0000000..83c1e2b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderSubjectRes.java @@ -0,0 +1,54 @@ +package com.blossom.backend.server.folder.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 专题列表 + * + * @author xzzz + */ +@Data +public class FolderSubjectRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + /** + * 目录文章的ID + */ + private Long tocId; + /** + * 文件夹名称 + */ + private String name; + /** + * 颜色 + */ + private String color; + /** + * 封面 + */ + private String cover; + /** + * 图标 + */ + private String icon; + /** + * 备注 + */ + private String describes; + /** + * 专题字数 + */ + private Integer subjectWords; + /** + * 专题的最后修改时间 + */ + private Date subjectUpdTime; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderUpdReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderUpdReq.java new file mode 100644 index 0000000..6a4f1e4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/folder/pojo/FolderUpdReq.java @@ -0,0 +1,53 @@ +package com.blossom.backend.server.folder.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * 文件夹请求 + *

通常使用在 save 接口 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FolderUpdReq extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + /** id */ + @NotNull(message = "[文件夹ID] 为必填项") + private Long id; + /** 上级菜单ID */ + @Min(value = 0, message = "[上级菜单ID] 不能小于0") + @NotNull(message = "[上级菜单] 为必填项") + private Long pid; + /** 文件夹名称 */ + @NotBlank(message = "[文件夹名称] 为必填项") + private String name; + /** 图标 */ + private String icon; + /** 标签 */ + private List tags; + /** 排序 */ + @NotNull(message = "排序为必填项") + @Min(value = 0, message = "排序不能小于0") + private Integer sort; + /** 封面图片 */ + private String cover; + /** 备注 */ + private String describes; + /** 存储地址 */ + @NotBlank(message = "存储地址为必填项, 如不指定请使用 \"/\"") + private String storePath; + /** + * 颜色 + */ + private String color; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteController.java new file mode 100644 index 0000000..bda2cf5 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteController.java @@ -0,0 +1,67 @@ +package com.blossom.backend.server.note; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.note.pojo.NoteEntity; +import com.blossom.backend.server.note.pojo.NoteSaveReq; +import com.blossom.backend.server.note.pojo.NoteTopReq; +import com.blossom.common.base.pojo.DelReq; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 便签 + * + * @author xzzz + */ +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/note") +public class NoteController extends ServiceImpl { + + private final NoteService baseService; + + /** + * 全部列表 + */ + @GetMapping("/list") + public R> listAll() { + return R.ok(baseService.listAll(AuthContext.getUserId())); + } + + /** + * 保存 + */ + @PostMapping("/add") + public R add(@Validated @RequestBody NoteSaveReq note) { + NoteEntity entity = note.to(NoteEntity.class); + entity.setUserId(AuthContext.getUserId()); + baseService.save(entity); + return R.ok(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R add(@Validated @RequestBody DelReq note) { + baseService.removeById(note.getId()); + return R.ok(); + } + + /** + * 置顶/取消置顶 + */ + @PostMapping("/top") + public R top(@Validated @RequestBody NoteTopReq note) { + baseService.top(note); + return R.ok(); + } +} + diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteMapper.java new file mode 100644 index 0000000..f8f84b1 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteMapper.java @@ -0,0 +1,23 @@ +package com.blossom.backend.server.note; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.note.pojo.NoteEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 便签 + * + * @author xzzz + */ +@Mapper +public interface NoteMapper extends BaseMapper { + + /** + * 置顶/取消置顶 + * + * @param id 便签ID + * @param top 是否置顶 + */ + void top(@Param("id") Long id, @Param("top") Integer top); +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteService.java new file mode 100644 index 0000000..5f5526f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/NoteService.java @@ -0,0 +1,42 @@ +package com.blossom.backend.server.note; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.note.pojo.NoteEntity; +import com.blossom.backend.server.note.pojo.NoteTopReq; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 便签 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class NoteService extends ServiceImpl { + + public List listAll(Long userId) { + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(NoteEntity::getUserId, userId) + .orderByDesc(NoteEntity::getTop) + .orderByDesc(NoteEntity::getTopTime) + .orderByDesc(NoteEntity::getId); + return baseMapper.selectList(where); + } + + /** + * 便签置顶 + * + * @param note 便签 + * @return 是否置顶 + */ + public Integer top(NoteTopReq note) { + baseMapper.top(note.getId(), note.getTop()); + return note.getTop(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteEntity.java new file mode 100644 index 0000000..1e76003 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteEntity.java @@ -0,0 +1,49 @@ +package com.blossom.backend.server.note.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 便签 + * + * @author xzzz + */ +@Data +@TableName("blossom_note") +@EqualsAndHashCode(callSuper = true) +public class NoteEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + /** + * 内容 + */ + private String content; + /** + * 置顶 + */ + private Integer top; + /** + * 置顶时间 + */ + private Date topTime; + /** + * 创建时间 + */ + private Date creTime; + /** + * 用户ID + */ + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteSaveReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteSaveReq.java new file mode 100644 index 0000000..26a5bf8 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteSaveReq.java @@ -0,0 +1,24 @@ +package com.blossom.backend.server.note.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * 保存便签 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoteSaveReq extends AbstractPOJO { + + /** + * 便签内容 + */ + @NotBlank(message = "便签内容为必填项") + private String content; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteTopReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteTopReq.java new file mode 100644 index 0000000..e41890b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/note/pojo/NoteTopReq.java @@ -0,0 +1,29 @@ +package com.blossom.backend.server.note.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +/** + * 便签置顶 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NoteTopReq extends AbstractPOJO { + + /** + * ID + */ + @NotNull(message = "便签ID为必填项") + private Long id; + + /** + * 是否置顶 + */ + @NotNull(message = "是否指定为必填项") + private Integer top; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureBlosController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureBlosController.java new file mode 100644 index 0000000..7ce4cb0 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureBlosController.java @@ -0,0 +1,141 @@ +package com.blossom.backend.server.picture; + +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.io.IoUtil; +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import com.blossom.backend.server.picture.pojo.PictureEntity; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.util.ServletUtil; +import com.blossom.common.base.util.spring.AntPathMatcherUtil; +import com.blossom.common.iaas.OSManager; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +/** + * 图片上传下载, 当使用本地图片存储时, 简短的接口名有助于优化文档布局 + * + * @author xzzz + */ +@Slf4j +@RestController +@AllArgsConstructor +public class PictureBlosController { + + private final PictureService baseService; + private final OSManager osManager; + /** + * 大于 300KB 的图片才进行压缩 + */ + private final static long COMPRESS_MIN_SIZE = 307200; + + // region 上传下载 + + /** + * 上传文件 + * + * @param file 文件 + * @apiNote 上传成功返回文件对象存储地址, 该接口需授权后才可使用。[Content-Type=multipart/form-data] + */ + @PostMapping("/picture/file/upload") + public R uploadFile(@RequestParam("file") MultipartFile file, + @RequestParam(value = "filename", required = false) String filename, + @RequestParam(value = "pid", required = false) Long pid) { + log.warn("上传文件: {}, {}", pid, file.getOriginalFilename()); + PictureEntity picture = baseService.insert(file, filename, pid, AuthContext.getUserId()); + if (picture.getId() == null) { + return R.ok("上传失败"); + } + try (InputStream inputStream = file.getInputStream()) { + return R.ok(osManager.put(picture.getPathName(), inputStream)); + } catch (IOException e) { + e.printStackTrace(); + } + return R.ok(picture.getUrl()); + } + + /** + * 查看图片 [OP] + * + * @param filename 文件名 + * @param scale 图片分辨率缩放 + * @param quality 图片质量缩放 + */ + @AuthIgnore + @GetMapping("/pic/{filename}/**") + public void getFile( + @PathVariable String filename, + @RequestParam(value = "scale", required = false) Float scale, + @RequestParam(value = "quality", required = false) Float quality, + HttpServletRequest request, + HttpServletResponse resp) { + if (scale == null) { + scale = 1f; + } + if (quality == null) { + quality = 1f; + } + + final String path = request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE).toString(); + final String bestMatchingPattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString(); + String arguments = AntPathMatcherUtil.getAntPathMatcher().extractPathWithinPattern(bestMatchingPattern, path); + if (!arguments.isEmpty()) { + filename = "/" + filename + '/' + arguments; + } + File file = osManager.get(filename); + + InputStream ips = null; + FastByteArrayOutputStream ops = null; + try { + // 图片大于 COMPRESS_MIN_SIZE 大小, 且请求进行压缩时 + if (file.length() > COMPRESS_MIN_SIZE && (scale < 1f || quality < 1f)) { + try { + ops = new FastByteArrayOutputStream(); + Thumbnails.of(file) + // 图片大小(长宽)压缩比例 从0-1,1表示原图 + .scale(scale) + // 图片质量压缩比例 从0-1,越接近1质量越好 + .outputQuality(quality) + .toOutputStream(ops); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.toString()); + ips = new FileInputStream(file); + ops = IoUtil.read(ips, true); + } + } else { + ips = new FileInputStream(file); + ops = IoUtil.read(ips, true); + } + + resp.setContentType(ServletUtil.getContentTypeImage(filename)); + OutputStream os = resp.getOutputStream(); + os.write(ops.toByteArray()); + os.flush(); + os.close(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (ips != null) { + ips.close(); + } + if (ops != null) { + ops.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // endregion +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureController.java new file mode 100644 index 0000000..e0d1ca6 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureController.java @@ -0,0 +1,69 @@ +package com.blossom.backend.server.picture; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.picture.pojo.*; +import com.blossom.common.base.pojo.DelReq; +import com.blossom.common.base.pojo.PageRes; +import com.blossom.common.base.pojo.R; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 图片 [Picture] + * + * @author xzzz + */ +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/picture") +public class PictureController { + + private final PictureService baseService; + + /** + * 分页列表 + * + * @return 分页结果 + */ + @GetMapping("/page") + public R> page(@ModelAttribute PicturePageReq req) { + return R.ok(baseService.page(req), PictureRes.class); + } + + /** + * 删除图片 + * + * @param req 图片对象 + * @return 删除结果 + */ + @PostMapping("/del") + public R deleteById(@Validated @RequestBody DelReq req) { + baseService.delete(req.getId()); + return R.ok(); + } + + /** + * 星标图片 + * + * @param req 文章对象 + */ + @PostMapping("/star") + public R star(@Validated @RequestBody PictureStarReq req) { + return R.ok(baseService.update(req.to(PictureEntity.class))); + } + + /** + * 统计图片 + * + * @param pid 文件夹ID + */ + @AuthIgnore + @GetMapping("/stat") + public R stat(@RequestParam(value = "pid", required = false) Long pid) { + return R.ok(baseService.stat(AuthContext.getUserId(), pid)); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureMapper.java new file mode 100644 index 0000000..3e8e2c4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureMapper.java @@ -0,0 +1,42 @@ +package com.blossom.backend.server.picture; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.picture.pojo.PictureEntity; +import com.blossom.backend.server.picture.pojo.PictureStatRes; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 图片 + * + * @author xzzz + */ +@Mapper +public interface PictureMapper extends BaseMapper { + + /** + * 查询全部 + */ + List listAll(PictureEntity entity); + + /** + * 查询所有的 pid, 并去重 + * + * @param userId 用户ID + */ + List listDistinctPid(@Param("userId") Long userId); + + /** + * 修改 + */ + void updById(PictureEntity entity); + + /** + * 统计文件夹下的图片 + * + * @param pid 文件夹ID + */ + PictureStatRes stat(@Param("userId") Long userId, @Param("pid") Long pid); +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureService.java new file mode 100644 index 0000000..ab6df3b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/PictureService.java @@ -0,0 +1,178 @@ +package com.blossom.backend.server.picture; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import com.blossom.backend.server.folder.FolderService; +import com.blossom.backend.server.folder.pojo.FolderEntity; +import com.blossom.backend.server.article.reference.ArticleReferenceService; +import com.blossom.backend.server.picture.pojo.PictureEntity; +import com.blossom.backend.server.picture.pojo.PicturePageReq; +import com.blossom.backend.server.picture.pojo.PictureStatRes; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException400HTTP; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.util.PrimaryKeyUtil; +import com.blossom.common.db.aspect.Pages; +import com.blossom.common.iaas.OSManager; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +/** + * 图片 + * + * @author xzzz + */ +@Slf4j +@Service +@AllArgsConstructor +public class PictureService extends ServiceImpl { + private final FolderService folderService; + private final ArticleReferenceService articleReferenceService; + private final OSManager osManager; + + /** + * 分页列表 + */ + @Pages + public PageInfo page(PicturePageReq page) { + return new PageInfo<>(baseMapper.listAll(page.to(PictureEntity.class))); + } + + /** + * 全部列表 + */ + public List listAll(PictureEntity req) { + return baseMapper.listAll(req); + } + + /** + * 查询所有的 pid, 并去重, 相当于获取所有有图片的文件夹. + *

{@code
+     * select distinct pid from blossom_picture
+     * }
+ * + * @return 如果无结果则返回空集合 + */ + public List listDistinctPid(Long userId) { + List pids = baseMapper.listDistinctPid(userId); + if (CollUtil.isEmpty(pids)) { + return new ArrayList<>(); + } + return pids; + } + + /** + * 新增图片 + *

+ * 文件路径由组成 + *

    + *
  1. 服务器的域名 + nginx 配置的访问路径 + 访问图片的接口(/pic), 例如:
    [https://www.domain.com/bl/pic]
    这个需要在配置文件中进行配置: project.iaas.blos.domain
  2. + *
  3. 文件的默认上传路径, 例如: /home/bl/img, 这个需要在配置文件中进行配置: project.iaas.blos.defaultPath
  4. + *
  5. U + 用户ID, 例如: U1 或 U2...
  6. + *
  7. 上传的文件夹配置的图片路径, 例如: /test/.
  8. + *
+ * + *

1. suffix 文件后缀, 传入的 filename 参数优先级更高, 并且会替换 + *

2. StorePath 存储地址

{@code
+     * 2.1. 图片的存储地址为 :
+     *              os.defaultPath + UID + parentFolder.storePath + filename
+     * 2.2. 图片的访问地址为 :
+     *  os.domain + os.defaultPath + UID + parentFolder.storePath + filename
+     * 2.3. 如果没有上级文件夹, 即 {@param pid} 为空, 则 parentFolder.storePath = "/"
+     * }
+ * + * @param file 文件 + * @param filename 文件名, 文件名不能包含后缀 + * @param pid 上级ID + */ + @Transactional(rollbackFor = Exception.class) + public PictureEntity insert(MultipartFile file, String filename, Long pid, Long userId) { + PictureEntity pic = new PictureEntity(); + pic.setUserId(userId); + pic.setId(PrimaryKeyUtil.nextId()); + // 文件原名 + pic.setSourceName(file.getOriginalFilename()); + // 文件后缀, 后缀无法修改 + pic.setSuffix(FileUtil.getSuffix(pic.getSourceName())); + // 文件名以传入文件名为优先 + if (StrUtil.isBlank(filename)) { + pic.setName(file.getOriginalFilename()); + } else { + pic.setName(filename); + if (StrUtil.isBlank(FileUtil.getSuffix(filename))) { + pic.setName(filename + "." + pic.getSuffix()); + } + } + pic.setSize(file.getSize()); + + final String domain = osManager.getDomain(); + final String rootPath = osManager.getDefaultPath(); + final String uid = "/U" + userId; + final String pname = "/" + pic.getName(); + + // 上传文件夹为空, 则上传至默认文件夹, 默认文件夹是系统提供的无法删除的文件夹, ID为 userId * -1 + if (pid == null || pid < 0) { + pic.setPid(userId * -1); + pic.setPathName(rootPath + uid + pname); + } else { + FolderEntity folder = folderService.selectById(pid); + XzException400HTTP.throwBy(ObjUtil.isNull(folder), "上传文件夹[" + pid + "]不存在, 请核对后再上传"); + final String storePath = StrUtil.isBlank(folder.getStorePath()) ? "/" : folder.getStorePath(); + pic.setPid(pid); + pic.setPathName(rootPath + uid + storePath + pname); + } + + pic.setPathName(pic.getPathName().replaceAll("//", "/")); + pic.setUrl(domain + pic.getPathName()); + + System.out.println(pic.getPathName()); + System.out.println(pic.getUrl()); + + if (baseMapper.exists(new LambdaQueryWrapper().eq(PictureEntity::getPathName, pic.getPathName()))) { + throw new XzException400HTTP("图片[" + pic.getPathName() + "]已存在, 请重命名文件或选择其他路径!"); + } + + baseMapper.insert(pic); + return pic; + } + + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public Long update(PictureEntity req) { + XzException404.throwBy(req.getId() == null, "ID不得为空"); + baseMapper.updById(req); + return req.getId(); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + PictureEntity pic = baseMapper.selectById(id); + if (pic == null) { + return; + } + XzException400.throwBy(articleReferenceService.check(pic.getUrl()), "尚有文章正在引用该图片, 请先将文章中的图片引用删除后, 再删除图片!"); + baseMapper.deleteById(id); + osManager.delete(pic.getPathName()); + } + + public PictureStatRes stat(Long userId, Long pid) { + return baseMapper.stat(userId, pid); + } +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureEntity.java new file mode 100644 index 0000000..d15b810 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureEntity.java @@ -0,0 +1,95 @@ +package com.blossom.backend.server.picture.pojo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 图片 + * + * @author xzzz + */ +@Data +@TableName("blossom_picture") +@EqualsAndHashCode(callSuper = true) +public class PictureEntity extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + /** + * 文件夹ID + */ + private Long pid; + /** + * 原文件名 + */ + private String sourceName; + /** + * 文件名 + */ + private String name; + /** + * 文件路径 + * linux: /usr/local/blossom/abc.png + * os: + */ + private String pathName; + /** + * 文件访问url + * http://localhost:9999/picture/usr/local/blossom/abc.png + */ + private String url; + /** + * 评分 {0,1,2,3,4,5} + */ + private Integer rate; + /** + * 收藏 0:否,1:是 + */ + private Integer starStatus; + /** + * 文件后缀 + */ + private String suffix; + /** + * 文件大小 + */ + private Long size; + /** + * 创建日期 + */ + private Date creTime; + /** + * 用户ID + */ + private Long userId; + + + //region ============================== 非数据库字段 ============================== + + /** + * 该图片被哪些文章引用 + */ + @TableField(exist = false) + private List referenceArticle; + + /** + * 使用了该图片的文章名称 + */ + @TableField(exist = false) + private String articleNames; + + //endregion +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PicturePageReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PicturePageReq.java new file mode 100644 index 0000000..f5760f7 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PicturePageReq.java @@ -0,0 +1,32 @@ +package com.blossom.backend.server.picture.pojo; + +import com.blossom.common.db.pojo.PageReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 图片查询请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PicturePageReq extends PageReq implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 文件夹ID */ + private Long pid; + /** 原文件名 */ + private String sourceName; + /** 文件名 */ + private String name; + /** 文件路径 */ + private String pathName; + /** 评分 {0,1,2,3,4,5} */ + private Integer rate; + /** 收藏 0:否,1:是 */ + private Integer starStatus; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureRes.java new file mode 100644 index 0000000..21d031b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureRes.java @@ -0,0 +1,41 @@ +package com.blossom.backend.server.picture.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + * 图片响应 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PictureRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** ID */ + private Long id; + /** 文件夹ID */ + private Long pid; + /** 文件名 */ + private String name; + /** 文件路径 */ + private String pathName; + /** 文件访问url */ + private String url; + /** 收藏 0:否,1:是 */ + private Integer starStatus; + /** 文件大小 */ + private Long size; + /** 创建日期 */ + private Date creTime; + /** + * 使用了该图片的文章名称 + */ + private String articleNames; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStarReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStarReq.java new file mode 100644 index 0000000..de2f947 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStarReq.java @@ -0,0 +1,29 @@ +package com.blossom.backend.server.picture.pojo; + + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * 图片 star + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PictureStarReq extends AbstractPOJO { + + @Min(value = 0, message = "[文章ID] 不能小于0") + @NotNull(message = "[图片ID] 为必填项") + private Long id; + + @Min(value = 0, message = "[star 状态] 不能小于0") + @Max(value = 1, message = "[star 状态] 不能大于1") + @NotNull(message = "[star 状态] 为必填项") + private Integer starStatus; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStatRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStatRes.java new file mode 100644 index 0000000..c1471d4 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/picture/pojo/PictureStatRes.java @@ -0,0 +1,28 @@ +package com.blossom.backend.server.picture.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 图片统计 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PictureStatRes extends AbstractPOJO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 图片个数 + */ + private Long pictureCount; + /** + * 图片大小 + */ + private Long pictureSize; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanController.java new file mode 100644 index 0000000..7bab24c --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanController.java @@ -0,0 +1,74 @@ +package com.blossom.backend.server.plan; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.plan.pojo.*; +import com.blossom.common.base.pojo.R; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 计划 + * + * @author xzzz + */ +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/plan") +public class PlanController { + + private final PlanService baseService; + + /** + * 每日计划 + * + * @param month 查询的月份, 会查询该月的所有每日计划 + */ + @GetMapping("/list/day") + public R>> days(String month) { + return R.ok(baseService.listDay(month, AuthContext.getUserId())); + } + + /** + * 日常计划 + */ + @GetMapping("/list/daily") + public R> daily() { + return R.ok(baseService.listDaily(AuthContext.getUserId())); + } + + /** + * 新增每日计划 + */ + @PostMapping("/add/day") + public R day(@Validated @RequestBody PlanDayAddReq req) { + req.setUserId(AuthContext.getUserId()); + baseService.addDay(req); + return R.ok(); + } + + /** + * 新增日常计划 + */ + @PostMapping("/add/daily") + public R daily(@Validated @RequestBody PlanDailyAddReq req) { + req.setUserId(AuthContext.getUserId()); + baseService.addDaily(req); + return R.ok(); + } + + /** + * 删除计划 + */ + @PostMapping("/del") + public R del(@RequestBody PlanDelReq req) { + baseService.del(req); + return R.ok(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanMapper.java new file mode 100644 index 0000000..9076750 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanMapper.java @@ -0,0 +1,42 @@ +package com.blossom.backend.server.plan; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.plan.pojo.PlanEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 计划 + * + * @author xzzz + */ +@Mapper +public interface PlanMapper extends BaseMapper { + + /** + * 查询全部 + */ + List listAll(PlanEntity entity); + + /** + * 批量插入 + * + * @param plans 计划列表 + */ + void insertList(@Param("plans") List plans); + + /** + * 根据ID修改 + */ + void updById(PlanEntity entity); + + /** + * 删除计划 + * + * @param id 计划ID + * @param groupId 计划分组ID + */ + void del(@Param("id") Long id, @Param("groupId") Long groupId); +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanPositionEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanPositionEnum.java new file mode 100644 index 0000000..15e2b1f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanPositionEnum.java @@ -0,0 +1,35 @@ +package com.blossom.backend.server.plan; + +import lombok.Getter; + +/** + * 计划位置, 用于前端处理不同样式 + * + * @author xzzz + */ +public enum PlanPositionEnum { + /** + * 每日计划的第一天 + */ + HEAD("head"), + /** + * 每日计划的中间日 + */ + MID("mid"), + /** + * 每日计划的最后一天 + */ + TAIL("tail"), + /** + * 仅有一天的计划 + */ + ALL("all") + ; + + @Getter + private final String position; + + PlanPositionEnum(String position) { + this.position = position; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanService.java new file mode 100644 index 0000000..f322183 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanService.java @@ -0,0 +1,121 @@ +package com.blossom.backend.server.plan; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.plan.pojo.*; +import com.blossom.backend.server.utils.PlanUtil; +import com.blossom.common.base.util.BeanUtil; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.PrimaryKeyUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 计划 + * + * @author xzzz + */ +@Slf4j +@Service +public class PlanService extends ServiceImpl { + + /** + * 按月查询每日计划 + */ + public Map> listDay(String month, Long userId) { + PlanEntity where = new PlanEntity(); + where.setPlanMonth(month); + where.setUserId(userId); + where.setType(PlanTypeEnum.DAY.getType()); + return PlanUtil.sortToTreeMap(BeanUtil.toList(baseMapper.listAll(where), PlanDayRes.class), false); + } + + /** + * 查询日常计划 + */ + public List listDaily(Long userId) { + PlanEntity where = new PlanEntity(); + where.setType(PlanTypeEnum.DAILY.getType()); + where.setUserId(userId); + + List entities = baseMapper.listAll(where); + if (CollUtil.isEmpty(entities)) { + return new ArrayList<>(); + } + + List plans = BeanUtil.toList(entities, PlanDailyRes.class); + String today = DateUtils.today() + " "; + for (PlanDailyRes plan : plans) { + Date start = DateUtils.parse(today + plan.getPlanStartTime() + ":00", DateUtils.PATTERN_YYYYMMDDHHMMSS); + Date end = DateUtils.parse(today + plan.getPlanEndTime() + ":00", DateUtils.PATTERN_YYYYMMDDHHMMSS); + plan.setCurrent(DateUtils.isIn(DateUtil.date(), start, end)); + } + return plans; + } + + /** + * 新增日常计划 + */ + public void addDay(PlanDayAddReq req) { + List plans = new ArrayList<>(); + + if (req.getRepeatDay() == null || req.getRepeatDay() < 1) { + req.setRepeatDay(1); + } + + if (req.getRepeatDay() == 1) { + PlanEntity plan = req.to(PlanEntity.class); + plan.setGroupId(PrimaryKeyUtil.nextId()); + plan.setType(PlanTypeEnum.DAY.getType()); + plan.setPosition(PlanPositionEnum.ALL.getPosition()); + plan.setPlanMonth(DateUtils.format(plan.getPlanDate(), DateUtils.PATTERN_YYYYMM)); + plan.setImg(""); + plans.add(plan); + } else { + final Long groupId = PrimaryKeyUtil.nextId(); + Date planDate = req.getPlanDate(); + for (int i = 1; i <= req.getRepeatDay(); i++) { + PlanEntity plan = req.to(PlanEntity.class); + plan.setGroupId(groupId); + plan.setType(PlanTypeEnum.DAY.getType()); + plan.setPlanMonth(DateUtils.format(planDate, DateUtils.PATTERN_YYYYMM)); + plan.setPlanDate(planDate); + plan.setImg(""); + if (i == 1) { + plan.setPosition(PlanPositionEnum.HEAD.getPosition()); + } else if (i == req.getRepeatDay()) { + plan.setPosition(PlanPositionEnum.TAIL.getPosition()); + } else { + plan.setPosition(PlanPositionEnum.MID.getPosition()); + } + plans.add(plan); + planDate = DateUtils.offsetDay(planDate, 1); + } + } + baseMapper.insertList(plans); + } + + /** + * 新增日常计划 + */ + public void addDaily(PlanDailyAddReq req) { + PlanEntity plan = req.to(PlanEntity.class); + plan.setGroupId(0L); + plan.setType(PlanTypeEnum.DAILY.getType()); + baseMapper.insert(plan); + } + + /** + * 删除计划 + */ + public void del(PlanDelReq req) { + baseMapper.del(req.getId(), req.getGroupId()); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanTypeEnum.java new file mode 100644 index 0000000..c4838d5 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/PlanTypeEnum.java @@ -0,0 +1,27 @@ +package com.blossom.backend.server.plan; + +import lombok.Getter; + +/** + * 计划类型 + * + * @author xzzz + */ +public enum PlanTypeEnum { + /** + * 每日计划 + */ + DAY(1), + /** + * 日常计划 + */ + DAILY(2), + ; + + @Getter + private final Integer type; + + PlanTypeEnum(Integer type) { + this.type = type; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyAddReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyAddReq.java new file mode 100644 index 0000000..03f1400 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyAddReq.java @@ -0,0 +1,39 @@ +package com.blossom.backend.server.plan.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * 新增日常计划 + * + * @author xzzz + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class PlanDailyAddReq extends AbstractPOJO { + + /** + * 内容 + */ + private String content; + /** + * 开始时间 + */ + @NotBlank(message = "开始时间为必填项") + private String planStartTime; + /** + * 结束日期 + */ + @NotBlank(message = "结束时间为必填项") + private String planEndTime; + /** + * 图片 + */ + private String img; + + private Long userId; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyRes.java new file mode 100644 index 0000000..686a813 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDailyRes.java @@ -0,0 +1,41 @@ +package com.blossom.backend.server.plan.pojo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 日常计划响应 + * + * @author xzzz + */ +@Data +public class PlanDailyRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + /** + * 内容 + */ + private String content; + /** + * 开始时间 + */ + private String planStartTime; + /** + * 结束日期 + */ + private String planEndTime; + /** + * 图片 + */ + private String img; + /** + * 是否当前计划 + */ + private Boolean current; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayAddReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayAddReq.java new file mode 100644 index 0000000..057c4d2 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayAddReq.java @@ -0,0 +1,62 @@ +package com.blossom.backend.server.plan.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * 新增每日计划 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PlanDayAddReq extends AbstractPOJO { + + /** + * 标题 + */ + @NotBlank(message = "标题为必填项") + private String title; + /** + * 内容 + */ + private String content; + /** + * 日期 + */ + @NotNull(message = "日期为必填项") + private Date planDate; + /** + * 开始时间 + */ + @NotBlank(message = "开始时间为必填项") + private String planStartTime; + /** + * 结束 + */ + @NotBlank(message = "结束时间为必填项") + private String planEndTime; + /** + * 颜色 + */ + private String color; + /** + * 是否全天 + */ + private Boolean allDay; + /** + * 是否重复 + */ + private Boolean repeat; + /** + * 重复天数 + */ + private Integer repeatDay; + + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayRes.java new file mode 100644 index 0000000..3fbc971 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDayRes.java @@ -0,0 +1,62 @@ +package com.blossom.backend.server.plan.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 每日计划 + * + * @author xzzz + */ +@Data +public class PlanDayRes implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + /** + * 分组ID + */ + private Long groupId; + /** + * 标题 + */ + private String title; + /** + * 内容 + */ + private String content; + /** + * 日期 + */ + private Date planDate; + /** + * 计划开始时间 + */ + private String planStartTime; + /** + * 计划结束时间 + */ + private String planEndTime; + /** + * 颜色 + */ + private String color; + /** + * 位置 + */ + private String position; + /** + * 图片 + */ + private String img; + /** + * 排序 + */ + private Integer sort; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDelReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDelReq.java new file mode 100644 index 0000000..f74a962 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanDelReq.java @@ -0,0 +1,22 @@ +package com.blossom.backend.server.plan.pojo; + +import lombok.Data; + +/** + * 删除计划 + * + * @author xzzz + */ +@Data +public class PlanDelReq { + + /** + * 计划ID + */ + private Long id; + + /** + * 计划组ID + */ + private Long groupId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanEntity.java new file mode 100644 index 0000000..6667168 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/plan/pojo/PlanEntity.java @@ -0,0 +1,69 @@ +package com.blossom.backend.server.plan.pojo; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 计划实体 + * + * @author xzzz + */ +@Data +@TableName("blossom_plan") +@EqualsAndHashCode(callSuper = true) +public class PlanEntity extends AbstractPOJO { + /** + * ID + */ + private Long id; + /** + * 分组ID + */ + private Long groupId; + /** + * 计划类型 + */ + private Integer type; + /** + * 标题 + */ + private String title; + /** + * 内容 + */ + private String content; + /** + * 日期月份 + */ + private String planMonth; + /** + * 日期 + */ + private Date planDate; + /** + * 开始时间 + */ + private String planStartTime; + /** + * 结束 + */ + private String planEndTime; + /** + * 颜色 + */ + private String color; + /** + * 位置 + */ + private String position; + /** + * 图片 + */ + private String img; + + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/ArticleUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/ArticleUtil.java new file mode 100644 index 0000000..c332f94 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/ArticleUtil.java @@ -0,0 +1,123 @@ +package com.blossom.backend.server.utils; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.util.DateUtils; + +import java.util.Date; + +/** + * 文章工具类 + * + * @author xzzz + */ +public class ArticleUtil { + + /** + * 统计文章字数, 只统计中文, 英文 + * + * @param markdown markdown 内容 + * @return 字数 + */ + public static Integer statWords(String markdown) { + if (StrUtil.isBlank(markdown)) { + return 0; + } + String[] lines = markdown.split("\n"); + if (ArrayUtil.isEmpty(lines)) { + return 0; + } + int ch = 0;// 中文字符 + int en = 0;// 英文字符 + int space = 0;// 空格 + int number = 0;// 数字 + int other = 0;// 其他字符 + + // 是否多行代码块内, 多行代码块内的内容不计算在内. + boolean isPreTag = false; + for (String line : lines) { + if (line.length() == 0) { + continue; + } + if (!isPreTag && line.startsWith("```")) { + isPreTag = true; + } else if (isPreTag && line.startsWith("```")) { + isPreTag = false; + } + + if (isPreTag) { + continue; + } + + for (int i = 0; i < line.length(); i++) { + char tmp = line.charAt(i); + if ((tmp >= 'A' && tmp <= 'Z') || (tmp >= 'a' && tmp <= 'z')) { + en++; + } else if ((tmp >= '0') && (tmp <= '9')) { + number++; + } else if (tmp == ' ') { + space++; + } else if (isChinese(tmp)) { + ch++; + } else { + other++; + } + } + } +// System.out.println("中文: " + ch + "个"); +// System.out.println("英文: " + en + "个"); +// System.out.println("数字: " + number + ""); +// System.out.println("空格: " + space + "个"); +// System.out.println("其他: " + other + "个"); + return ch + en + number; + } + + /** + * 字符是否中文 + * + * @param ch 字符 + * @return 是否中文 + */ + private static boolean isChinese(char ch) { + //获取此字符的UniCodeBlock + Character.UnicodeBlock ub = Character.UnicodeBlock.of(ch); + // GENERAL_PUNCTUATION 判断中文的“号 + // CJK_SYMBOLS_AND_PUNCTUATION 判断中文的。号 + if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS + || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B + || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS + || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) { + return true; + } + return false; + + } + + public static void main(String[] args) { +// genHeatmap(); +// genWords(); + System.out.println(DateUtils.today().substring(0, 7) + "-01"); + } + + private static void genHeatmap() { + Date begin = DateUtils.parse("2023-04-01", DateUtils.PATTERN_YYYYMMDD); + Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD); + for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetDay(begin, 1)) { + String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD); + int value = RandomUtil.randomInt(0, 10); + System.out.println(String.format("insert into blossom_stat values(null, 1, '%s' ,%s);", dt, value)); + } + } + + private static void genWords() { + int value = 203012; + Date begin = DateUtils.parse("2019-01-01", DateUtils.PATTERN_YYYYMMDD); + Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD); + for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetMonth(begin, 1)) { + String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD); + value = value + RandomUtil.randomInt(0, 10000); + System.out.println(String.format("insert into blossom_stat values(null, 2, '%s' ,%s);", dt, value)); + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/DocUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/DocUtil.java new file mode 100644 index 0000000..c17b363 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/DocUtil.java @@ -0,0 +1,205 @@ +package com.blossom.backend.server.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.server.article.draft.pojo.ArticleEntity; +import com.blossom.backend.server.doc.DocTypeEnum; +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import com.blossom.backend.server.folder.pojo.FolderEntity; +import com.blossom.common.base.enums.YesNo; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文档工具类 + * + * @author xzzz + */ +public class DocUtil { + + /** + * 即一次菜单的父ID + */ + private static final Long ROOT_FOLDER_ID = 0L; + + /** + * 将菜单列表构造成树状 + * + * @param list 菜单列表 + * @return 树状菜单对象 + */ + public static List treeWrap(List list) { + final List allList = list; + //查询根菜单 + List rootLevel = + allList.stream() + .filter(p -> p.getP().equals(ROOT_FOLDER_ID)) + .sorted(Comparator.comparing(DocTreeRes::getS)) + .collect(Collectors.toList()); + rootLevel.parallelStream().forEach(item -> setChild(item, allList)); + return rootLevel; + } + + private static void setChild(DocTreeRes p, List allFolder) { + //将集合中自己去除 + List child = allFolder.parallelStream() + .filter(a -> a.getP().equals(p.getI())) + .sorted(Comparator.comparing(DocTreeRes::getS)) + .collect(Collectors.toList()); + //如果集合不为空 + if (!CollUtil.isEmpty(child)) { + p.setChildren(child); + //递归设置子元素,多级菜单支持 + child.parallelStream().forEach(c -> setChild(c, allFolder)); + } + } + + /** + * 字符串标签转数组 + * + * @param tags 便签字符串 + * @return 数组字符串 + */ + public static List toTagList(String tags) { + if (StrUtil.isBlank(tags)) { + return new ArrayList<>(); + } + return StrUtil.split(tags, ","); + } + + /** + * 字符串标签转数组 + * + * @param tags 便签字符串 + * @return 数组字符串 + */ + public static String toTagStr(List tags) { + if (CollUtil.isNotEmpty(tags)) { + return CollUtil.join(tags, ","); + } + return ""; + } + + /** + * 文章转 docTree + * + * @param article 文章 + * @return docTree + */ + public static DocTreeRes toDocTree(ArticleEntity article) { + DocTreeRes tree = new DocTreeRes(); + tree.setI(article.getId()); + tree.setP(article.getPid()); + tree.setO(article.getOpenStatus()); + tree.setS(article.getSort()); + tree.setN(article.getName()); + tree.setStar(article.getStarStatus()); + tree.setIcon(article.getIcon()); + tree.setTy(DocTypeEnum.A.getType()); + + // 判断文章的版本与公开版本是否有差异 + if (article.getOpenStatus().equals(YesNo.YES.getValue()) && article.getVersion() > article.getOpenVersion()) { + tree.setVd(YesNo.YES.getValue()); + } + + if (StrUtil.isBlank(article.getTags())) { + tree.setT(new ArrayList<>()); + } else { + tree.setT(DocUtil.toTagList(article.getTags())); + } + return tree; + } + + /** + * 文件夹转 docTree + * + * @param folder 文件夹 + * @return docTree + */ + public static DocTreeRes toDocTree(FolderEntity folder) { + DocTreeRes tree = new DocTreeRes(); + tree.setI(folder.getId()); + tree.setP(folder.getPid()); + tree.setO(folder.getOpenStatus()); + tree.setS(folder.getSort()); + tree.setN(folder.getName()); + tree.setSp(folder.getStorePath()); + tree.setStar(0); + tree.setTy(folder.getType()); + tree.setIcon(folder.getIcon()); + if (StrUtil.isBlank(folder.getTags())) { + tree.setT(new ArrayList<>()); + } else { + tree.setT(DocUtil.toTagList(folder.getTags())); + } + return tree; + } + + /** + * 文件夹集合转 docTree集合 + * + * @param folders 文件夹集合 + * @return docTree + */ + public static List toTreeRes(List folders) { + List folderTrees = new ArrayList(folders.size()); + for (FolderEntity folder : folders) { + folderTrees.add(toDocTree(folder)); + } + return folderTrees; + } + + /** + * 从文件夹集合 {@param folders} 中获取 {@param id} 的子文件夹集合, 并返回这些文件夹的ID + * + * @param id 顶级父ID + * @param folders 文件夹集合 + */ + public static List getChildrenIds(Long id, List folders) { + List result = new ArrayList<>(); + getChildrenIds(result, folders, id); + List ids = result.stream().map(FolderEntity::getId).collect(Collectors.toList()); + ids.add(id); + return ids; + } + + /** + * 递归获取子ID + */ + private static void getChildrenIds(List result, List folders, long id) { + for (FolderEntity org : folders) { + if (org.getPid() != null) { + //遍历出父id等于参数的id,add进子节点集合 + if (org.getPid() == id) { + //递归遍历下一级 + getChildrenIds(result, folders, org.getId()); + result.add(org); + } + } + } + } + + public static void main(String[] args) { + FolderEntity a = new FolderEntity(); + a.setId(1L); + a.setPid(0L); + FolderEntity b = new FolderEntity(); + b.setId(2L); + b.setPid(1L); + FolderEntity c = new FolderEntity(); + c.setId(3L); + c.setPid(2L); + FolderEntity d = new FolderEntity(); + d.setId(4L); + d.setPid(2L); + List folders = new ArrayList<>(); + folders.add(a); + folders.add(b); + folders.add(c); + folders.add(d); + + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PictureUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PictureUtil.java new file mode 100644 index 0000000..b97ba83 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PictureUtil.java @@ -0,0 +1,58 @@ +package com.blossom.backend.server.utils; + +import com.blossom.backend.server.doc.pojo.DocTreeRes; +import net.coobird.thumbnailator.Thumbnails; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * 图片工具类 + * + * @author xzzz + */ +public class PictureUtil { + + /** + * 获取默认图片文件夹 + * + * @return 默认图片文件夹 + */ + public static DocTreeRes getDefaultFolder(Long userId) { + DocTreeRes defaultFolder = new DocTreeRes(); + defaultFolder.setS((int) (userId * -1)); + defaultFolder.setI(userId * -1); + defaultFolder.setP(0L); + defaultFolder.setN("\uD83C\uDF0C 默认文件夹"); + defaultFolder.setO(0); + defaultFolder.setT(new ArrayList<>()); + defaultFolder.setIcon(""); + defaultFolder.setTy(1); + defaultFolder.setStar(0); + return defaultFolder; + } + + public static void compress(String path, String to) { + File file = new File(path); + File toFile = new File(to); + // 图片大于 COMPRESS_MIN_SIZE 大小, 且请求进行压缩时 + try { + Thumbnails.of(file) + // 图片大小(长宽)压缩比例 从0-1,1表示原图 + .scale(0.9) + // 图片质量压缩比例 从0-1,越接近1质量越好 + .outputQuality(0.8) + .toFile(toFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + compress( + "C:\\Users\\Administrator\\Desktop\\home_ld.jpg", + "C:\\Users\\Administrator\\Desktop\\home_ldc.jpg" + ); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PlanUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PlanUtil.java new file mode 100644 index 0000000..abe5a70 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/utils/PlanUtil.java @@ -0,0 +1,157 @@ +package com.blossom.backend.server.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.server.plan.pojo.PlanDayRes; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.SortUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 计划工具类 + * + * @author xzzz + */ +public class PlanUtil { + + /** + * 将计划按天分组, 天数逐天递增 + * + * @param plans 计划列表 + */ + private static TreeMap> byDay(List plans) { + TreeMap> map = new TreeMap<>(); + for (PlanDayRes plan : plans) { + List list = map.getOrDefault(plan.getPlanDate(), new ArrayList<>()); + list.add(plan); + map.put(plan.getPlanDate(), list); + } + return map; + } + + /** + * 计划按 groupId 分组 + * + * @param plans 计划列表 + */ + private static TreeMap> byGroupId(List plans) { + TreeMap> map = new TreeMap<>(); + for (PlanDayRes plan : plans) { + List list = map.getOrDefault(plan.getGroupId(), new ArrayList<>()); + list.add(plan); + map.put(plan.getGroupId(), list); + } + return map; + } + + /** + * 对计划进行排序和填充占位的空计划 + * + * @param plans 计划列表, 传入的计划列表需要提前按 {@link PlanDayRes#getPlanDate()} 升序 + * @return 排序后的计划列表, 注意并不是在 List 中进行排序, 而是补充了 sort 字段 + */ + public static List sort(List plans) { + if (CollUtil.isEmpty(plans)) { + return plans; + } + Map> byDay = byDay(plans); + Map> byGroupId = byGroupId(plans); + + plans.forEach(p -> p.setSort(-1)); + + byDay.forEach((date, list) -> { + for (PlanDayRes plan : list) { + if (plan.getSort() == null) { + plan.setSort(-1); + } + if (plan.getSort() > -1) { + continue; + } + for (int i = 0; i < list.size(); i++) { + final int targetSort = i; + boolean targetSortHasUsed = list.stream().anyMatch(p -> p.getSort() == targetSort && p.getId() > 0); + if (targetSortHasUsed) { + continue; + } + for (PlanDayRes groupPlan : byGroupId.get(plan.getGroupId())) { + groupPlan.setSort(targetSort); + } + } + } + + for (int i = 0; i < list.stream().mapToInt(PlanDayRes::getSort).max().getAsInt(); i++) { + final int targetSort = i; + if (list.stream().noneMatch(p -> p.getSort() == targetSort)) { + plans.add(getHolderPlan(date, targetSort)); + } + } + }); + return plans; + } + + /** + * 对计划进行排序和填充占位的空计划, 并转换成按天分组后的 treeMap + * + * @param plans 计划列表, 传入的计划列表需要提前按 {@link PlanDayRes#getPlanDate()} 升序 + * @return treeMap key 为日期(天), value 为该日期计划数组 + */ + public static TreeMap> sortToTreeMap(List plans, boolean debug) { + if (CollUtil.isEmpty(plans)) { + return new TreeMap<>(); + } + Map> map = sort(plans).stream() + .collect(Collectors.groupingBy(PlanDayRes::getPlanDate)); + TreeMap> treeMap = MapUtil.sort(map); + treeMap.forEach((k, v) -> { + v = v.stream().sorted((p1, p2) -> SortUtil.intSort.compare(p1.getSort(), p2.getSort())).collect(Collectors.toList()); + treeMap.put(k, v); + }); + + if (debug) { + debugTreeMap(treeMap); + } + + return treeMap; + } + + /** + * 控制台打印计划的排序 + */ + private static void debugTreeMap(TreeMap> treeMap) { + for (Date date : treeMap.keySet()) { + System.out.print(DateUtils.format(date, DateUtils.PATTERN_YYYYMMDD) + "|"); + } + System.out.println(); + for (int i = 0; i < treeMap.values().size(); i++) { + for (List value : treeMap.values()) { + if (i > value.size() - 1) { + System.out.print(" |"); + } else { + System.out.print(StrUtil.fillBefore(String.valueOf(value.get(i).getGroupId()), ' ', 10)); + System.out.print("|"); + } + } + System.out.println(); + } + } + + /** + * 获取一个占位计划, 用于在每日的计划列表数组中站位 + * + * @param date 日期 + * @param sort 顺序 + */ + private static PlanDayRes getHolderPlan(Date date, int sort) { + PlanDayRes holder = new PlanDayRes(); + holder.setId(sort * -1L - 1); + holder.setGroupId(0L); + holder.setPlanDate(date); + holder.setSort(sort); + holder.setColor("hold"); + return holder; + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebController.java new file mode 100644 index 0000000..ca8c1bf --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebController.java @@ -0,0 +1,60 @@ +package com.blossom.backend.server.web; + +import com.blossom.backend.base.auth.AuthContext; +import com.blossom.backend.server.web.pojo.WebEntity; +import com.blossom.backend.server.web.pojo.WebSaveReq; +import com.blossom.common.base.pojo.DelReq; +import com.blossom.common.base.pojo.R; +import com.blossom.backend.base.auth.annotation.AuthIgnore; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 网站收藏 + * + * @author xzzz + */ +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/web") +public class WebController { + + private final WebService baseService; + + /** + * 网站列表 + */ + @AuthIgnore + @GetMapping("/list") + public R>> listAll() { + return R.ok(baseService.listAll(AuthContext.getUserId())); + } + + /** + * 保存 + */ + @PostMapping("/save") + public R save(@Validated @RequestBody WebSaveReq req) { + if (req.getId() != null && baseService.getById(req.getId()) != null) { + return R.ok(baseService.updateById(req.to(WebEntity.class))); + } else { + req.setUserId(AuthContext.getUserId()); + return R.ok(baseService.save(req.to(WebEntity.class))); + } + } + + /** + * 删除 + */ + @PostMapping("/del") + public R save(@Validated @RequestBody DelReq req) { + baseService.removeById(req.getId()); + return R.ok(); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebMapper.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebMapper.java new file mode 100644 index 0000000..17d430f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebMapper.java @@ -0,0 +1,15 @@ +package com.blossom.backend.server.web; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.blossom.backend.server.web.pojo.WebEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 网站收藏 + * + * @author xzzz + */ +@Mapper +public interface WebMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebService.java new file mode 100644 index 0000000..22cedf7 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/WebService.java @@ -0,0 +1,40 @@ +package com.blossom.backend.server.web; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.blossom.backend.server.web.pojo.WebEntity; +import com.blossom.common.base.util.SortUtil; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 网站收藏 + * + * @author xzzz + */ +@Service +public class WebService extends ServiceImpl { + + /** + * 全部列表 + * + * @return 网站的分类:分类下的网站集合 + */ + public Map> listAll(Long userId) { + LambdaQueryWrapper where = new LambdaQueryWrapper<>(); + where.eq(WebEntity::getUserId, userId); + List list = baseMapper.selectList(where); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(0); + } + return list.stream() + .sorted((w1, w2) -> SortUtil.intSort.compare(w1.getSort(), w2.getSort())) + .collect(Collectors.groupingBy(WebEntity::getType)); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebEntity.java new file mode 100644 index 0000000..772aa39 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebEntity.java @@ -0,0 +1,57 @@ +package com.blossom.backend.server.web.pojo; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 网站收藏 + * + * @author xzzz + */ +@Data +@TableName("blossom_web") +@EqualsAndHashCode(callSuper = true) +public class WebEntity extends AbstractPOJO { + + /** + * ID + */ + @TableId + private Long id; + /** + * 网站名称 + */ + private String name; + /** + * 网站链接 + */ + private String url; + /** + * 图标 + */ + private String icon; + /** + * 图片, 图片的优先级比图标高 + */ + private String img; + /** + * 类型 + */ + private String type; + /** + * 排序 + */ + private Integer sort; + /** + * 创建时间 + */ + private Date creTime; + + private Long userId; +} + diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebSaveReq.java b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebSaveReq.java new file mode 100644 index 0000000..3310c71 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/server/web/pojo/WebSaveReq.java @@ -0,0 +1,52 @@ +package com.blossom.backend.server.web.pojo; + +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 网站保存请求 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WebSaveReq extends AbstractPOJO { + /** + * ID 新增非必填, 修改必填 + */ + private Long id; + /** + * 网站名称 + */ + @NotBlank(message = "网站名称为必填项") + private String name; + /** + * 网站链接 + */ + @NotBlank(message = "网站地址为必填项") + private String url; + /** + * 网站图标 + */ + private String icon; + /** + * 网站图片 + */ + private String img; + /** + * 网站类型 + */ + @NotBlank(message = "网站类型为必填项") + private String type; + /** + * 网站排序 + */ + @NotNull(message = "排序为必填项") + private Integer sort; + + private Long userId; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/ThirdPartyScheduled.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/ThirdPartyScheduled.java new file mode 100644 index 0000000..77df22b --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/ThirdPartyScheduled.java @@ -0,0 +1,68 @@ +package com.blossom.backend.thirdparty; + +import com.blossom.backend.base.user.UserService; +import com.blossom.backend.base.user.pojo.UserEntity; +import com.blossom.backend.thirdparty.gitee.GiteeManager; +import com.blossom.backend.thirdparty.hefeng.WeatherManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 三方接口定时任务 + * + * @author : xzzz + */ +@Component +@RestController +@RequestMapping("/thirdparty/scheduled") +public class ThirdPartyScheduled { + private static final Logger log = LoggerFactory.getLogger(ThirdPartyScheduled.class); + + @Autowired + private GiteeManager giteeManager; + + @Autowired + private WeatherManager weatherManager; + + @Autowired + private UserService userService; + + /** + * 每30分钟刷新 + */ + @RequestMapping("/weather") + @Scheduled(cron = "0 0/30 * * * ?") + public void refreshWeather() { + log.info("[BLOSSOM] 刷新天气"); + List users = userService.listAll(); + Set locations = users.stream().collect(Collectors.groupingBy(UserEntity::getLocation)).keySet(); + for (String location : locations) { + weatherManager.clearAll(location); + weatherManager.findWeatherAll(location); + } + } + + /** + * 每10分钟刷新 + */ + @RequestMapping("/gitee") + public void refreshHeatmapCache() { + try { + giteeManager.clearCache(); + giteeManager.heatmap(); + log.info("[BLOSSOM] 刷新码云热力图"); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/GiteeManager.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/GiteeManager.java new file mode 100644 index 0000000..863dc18 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/GiteeManager.java @@ -0,0 +1,133 @@ +package com.blossom.backend.thirdparty.gitee; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.backend.base.param.ParamEnum; +import com.blossom.backend.base.param.ParamService; +import com.blossom.backend.thirdparty.gitee.pojo.Events; +import com.blossom.backend.thirdparty.gitee.pojo.HeatmapRes; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.base.util.okhttp.HttpUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 码云服务 + * + * @author jasmine + */ +@Slf4j +@Service +public class GiteeManager { + private static final String CACHE_HEATMAP = "cache_heatmap"; + private static final Map CACHE_MAP = new HashMap<>(); + + private static final String EVENTS_URL = "https://gitee.com/api/v5/users/jasminexz/events?access_token=%s&limit=50&prev_id=%s"; + + private static String accessToken; + + @Autowired + private ParamService paramService; + + /** + * 获取事件列表, 用于获取事件发生时间, 并构造成日历热力图 + *
    + *
  • 码云: 查询3个月内的事件, 并按天分组
  • + *
+ * + * @return 热力图 + */ + public HeatmapRes heatmap() { + if (checkToken()) { + log.warn("未配置GITEE信息"); + return new HeatmapRes(); + } + + HeatmapRes cache = CACHE_MAP.get(CACHE_HEATMAP); + if (cache != null) { + return cache; + } + + DateTime today = DateUtil.date(); + DateTime thisMonthLastDay = DateUtil.endOfMonth(today); + DateTime pass3Month = DateUtil.offsetMonth(today, -2); + DateTime pass3MonthFirstDay = DateUtil.beginOfMonth(pass3Month); + // 3个月内的 + 本月的 + List dateRange = DateUtil.rangeToList(pass3MonthFirstDay, thisMonthLastDay, DateField.DAY_OF_MONTH); + + List all = new ArrayList<>(); + // 分页循环查询动态,知道查询到3个月之前的数据则停止 + int prev_id = 0; + while (true) { + String url = String.format(EVENTS_URL, accessToken, prev_id); + List res = JsonUtil.toObj(getDate(url), new TypeReference>() {}); + if (CollUtil.isEmpty(res)) { + break; + } + all.addAll(res); + Integer id = res.get(res.size() - 1).getId(); + Date lastIndexDate = DateUtil.parseDate(res.get(res.size() - 1).getCreated_at()); + if (DateUtil.compare(lastIndexDate, pass3MonthFirstDay) < 0) { + break; + } + prev_id = id; + } + + Map> maps = all.stream().peek(doc -> { + Date date = DateUtil.parse(doc.getCreated_at(), "yyyy-MM-dd"); + doc.setCreated_at(DateUtil.format(date, "yyyy-MM-dd")); + }).collect(Collectors.groupingBy(Events::getCreated_at)); + + List resultList = new ArrayList<>(); + int maxUpdateNum = 0; + for (DateTime dateTime : dateRange) { + String ymdDate = dateTime.toString("yyyy-MM-dd"); + List list = maps.get(ymdDate); + int size = list == null ? 0 : list.size(); + if (size > maxUpdateNum) { + maxUpdateNum = size; + } + resultList.add(new Object[]{ymdDate, size}); + } + + HeatmapRes res = new HeatmapRes(); + res.setDateBegin(pass3MonthFirstDay.toString("yyyy-MM-dd")); + res.setDateEnd(thisMonthLastDay.toString("yyyy-MM-dd")); + res.setMaxUpdateNum(maxUpdateNum); + res.setData(resultList); + + CACHE_MAP.put(CACHE_HEATMAP, res); + + return res; + } + + public void clearCache() { + CACHE_MAP.put(CACHE_HEATMAP, null); + } + + private String getDate(String url) { + String body = HttpUtil.get(url); + if (StrUtil.isBlank(body)) { + throw new XzException404("未获取到响应数据"); + } + return body; + } + + private boolean checkToken() { + Map paramMap = paramService.selectMap(false, ParamEnum.GITEE_ACCESS_TOKEN); + if (MapUtil.isNotEmpty(paramMap)) { + accessToken = paramMap.get(ParamEnum.GITEE_ACCESS_TOKEN.name()); + } + return StrUtil.isBlank(accessToken); + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/Events.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/Events.java new file mode 100644 index 0000000..4e63b27 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/Events.java @@ -0,0 +1,27 @@ +package com.blossom.backend.thirdparty.gitee.pojo; + +/** + * gitee events + * + * @author xzzz + */ +public class Events { + private Integer id; + private String created_at; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCreated_at() { + return created_at; + } + + public void setCreated_at(String created_at) { + this.created_at = created_at; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/HeatmapRes.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/HeatmapRes.java new file mode 100644 index 0000000..7948b2f --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/gitee/pojo/HeatmapRes.java @@ -0,0 +1,30 @@ +package com.blossom.backend.thirdparty.gitee.pojo; + +import lombok.Data; + +import java.util.List; + +/** + * 热力图响应 + * + * @author xzzz + */ +@Data +public class HeatmapRes { + /** + * 最大修改数据 + */ + private Integer maxUpdateNum; + /** + * 开始时间 + */ + private String dateBegin; + /** + * 结束时间 + */ + private String dateEnd; + /** + * 从开始时间到结束的数据 + */ + private List data; +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeCondCode.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeCondCode.java new file mode 100644 index 0000000..151a920 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeCondCode.java @@ -0,0 +1,122 @@ +package com.blossom.backend.thirdparty.hefeng; + +import cn.hutool.core.util.StrUtil; + +import java.util.HashMap; +import java.util.Map; + +/** + * 图标地址 + * + * @author xzzz + */ +@SuppressWarnings("all") +public class HeCondCode { + + private static final Map WEATHER_CODE_MAP = new HashMap<>(100); + + static { + WEATHER_CODE_MAP.put("100",new Node("#wt-qing", "晴")); + WEATHER_CODE_MAP.put("101",new Node("#wt-duoyun", "多云")); + WEATHER_CODE_MAP.put("102",new Node("#wt-duoyun", "少云")); + WEATHER_CODE_MAP.put("103",new Node("#wt-qingduoyun", "晴间多云")); + WEATHER_CODE_MAP.put("104",new Node("#wt-yin", "阴")); + WEATHER_CODE_MAP.put("150",new Node("#wt-qing", "晴")); + WEATHER_CODE_MAP.put("151",new Node("#wt-duoyun", "多云")); + WEATHER_CODE_MAP.put("152",new Node("#wt-duoyun", "少云")); + WEATHER_CODE_MAP.put("153",new Node("#wt-qingduoyun", "晴间多云")); + WEATHER_CODE_MAP.put("154",new Node("#wt-yin", "阴")); + WEATHER_CODE_MAP.put("200",new Node("#wt-feng", "有风")); + WEATHER_CODE_MAP.put("201",new Node("#wt-qing", "平静")); + WEATHER_CODE_MAP.put("202",new Node("#wt-qing", "微风")); + WEATHER_CODE_MAP.put("203",new Node("#wt-qing", "和风")); + WEATHER_CODE_MAP.put("204",new Node("#wt-qing", "清风")); + WEATHER_CODE_MAP.put("205",new Node("#wt-feng", "强风劲风")); + WEATHER_CODE_MAP.put("206",new Node("#wt-feng", "疾风")); + WEATHER_CODE_MAP.put("207",new Node("#wt-feng", "大风")); + WEATHER_CODE_MAP.put("208",new Node("#wt-feng", "烈风")); + WEATHER_CODE_MAP.put("209",new Node("#wt-feng", "风暴")); + WEATHER_CODE_MAP.put("210",new Node("#wt-feng", "狂爆风")); + WEATHER_CODE_MAP.put("211",new Node("#wt-feng", "飓风")); + WEATHER_CODE_MAP.put("212",new Node("#wt-feng", "龙卷风")); + WEATHER_CODE_MAP.put("213",new Node("#wt-feng", "热带风暴")); + WEATHER_CODE_MAP.put("300",new Node("#wt-yu", "阵雨")); + WEATHER_CODE_MAP.put("301",new Node("#wt-yu", "强阵雨")); + WEATHER_CODE_MAP.put("302",new Node("#wt-leijiayu", "雷阵雨")); + WEATHER_CODE_MAP.put("303",new Node("#wt-leijiayu", "强雷阵雨")); + WEATHER_CODE_MAP.put("304",new Node("#wt-leijiayu", "阵雨冰雹")); + WEATHER_CODE_MAP.put("305",new Node("#wt-yu", "小雨")); + WEATHER_CODE_MAP.put("306",new Node("#wt-yu", "中雨")); + WEATHER_CODE_MAP.put("307",new Node("#wt-zhongyu", "大雨")); + WEATHER_CODE_MAP.put("308",new Node("#wt-zhongyu", "极端降雨")); + WEATHER_CODE_MAP.put("309",new Node("#wt-yu ", "毛毛细雨")); + WEATHER_CODE_MAP.put("310",new Node("#wt-leijiayu", "暴雨")); + WEATHER_CODE_MAP.put("311",new Node("#wt-leijiayu", "大暴雨")); + WEATHER_CODE_MAP.put("312",new Node("#wt-leijiayu", "特大暴雨")); + WEATHER_CODE_MAP.put("313",new Node("#wt-leijiayu", "冻雨")); + WEATHER_CODE_MAP.put("314",new Node("#wt-zhongyu", "小到中雨")); + WEATHER_CODE_MAP.put("315",new Node("#wt-leijiayu", "中到大雨")); + WEATHER_CODE_MAP.put("316",new Node("#wt-leijiayu", "大到暴雨")); + WEATHER_CODE_MAP.put("317",new Node("#wt-leijiayu", "暴到大暴雨")); + WEATHER_CODE_MAP.put("318",new Node("#wt-leijiayu", "大暴特暴雨")); + WEATHER_CODE_MAP.put("399",new Node("#wt-yu", "雨")); + WEATHER_CODE_MAP.put("400",new Node("#wt-xue", "小雪")); + WEATHER_CODE_MAP.put("401",new Node("#wt-daxue", "中雪")); + WEATHER_CODE_MAP.put("402",new Node("#wt-daxue", "大雪")); + WEATHER_CODE_MAP.put("403",new Node("#wt-daxue", "暴雪")); + WEATHER_CODE_MAP.put("404",new Node("#wt-daxue", "雨夹雪")); + WEATHER_CODE_MAP.put("405",new Node("#wt-daxue", "雨雪天气")); + WEATHER_CODE_MAP.put("406",new Node("#wt-daxue", "阵雨夹雪")); + WEATHER_CODE_MAP.put("407",new Node("#wt-xue", "阵雪")); + WEATHER_CODE_MAP.put("408",new Node("#wt-xue", "小到中雪")); + WEATHER_CODE_MAP.put("409",new Node("#wt-daxue", "中到大雪")); + WEATHER_CODE_MAP.put("410",new Node("#wt-daxue", "大到暴雪")); + WEATHER_CODE_MAP.put("499",new Node("#wt-xue", "雪")); + WEATHER_CODE_MAP.put("500",new Node("#wt-wu", "薄雾")); + WEATHER_CODE_MAP.put("501",new Node("#wt-wu", "雾")); + WEATHER_CODE_MAP.put("502",new Node("#wt-wu", "霾")); + WEATHER_CODE_MAP.put("503",new Node("#wt-wu", "扬沙")); + WEATHER_CODE_MAP.put("504",new Node("#wt-wu", "浮尘")); + WEATHER_CODE_MAP.put("507",new Node("#wt-wu", "沙尘暴")); + WEATHER_CODE_MAP.put("508",new Node("#wt-wu", "强沙尘暴")); + WEATHER_CODE_MAP.put("509",new Node("#wt-wu", "浓雾")); + WEATHER_CODE_MAP.put("510",new Node("#wt-wu", "强浓雾")); + WEATHER_CODE_MAP.put("511",new Node("#wt-wu", "中度霾")); + WEATHER_CODE_MAP.put("512",new Node("#wt-wu", "重度霾")); + WEATHER_CODE_MAP.put("513",new Node("#wt-wu", "严重霾")); + WEATHER_CODE_MAP.put("514",new Node("#wt-wu", "大雾")); + WEATHER_CODE_MAP.put("515",new Node("#wt-wu", "特强浓雾")); + WEATHER_CODE_MAP.put("900",new Node("#", "热")); + WEATHER_CODE_MAP.put("901",new Node("#", "冷")); + WEATHER_CODE_MAP.put("999",new Node("#", "未知")); + } + + + static String getIcon(String code) { + Node node = WEATHER_CODE_MAP.get(StrUtil.isBlank(code) ? "999" : code); + return node == null ? "#" : node.getIcon(); + } + + static String getText(String code) { + Node node = WEATHER_CODE_MAP.get(StrUtil.isBlank(code) ? "999" : code); + return node == null ? "#" : node.getText(); + } + + static class Node { + private String icon; + private String text; + + Node(String icon, String text) { + this.icon = icon; + this.text = text; + } + + public String getIcon() { + return icon; + } + + public String getText() { + return text; + } + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeWeatherDTO.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeWeatherDTO.java new file mode 100644 index 0000000..9a28981 --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/HeWeatherDTO.java @@ -0,0 +1,158 @@ +package com.blossom.backend.thirdparty.hefeng; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.util.List; + +/** + * 天气信息 + * + * @author xzzz + */ +@Data +@SuppressWarnings("all") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class HeWeatherDTO { + + private Location location; + private Now now; + private List hourly; + private List daily; + + /** + * 位置 + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Location { + /** + * 地区-城市名称 + */ + private String name; + } + + /** + * 当前天气 + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Now { + /** + * 实况观测时间 + */ + private String obsTime; + /** + * 实况温度,默认单位:摄氏度 + */ + private String temp; + /** + * 实况体感温度,默认单位:摄氏度 + */ + private String feelsLike; + /** + * 当前天气状况和图标的代码 + */ + private String icon; + private String iconValue; + /** + * 实况天气状况的文字描述,包括阴晴雨雪等天气状态的描述 + */ + private String text; + /** + * 实况风向360角度 + */ + private String wind360; + /** + * 实况风向 + */ + private String windDir; + /** + * 实况风力等级 + */ + private String windScale; + /** + * 实况风速,公里/小时 + */ + private String windSpeed; + /** + * 实况相对湿度,百分比数值 + */ + private String humidity; + /** + * 实况降水量,默认单位:毫米 + */ + private String precip; + /** + * 实况大气压强,默认单位:百帕 + */ + private String pressure; + /** + * 实况能见度,默认单位:公里 + */ + private String vis; + /** + * 实况云量,百分比数值 + */ + private String cloud; + /** + * 实况露点温度 + */ + private String dew; + + } + + /** + * 天气预报 + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Daily { + /** + * 预报日期2013-12-30 + */ + private String fxDate; + /** + * 预报当天最高温度 + */ + private String tempMax; + /** + * 预报当天最低温度 + */ + private String tempMin; + private String iconDay; + private String iconValueDay; + private String textDay; + private String iconNight; + private String iconValueNight; + private String textNight; + } + + /** + * 逐小时预报 + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Hourly { + /** + * 逐小时预报时间 + */ + private String fxTime; + /** + * 逐小时预报温度 + */ + private String temp; + /** + * 逐小时预报天气状况图标代码 + */ + private String icon; + /** + * 图标 + */ + private String iconValue; + /** + * 逐小时预报天气状况文字描述 + */ + private String text; + } +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherController.java new file mode 100644 index 0000000..3fd10cc --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherController.java @@ -0,0 +1,30 @@ +package com.blossom.backend.thirdparty.hefeng; + +import com.blossom.common.base.pojo.R; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 和风天气 + * + * @author xzzz + */ +@RestController +@RequestMapping("/weather") +public class WeatherController { + + @Autowired + private WeatherManager weatherManager; + + /** + * 获取天气信息 + */ + @GetMapping + public R weather(@RequestParam("location")String location) { + return R.ok(weatherManager.findWeatherAll(location)); + } + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherHeEntity.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherHeEntity.java new file mode 100644 index 0000000..1bea0dc --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherHeEntity.java @@ -0,0 +1,25 @@ +package com.blossom.backend.thirdparty.hefeng; + +import lombok.Data; + +/** + * @author xzzz + */ +@Data +public class WeatherHeEntity { + + private String locationId; + private String locationEName; + private String locationCName; + private String countryCode; + private String countryEName; + private String countryCName; + private String adm1EName; + private String adm1CName; + private String adm2EName; + private String adm2CName; + private String lat; + private String lng; + private String adCode; + +} diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherManager.java b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherManager.java new file mode 100644 index 0000000..d24757e --- /dev/null +++ b/blossom-backend/backend/src/main/java/com/blossom/backend/thirdparty/hefeng/WeatherManager.java @@ -0,0 +1,140 @@ +package com.blossom.backend.thirdparty.hefeng; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.blossom.backend.base.param.ParamEnum; +import com.blossom.backend.base.param.ParamService; +import com.blossom.common.base.exception.XzException400; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author : xzzz + */ +@Component +public class WeatherManager { + private static final Logger log = LoggerFactory.getLogger(WeatherManager.class); + + private static final String URL_CITY = "https://geoapi.qweather.com/v2/city/lookup"; + private static final String URL_NOW = "https://devapi.heweather.net/v7/weather/now"; + private static final String URL_DAILY = "https://devapi.heweather.net/v7/weather/3d"; + private static final String URL_HOURLY = "https://devapi.heweather.net/v7/weather/24h"; + + private static final String WEATHER_ALL = "weather_all"; + + @Autowired + private ParamService paramService; + + /** + * 查询天气信息 + */ + @Cacheable(cacheNames = WEATHER_ALL, key = "'location_' + #location", unless = "#result == null") + public HeWeatherDTO findWeatherAll(String location) { + Map maps = initParam(location); + if (maps == null) { + log.warn("未配置天气信息"); + return null; + } + HttpResponse city; + HttpResponse now; + HttpResponse daily; + HttpResponse hourly; + try { + city = HttpRequest.get(URL_CITY).form(maps).execute(); + now = HttpRequest.get(URL_NOW).form(maps).execute(); + daily = HttpRequest.get(URL_DAILY).form(maps).execute(); + hourly = HttpRequest.get(URL_HOURLY).form(maps).execute(); + } catch (Exception e) { + throw new XzException400("获取天气信息失败:" + e.getMessage()); + } + + JSONObject locationJson = JSONUtil.parseObj(city.body()); + JSONObject nowJson = JSONUtil.parseObj(now.body()); + JSONObject dailyJson = JSONUtil.parseObj(daily.body()); + JSONObject hourlyJson = JSONUtil.parseObj(hourly.body()); + + HeWeatherDTO weather = new HeWeatherDTO(); + + if ("200".equals(locationJson.getStr("code"))) { + List los = JSONUtil.toList(locationJson.getJSONArray("location"), HeWeatherDTO.Location.class); + if (CollUtil.isNotEmpty(los)) { + weather.setLocation(los.get(0)); + } + } else { + log.error("获取城市信息失败,body : [{}]", locationJson.toString()); + } + + // 1. 当前时间 + if ("200".equals(nowJson.getStr("code"))) { + HeWeatherDTO.Now heNow = JSONUtil.toBean(nowJson.getJSONObject("now"), HeWeatherDTO.Now.class); + heNow.setIconValue(HeCondCode.getIcon(heNow.getIcon())); + weather.setNow(heNow); + } else { + log.error("获取当天气失败,body : [{}]", nowJson.toString()); + } + + // 2. 天气预报 + if ("200".equals(dailyJson.getStr("code"))) { + List daily3D = JSONUtil.toList(dailyJson.getJSONArray("daily"), HeWeatherDTO.Daily.class); + for (int i = 0; i < 3; i++) { + daily3D.get(i).setIconValueDay(HeCondCode.getIcon(daily3D.get(i).getIconDay())); + daily3D.get(i).setIconValueNight(HeCondCode.getIcon(daily3D.get(i).getIconNight())); + } + weather.setDaily(daily3D); + } else { + log.error("获取3日天气预报失败,body : [{}]", dailyJson.toString()); + } + + // 3. 小时预报 + if ("200".equals(hourlyJson.getStr("code"))) { + List originalHourly = JSONUtil.toList(hourlyJson.getJSONArray("hourly"), HeWeatherDTO.Hourly.class); + List newHourly = new ArrayList<>(); + newHourly.add(originalHourly.get(0)); + for (int i = 0; i < 1; i++) { + newHourly.get(i).setIconValue(HeCondCode.getIcon(newHourly.get(i).getIcon())); + } + weather.setHourly(newHourly); + } else { + log.error("获取小时预报失败,body : [{}]}", hourlyJson.toString()); + } + + return weather; + } + + /** + * 清除缓存 + */ + @CacheEvict(cacheNames = WEATHER_ALL, key = "'location_' + #location") + public void clearAll(String location) { + + } + + /** + * 获取参数 + * + * @return 返回查询参数 + */ + public Map initParam(String location) { + Map paramMap = paramService.selectMap(false, ParamEnum.HEFENG_KEY); + if (MapUtil.isNotEmpty(paramMap)) { + Map map = new HashMap<>(2); + map.put("location", location); + map.put("key", paramMap.get(ParamEnum.HEFENG_KEY.name())); + return map; + } + return null; + } +} diff --git a/blossom-backend/backend/src/main/resources/application.yml b/blossom-backend/backend/src/main/resources/application.yml new file mode 100644 index 0000000..85e4922 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/application.yml @@ -0,0 +1,65 @@ +server: + port: 9999 + +spring: + main: + allow-circular-references: true + application: + name: blossom-backend + profiles: + active: dev + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mvc: + pathmatch: + matching-strategy: ant_path_matcher + format: + # mvc 接收参数时对日期进行格式化 + date-time: yyyy-MM-dd HH:mm:ss + date: yyyy-MM-dd + throw-exception-if-no-handler-found: true + web: + resources: + add-mappings: false + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ mybatis ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +mybatis: + mapper-locations: classpath:mapper/*.xml + configuration: + # 驼峰命名转换 + map-underscore-to-camel-case: true + +mybatis-plus: + global-config: + banner: false + db-config: + # 主键类型 + # AUTO : 数据库ID自增, 该类型请确保数据库设置了 ID自增 否则无效 + # INPUT : 用户输入ID, 该类型可以通过自己注册自动填充插件进行填充 + # ASSIGN_ID : 分配ID (主键类型为 number 或 string), 默认实现雪花算法, 只有当插入对象ID 为空,才自动填充 + # ASSIGN_UUID : 全局唯一ID UUID + id-type: input + # 字段策略 + # IGNORED : 忽略判断 + # NOT_NULL : 非 NULL 判断 + # NOT_EMPTY : 非空判断 (只对字符串类型字段,其他类型字段依然为非 NULL判断) + insert-strategy: not_empty + update-strategy: not_null + where-strategy: not_null + logic-delete-field: 'delTime' + logic-not-delete-value: 0 + +# pageHelper 分页配置 +pagehelper: + # 默认为 false,为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果 + page-size-zero: true + # 分页合理化参数,默认值为 false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页 + reasonable: true + support-methods-arguments: true + params: count=countsql \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/config/application-dev.yml b/blossom-backend/backend/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..a6a813f --- /dev/null +++ b/blossom-backend/backend/src/main/resources/config/application-dev.yml @@ -0,0 +1,76 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://192.168.31.99:3306/xzzz-blossom?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8 + username: root + password: jasmine888 + hikari: + max-lifetime: 120000 + redis: + # 单机模式 + host: 192.168.31.99 + port: 6379 + password: + database: 0 + +logging: + level: + com.blossom: info + com.blossom.expand.tracker: info + com.blossom.admin.base.test: trace + org.springframework.boot.web.embedded.tomcat.TomcatWebServer: warn + com.baomidou.dynamic.datasource.DynamicRoutingDataSource: warn + com.zaxxer.hikari.HikariDataSource: warn + org.apache.coyote.http11.Http11NioProtocol: warn + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +project: + base: # 基础信息 + version: @project.version@ + ex: # 异常打印格式 + stack-trace: project + format: format + log: # 日志配置 + duration: 600000 # 动态日志级别 6 分钟后失效 + restore-duration: 30000 # 30 秒判断一次是否失效 + db: + slow-interval: 10 # 慢SQL + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Auth ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + auth: # 授权 + enabled: true # 开启授权 + log-type: simple # 日志类型 + # 授权 token 的方式, 注意, 如果使用 redis, 需要引入 redis 包, 否则会提示错误 + type: jwt + default-password: 123456 # 默认密码 + password-encoder: bcrypt # 加密方式 + clients: # 客户端 + - client-id: blossom + grant-type: 'password' + expire: 21600 # 客户端授权时间 6 小时 + multi-place-login: true # 客户端是否允许多地登录 + white-list: # 白名单 + - /sentinel/** + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Cache/Redis ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + cache: # 缓存 + names-config: # 缓存键超时时间配置 + - name: test_cache1 + seconds: 10 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Tracker ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + tracker: + collector: + enabled: false # 关闭收集器 + max-cache: 5000 + rate: 100 + repository: + type: disk1 # 关闭存储 + disk: + use-pid: true + log-dir: /logs/tracker + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ IAAS ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + iaas: + os-type: blossom + blos: + # 请以 / 结尾 + domain: "http://localhost:9999/pic/" + # 请以 / 结尾 + default-path: "/home/bl/img/" \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/config/application-prod.yml b/blossom-backend/backend/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..0db74d4 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/config/application-prod.yml @@ -0,0 +1,76 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/xzzz-blossom?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8 + username: root + password: jasmine888 + hikari: + max-lifetime: 120000 + +logging: + level: + com.blossom: info + com.blossom.expand.tracker: info + com.blossom.admin.base.test: trace + org.springframework.boot.web.embedded.tomcat.TomcatWebServer: warn + com.baomidou.dynamic.datasource.DynamicRoutingDataSource: warn + com.zaxxer.hikari.HikariDataSource: warn + org.apache.coyote.http11.Http11NioProtocol: warn + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +project: + # 基础信息 + base: + version: @project.version@ + # 异常日志配置 + ex: + stack-trace: project + format: format + # 动态日志级别配置 + log: + duration: 600000 # 动态日志级别 6 分钟后失效 + restore-duration: 30000 # 30 秒判断一次是否失效 + db: + slow-interval: 200 # 慢SQL + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Auth ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + auth: # 授权 + enabled: true # 开启授权 + # 请求日志: com.blossom.backend.base.auth.enums.LogTypeEnum + log-type: simple + # 授权 token 的方式, 注意, 如果使用 redis, 需要引入 redis 包, 否则会提示错误 + # com.blossom.backend.base.auth.enums.AuthTypeEnum + type: jwt + default-password: 123456 # 默认密码 + password-encoder: bcrypt # 加密方式 + clients: # 客户端 + - client-id: blossom + grant-type: 'password' + expire: 21600 # 客户端授权时间 6 小时 + multi-place-login: true # 客户端是否允许多地登录 + white-list: # 白名单 + - /sentinel/** + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Cache/Redis ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + cache: # 缓存 + names-config: # 缓存键超时时间配置 + - name: test_cache1 + seconds: 10 + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Tracker ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # 一个简易的链路追踪功能, 会在日志中输出 traceId + tracker: + collector: + enabled: false # 关闭收集器 + max-cache: 5000 + rate: 100 + repository: + type: disk1 # 关闭存储 + disk: + use-pid: true + log-dir: /logs/tracker + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ IAAS ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + iaas: + os-type: blossom + blos: + # 请以 /pic 结尾, 如果你在 nginx 中配置有代理, 注意别忘了添加你的代理路径 + domain: "https://www.wangyunf.com/bl/pic/" + # 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱 + default-path: "/home/bl/img/" diff --git a/blossom-backend/backend/src/main/resources/logback-spring.xml b/blossom-backend/backend/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f7d615f --- /dev/null +++ b/blossom-backend/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + ${LOG_PATTERN_CONSOLE} + + + + + + ${log.path}/info.log + + + ${log.path}/%d{yyyy-MM, aux}/info.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 15 + + + + ${LOG_PATTERN} + + + + + + ${log.path}/error.log + + ${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 15 + + + ${LOG_PATTERN} + + + + ERROR + + + + + ${log.path}/slowsql.log + + ${log.path}/%d{yyyy-MM}/slowsql.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 15 + + + ${LOG_PATTERN} + + + + ERROR + + + + + + + + + + + + + + + + + diff --git a/blossom-backend/backend/src/main/resources/mapper/ArticleMapper.xml b/blossom-backend/backend/src/main/resources/mapper/ArticleMapper.xml new file mode 100644 index 0000000..b2d02ee --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/ArticleMapper.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + update blossom_article + + pid = #{pid}, + `name` = #{name}, + icon = #{icon}, + tags = #{tags}, + sort = #{sort}, + cover = #{cover}, + color = #{color}, + open_status = #{openStatus}, + star_status = #{starStatus}, + describes = #{describes}, + open_version = #{openVersion}, + upd_time = now() + + where id = #{id} + + + + + update blossom_article + set markdown = #{markdown}, + html = #{html}, + words = #{words}, + toc = #{toc}, + version = version + 1 + where id = #{id} + and user_id = #{userId} + + + + + + + + update blossom_article + set open_version = version + where id = #{articleId} and open_status = 1 + + + + update blossom_article + set pv = pv + #{pv} + + ,uv = uv + #{uv} + + where id = #{articleId} + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/ArticleOpenMapper.xml b/blossom-backend/backend/src/main/resources/mapper/ArticleOpenMapper.xml new file mode 100644 index 0000000..8f5a079 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/ArticleOpenMapper.xml @@ -0,0 +1,30 @@ + + + + + + + insert + into blossom_article_open + select id,pid,words,version,now(),now(),toc,markdown,html,user_id + from blossom_article + where id = #{id}; + + + + + delete from blossom_article_open where id = #{id} + + + + + update blossom_article_open o + inner join blossom_article a on o.id = a.id and a.id = #{id} + set o.words = a.words, + o.open_version = a.version, + o.toc = a.toc, + o.markdown = a.markdown, + o.html = a.html, + o.sync_time = now() + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/ArticleReferenceMapper.xml b/blossom-backend/backend/src/main/resources/mapper/ArticleReferenceMapper.xml new file mode 100644 index 0000000..100b5c1 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/ArticleReferenceMapper.xml @@ -0,0 +1,28 @@ + + + + + + + insert into blossom_article_reference ( + source_id, + source_name, + target_Id, + target_name, + target_url, + type, + user_id + ) values + + ( + #{ref.sourceId}, + #{ref.sourceName}, + #{ref.targetId}, + #{ref.targetName}, + #{ref.targetUrl}, + #{ref.type}, + #{ref.userId} + ) + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/ArticleViewMapper.xml b/blossom-backend/backend/src/main/resources/mapper/ArticleViewMapper.xml new file mode 100644 index 0000000..d699214 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/ArticleViewMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/FolderMapper.xml b/blossom-backend/backend/src/main/resources/mapper/FolderMapper.xml new file mode 100644 index 0000000..af00eb0 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/FolderMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + update blossom_folder + + pid = #{pid}, + `name` = #{name}, + icon = #{icon}, + tags = #{tags}, + sort = #{sort}, + cover = #{cover}, + color = #{color}, + open_status = #{openStatus}, + store_path = #{storePath}, + subject_words = #{subjectWords}, + subject_upd_time = #{subjectUpdTime}, + describes = #{describes}, + upd_time = now() + + where id = #{id} + + + + update blossom_folder + + store_path = #{storePath}, + upd_time = now() + + where id in #{item} + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/NoteMapper.xml b/blossom-backend/backend/src/main/resources/mapper/NoteMapper.xml new file mode 100644 index 0000000..1d1e673 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/NoteMapper.xml @@ -0,0 +1,19 @@ + + + + + + + update blossom_note + + set top = 0, + top_time = null + + + set top = 1, + top_time = now() + + where id = #{id} + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/PictureMapper.xml b/blossom-backend/backend/src/main/resources/mapper/PictureMapper.xml new file mode 100644 index 0000000..e42309f --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/PictureMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + update blossom_picture + + star_status = #{starStatus} + + where id = #{id} + + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/PlanMapper.xml b/blossom-backend/backend/src/main/resources/mapper/PlanMapper.xml new file mode 100644 index 0000000..ebe73fe --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/PlanMapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + insert into blossom_plan + values + + ( + null, + #{plan.groupId}, + #{plan.type}, + #{plan.title}, + #{plan.content}, + #{plan.planMonth}, + #{plan.planDate}, + #{plan.planStartTime}, + #{plan.planEndTime}, + #{plan.color}, + #{plan.position}, + #{plan.img}, + now(), + #{plan.userId} + ) + + + + + + update blossom_plan + + group_id = #{groupId}, + type = #{type}, + title = #{title}, + content = #{content}, + plan_month = #{planMonth}, + plan_date = #{planDate}, + plan_time = #{planTime}, + color = #{color}, + position = #{position}, + img = #{img}, + upd_time = now() + + where id = #{id} + + + + delete from blossom_plan + where + + id = #{id} + + + group_id = #{groupId} + + + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/mapper/UserMapper.xml b/blossom-backend/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..f62e14c --- /dev/null +++ b/blossom-backend/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,22 @@ + + + + + + + update base_user + + username = #{username}, + nick_name = #{nickName}, + avatar = #{avatar}, + remark = #{remark}, + location = #{location}, + upd_time = now() + + where id = #{id} + + + + update base_user set password = #{password},upd_time = now() where id = #{userId} + + \ No newline at end of file diff --git a/blossom-backend/backend/src/main/resources/sentinel.properties b/blossom-backend/backend/src/main/resources/sentinel.properties new file mode 100644 index 0000000..7662616 --- /dev/null +++ b/blossom-backend/backend/src/main/resources/sentinel.properties @@ -0,0 +1,7 @@ +project.name=blossom-backend + + +csp.sentinel.log.dir=logs/csp + +csp.sentinel.metric.file.single.size=20971520 +csp.sentinel.metric.file.total.count=30 diff --git a/blossom-backend/common/common-base/pom.xml b/blossom-backend/common/common-base/pom.xml new file mode 100644 index 0000000..50968c5 --- /dev/null +++ b/blossom-backend/common/common-base/pom.xml @@ -0,0 +1,96 @@ + + + + common + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + jar + + common-base + + + 5.3.2 + 5.8.18 + 4.9.3 + 2.9.3 + 3.2.1 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + org.springframework.boot + spring-boot-autoconfigure + + + + + org.springframework.boot + spring-boot-starter-json + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.hibernate.validator + hibernate-validator + + + + org.springframework.boot + spring-boot-starter-validation + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + + + com.github.ben-manes.caffeine + caffeine + + + + + + \ No newline at end of file diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConfig.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConfig.java new file mode 100644 index 0000000..2cc20c5 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConfig.java @@ -0,0 +1,13 @@ +package com.blossom.common.base; + +import org.springframework.context.annotation.Configuration; + +/** + * core 模块公共配置 + * + * @author xzzz + */ +@Configuration +public class BaseConfig { + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConstants.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConstants.java new file mode 100644 index 0000000..e986dd0 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseConstants.java @@ -0,0 +1,48 @@ +package com.blossom.common.base; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.enums.FrameworkEnum; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; + + +/** + * 基础静态参数 + * + * @author xzzz + */ +@Slf4j +public final class BaseConstants { + + /** + * 项目名称 + */ + public static final String PROJECT_NAME = "blossom-backend"; + + /** + * 项目说明 + */ + public static void desc() { + log.info("启动完成" + + "\n==========================================================" + + "\n项目名称: [{}]" + + "\n当前环境: [{}], 版本: [{}]" + + "\n访问地址: http://127.0.0.1:{}{}" + + "\n" + + "\n使用框架: {}" + + "\n==========================================================", + SpringUtil.getAppName(), + SpringUtil.getProfileAction(), SpringUtil.get("project.base.version"), + SpringUtil.getPort(), getContextPath(), + FrameworkEnum.getExistFrameworkStr() + ); + } + + private static String getContextPath() { + String contextPath = SpringUtil.getServletContextPath(); + if (StrUtil.isNotBlank(contextPath)) { + return contextPath + "/"; + } + return "/"; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseProperties.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseProperties.java new file mode 100644 index 0000000..0ea67c2 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/BaseProperties.java @@ -0,0 +1,70 @@ +package com.blossom.common.base; + +import com.blossom.common.base.enums.ExStackTrace; +import com.blossom.common.base.enums.ExFormat; +import com.blossom.common.base.exception.AbstractExceptionAdvice; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * base 基础功能配置 + * + * @author xzzz + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "project.base") +public class BaseProperties { + + /** + * 系统版本, 可以使用 @project.version@ 获取 pom 中版本 + */ + private String version = "xyz"; + + /** + * 异常处理信息配置 + */ + private Ex ex = new Ex(); + + /** + * 日志配置 + */ + private Log log = new Log(); + + @Data + public static class Ex { + /** + * {@link AbstractExceptionAdvice} 中记录异常堆栈信息的内容; + *

如果是 all ,则记录全部堆栈信息; + *

如果是 project, 堆栈信息只包含项目包下的类; + */ + private ExStackTrace stackTrace = ExStackTrace.project; + + /** + * {@link AbstractExceptionAdvice} 中是否将异常信息进行格式化打印; + *

如果是 line, 则异常堆栈信息按照一行记录在日志中; + *

如果是 project, 则异常堆栈信息按照格式化方式记录在日志中 + */ + private ExFormat format = ExFormat.format; + } + + @Data + public static class Log { + /** + * 日志级别的缓存时间, 单位毫秒, 超过该时间将会重置为 INFO, 用来防止日志过大。 + *

仅针对通过动态日志配置的日志路径, 如果日志级别已在配置文件中配置, 则会 + * 被覆盖并在超时后重置为 INFO, 但项目重启后会重置为 + */ + private Long duration = 10 * 60 * 1000L; + + /** + * 如果以 redis 存储日志级别, 则日志级别失效后回滚需要监听 key 过期, 可能有性能隐患 + *

如果以 caffeine 存储日志级别, 则日志级别失效后 caffeine 会惰性删除, 可能存在很久的延迟. + *

所以由于动态日志的回滚是每个应用定时去完成的 + *

每 restoreDuration 秒, 会重新刷新日志级别。如果日志级别设置的时间已超时, 则会重置为 INFO + */ + private Long restoreDuration = 30 * 1000L; + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/caffeine/Test.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/caffeine/Test.java new file mode 100644 index 0000000..c8bd64a --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/caffeine/Test.java @@ -0,0 +1,37 @@ +package com.blossom.common.base.caffeine; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class Test { + + private static final Cache cache = Caffeine.newBuilder() + .initialCapacity(100) + .expireAfterWrite(10, TimeUnit.SECONDS) + .removalListener((String key, String value, RemovalCause cause) -> System.out.println(key + " 被删除")) + .build();; + private static final ScheduledExecutorService clearUpScheduled = Executors.newScheduledThreadPool(1); + + public Test() { + clearUpScheduled.scheduleWithFixedDelay(this::clear, 5, 1, TimeUnit.SECONDS); + } + + private void clear() { + log.info("尝试过期缓存"); + cache.cleanUp(); + } + + public static void main(String[] args) { + Test test = new Test(); + cache.policy().expireVariably().ifPresent(e -> { + }); + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/JacksonConfig.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/JacksonConfig.java new file mode 100644 index 0000000..3b0a88d --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/JacksonConfig.java @@ -0,0 +1,33 @@ +package com.blossom.common.base.config; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.blossom.common.base.util.json.JsonUtil; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Jackson 自定义配置 + */ +@Configuration +public class JacksonConfig { + + /** + * 修改全局的Jackson序列化配置 + */ + @Bean("jackson2ObjectMapperBuilderCustomizer") + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder + .modules(JsonUtil.getJavaTimeModule()) + // 序列化枚举是以 toString() 来输出,默认 false 。用来处理 Mybatis-plus 通用枚举类的使用 + .featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .serializerByType(Long.class, ToStringSerializer.instance) + .serializerByType(Long.TYPE, ToStringSerializer.instance) + .serializerByType(BigInteger.class, ToStringSerializer.instance) + .serializerByType(BigDecimal.class, ToStringSerializer.instance); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/WebMvcConfig.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/WebMvcConfig.java new file mode 100644 index 0000000..8acdf78 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/config/WebMvcConfig.java @@ -0,0 +1,67 @@ +package com.blossom.common.base.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +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.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.concurrent.TimeUnit; + +/** + * MVC 配置 + * + * @author xzzz + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + /** + * 静态资源 + * + * @param registry + */ + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/apidoc/**") + .addResourceLocations("classpath:/static/apidoc/") + .setCacheControl(CacheControl.maxAge(4, TimeUnit.MINUTES).cachePublic()); + + registry.addResourceHandler("/favicon.ico") + .addResourceLocations("classpath:/static/img/favicon.ico") + .setCacheControl(CacheControl.maxAge(1, TimeUnit.SECONDS).cachePublic()) + ; + } + + /** + * 过滤器方式配置跨域 + * + * @return 跨域过滤器 + */ + @Bean + public FilterRegistrationBean filterFilterRegistrationBean() { + CorsConfiguration corsConfig = new CorsConfiguration(); + corsConfig.setAllowCredentials(true); + // 允许的请求来源 + corsConfig.addAllowedOriginPattern("*"); + // 允许的请求头 + corsConfig.addAllowedHeader("*"); + // 暴露头信息 + corsConfig.addExposedHeader(HttpHeaders.SET_COOKIE); + // 允许的请求方式 + corsConfig.addAllowedMethod("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfig); + + FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source)); + // 过滤器优先级 + bean.setOrder(Integer.MIN_VALUE); + return bean; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExFormat.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExFormat.java new file mode 100644 index 0000000..da673e9 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExFormat.java @@ -0,0 +1,21 @@ +package com.blossom.common.base.enums; + +import com.blossom.common.base.exception.AbstractExceptionAdvice; + +/** + * {@link AbstractExceptionAdvice} 中是否将异常信息进行格式化打印; + *

如果是 line, 则异常堆栈信息按照一行记录在日志中; + *

如果是 format, 则异常堆栈信息按照格式化方式记录在日志中 + * + * @author xzzz + */ +public enum ExFormat { + /** + * 则异常堆栈信息按照一行记录在日志中, 通常可以方便进行日志收集, 例如有结构化的数据, 可以避免使用某分隔符进行分割, 以提升效率 + */ + line, + /** + * 异常堆栈信息按照格式化方式记录在日志中 + */ + format +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExStackTrace.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExStackTrace.java new file mode 100644 index 0000000..3047091 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/ExStackTrace.java @@ -0,0 +1,24 @@ +package com.blossom.common.base.enums; + +import com.blossom.common.base.exception.AbstractExceptionAdvice; +import com.blossom.common.base.util.ExceptionUtil; + +/** + * {@link AbstractExceptionAdvice} 中记录异常堆栈信息的内容; + *

如果是 all ,则记录全部堆栈信息; + *

如果是 project, 堆栈信息只包含项目包下的类; + * + * @author xzzz + */ +public enum ExStackTrace { + + /** + * 打印全部堆栈异常 + */ + all, + + /** + * 只显式指定包下的堆栈信息, {@link ExceptionUtil#FILTER_PACKAGE} + */ + project +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/FrameworkEnum.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/FrameworkEnum.java new file mode 100644 index 0000000..90de4ff --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/FrameworkEnum.java @@ -0,0 +1,77 @@ +package com.blossom.common.base.enums; + + +import cn.hutool.core.util.ClassLoaderUtil; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author xzzz + */ +@Slf4j +@SuppressWarnings("all") +public enum FrameworkEnum { + /** + * 各类框架的索引类, 用来查询是否使用了此框架 + */ + SPRINGBOOT ("org.springframework.boot.SpringApplication"), + DUBBO ("org.apache.dubbo.rpc.protocol.dubbo.DubboProtocolServer"), + ROCKET_MQ ("org.apache.rocketmq.common.Configuration"), + APOLLO ("com.ctrip.framework.apollo.core.utils.ApolloThreadFactory"), + MYSQL ("com.mysql.cj.jdbc.ConnectionImpl"), + REDIS ("org.springframework.data.redis.connection.lettuce.DefaultLettucePoolingClientConfiguration"), + ELASTIC_SEARCH ("org.elasticsearch.client.RestClientBuilder"), + SENTRY ("io.sentry.protocol.SentryThread"), + XXL_JOB ("com.xxl.job.core.thread.ExecutorRegistryThread"), + SENTINEL ("com.alibaba.csp.sentinel.Env"), + ZOOKEEPER ("org.apache.zookeeper.ZooKeeper"), + + /** + * 自研模块 + */ + TRACE ("com.blossom.expand.tracker.core.Tracker"), + ; + + @Getter + private final String clazz; + + FrameworkEnum(String clazz) { + this.clazz = clazz; + } + + public static List getExistFramework() { + List frameworkEnums = new ArrayList<>(); + + for (FrameworkEnum framework : FrameworkEnum.values()) { + try { + if (ClassLoaderUtil.isPresent(framework.getClazz())) { + frameworkEnums.add(framework); + } + } catch (Exception e) { + log.warn("检查框架错误:{}", e.getMessage()); + } + } + return frameworkEnums; + } + + public static String getExistFrameworkStr() { + List frameworkEnums = getExistFramework(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < frameworkEnums.size(); i++) { + sb.append(frameworkEnums.get(i).name()); + if ((i + 1) < frameworkEnums.size()) { + sb.append(", "); + } + } + for (FrameworkEnum frameworkEnum : frameworkEnums) { + + } + return sb.toString(); + } + +} + diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/YesNo.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/YesNo.java new file mode 100644 index 0000000..cbf4bc7 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/enums/YesNo.java @@ -0,0 +1,45 @@ +package com.blossom.common.base.enums; + +import lombok.Getter; + +/** + * 是否枚举 + * + * @author xzzz + */ +@Getter +public enum YesNo { + + /** + * 是, TRUE + */ + YES(1, "是", true), + /** + * 否, FALSE + */ + NO(0, "否", false); + + Integer value; + String name; + Boolean aBoolean; + + YesNo(Integer value, String name, Boolean aBoolean) { + this.value = value; + this.name = name; + this.aBoolean = aBoolean; + } + + public static Integer toInt(Boolean aBoolean) { + if (aBoolean) { + return YES.value; + } + return NO.value; + } + + public static Boolean is(int value) { + if (value == 1) { + return YES.aBoolean; + } + return NO.aBoolean; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/AbstractExceptionAdvice.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/AbstractExceptionAdvice.java new file mode 100644 index 0000000..7d20e60 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/AbstractExceptionAdvice.java @@ -0,0 +1,70 @@ +package com.blossom.common.base.exception; + +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.enums.ExFormat; +import com.blossom.common.base.enums.ExStackTrace; +import com.blossom.common.base.util.ExceptionUtil; +import com.blossom.common.base.util.ServletUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +/** + * 所有的该抽象类的子类在实现异常捕获时,必须调用 printExLog 方法 + * + * @author xzzz + */ +@Slf4j +public abstract class AbstractExceptionAdvice { + + /** + * 基础功能配置 + */ + private final BaseProperties baseProperties; + + public AbstractExceptionAdvice(BaseProperties baseProperties) { + this.baseProperties = baseProperties; + } + + /** + * 对 sentinel 的增强, 在 request 中增加异常标记, 供 sentinel 拦截器捕获并记录 + */ + private static final String REQUEST_ATTRIBUTE_SENTINEL_EXCEPTION = "request_attribute_sentinel_exception"; + + /** + * 打印日志信息, 显示错误堆栈 + * + * @param ex 异常 + * @param msg 错误信息 + */ + protected void printExLog(Exception ex, String msg) { + printExLog(ex, msg, true); + } + + /** + * 打印错误信息 + * + * @param ex 异常类 + * @param msg 异常信息 + * @param showStackTrace 是否显示异常堆栈信息 + */ + protected void printExLog(Exception ex, String msg, boolean showStackTrace) { + HttpServletRequest request = ServletUtil.getRequest(); + String url = ""; + if (request != null) { + // 对 sentinel 的增强方法 + request.setAttribute(REQUEST_ATTRIBUTE_SENTINEL_EXCEPTION, ex); + url = request.getRequestURL().toString(); + } + + boolean filterStackTrace = baseProperties.getEx().getStackTrace().equals(ExStackTrace.project); + boolean onLine = baseProperties.getEx().getFormat().equals(ExFormat.line); + + log.error("{} | {}({})", url, ex.getClass().getSimpleName(), Optional.ofNullable(msg).orElse("")); + if (showStackTrace) { + log.error(ExceptionUtil.printStackTrace(ex, filterStackTrace, onLine)); + } + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByGlobal.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByGlobal.java new file mode 100644 index 0000000..b919644 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByGlobal.java @@ -0,0 +1,277 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.collection.CollectionUtil; +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.pojo.RCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException; +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.core.annotation.Order; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.List; + +/** + * 全局异常处理器 + *

+ * 统一以 R 对象格式返回给客服端 + * + * @authoR xzzz + * @since 0.0.1 + */ +@Slf4j +@Order(1) +@RestControllerAdvice +public class ExceptionAdviceByGlobal extends AbstractExceptionAdvice { + + public ExceptionAdviceByGlobal(BaseProperties baseProperties) { + super(baseProperties); + } + + /** + * 业务参数异常 + */ + @ExceptionHandler(Exception.class) + public R handleException(Exception exception) { + exception.printStackTrace(); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), exception.getMessage()); + } + + /** + * 参数处理错误 + */ + @ExceptionHandler(IllegalArgumentException.class) + public R illegalArgumentExceptionHandler(Exception exception) { + exception.printStackTrace(); + return R.fault(RCode.BAD_REQUEST.getCode(), exception.getMessage()); + } + + /** + * 数据资源访问失败异常 + */ + @ExceptionHandler(ConnectException.class) + public R handleDataAccessResourceFaultException(ConnectException exception) { + exception.printStackTrace(); + return R.fault("99999", "连接失败", exception.getMessage()); + } + + /** + * ServletRequest 绑定异常。 属于 HTTP 中的 400 错误 + *

+ * 示例:返回的 pojo 在序列化成 json 过程失败了,那么抛该异常; + */ + @ExceptionHandler(ServletRequestBindingException.class) + public R handleServletRequestBindingException(ServletRequestBindingException exception) { + String msg = "缺少请求参数"; + printExLog(exception, msg); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + /** + * 缺少请求参数 file + */ + @ExceptionHandler(MissingServletRequestPartException.class) + public R illegalArgumentExceptionHandler(MissingServletRequestPartException exception) { + String msg = String.format("缺少请求参数 [%s]", exception.getRequestPartName()); + printExLog(exception, msg); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + /** + * 缺少 ServletRequest 请求参数。 属于 HTTP 中的 400 错误 + *

+ * 1、@RequestParam("licenceId") String licenceId,但发起请求时,未携带该参数,则会抛该异常; + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public R handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) { + String msg = String.format("缺少请求参数 [%s]", exception.getParameterName()); + printExLog(exception, msg); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + /** + * 未检测到路径参数异常。 属于 HTTP 中的 500 错误 + *

+ * 1、url为:/licence/{licenceId},参数签名包含 @PathVariable("licenceId"),当请求的 url 为 /licence 时。 + */ + @ExceptionHandler(MissingPathVariableException.class) + public R handleMissingPathVariableException(MissingPathVariableException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.BAD_REQUEST.getCode(), "缺少请求参数" + exception.getMessage(), exception.getMessage()); + } + + /** + * 参数解析异常。 属于 HTTP 中的 400 错误 + *

1、JSON parse error: 使用 @RequestBody 接收参数时,参数是 json 字符串,数据是 Long 类型的,json 字符串中却传入了 String 类型。 + *

2、Required request body is missing: 使用 @RequestBody 接收参数时,不传递参数。 + *

3、Required request body is missing: 使用 get 请求去请求使用 @RequestBody 接收参数的接口时。 + *

4、JSON parse error: Numeric value (9999999999999999999) out of range of int (-2147483648 - 2147483647); + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public R handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { + String msg = "请检查 RequestBody 格式是否正确。"; + printExLog(exception, msg); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + /** + * Spring 参数绑定异常 + *

1、org.springframework.validation.BindException: 参数上添加了 @Validated 注解,不传递参数。 + *

2、org.springframework.validation.BindException: 参数上添加了 @Validated 注解,不使用 @RequestBody 接收参数且参数值超出了校验范围。 + */ + @ExceptionHandler(BindException.class) + public R handleBindException(BindException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.BAD_REQUEST.getCode(), "参数绑定异常:" + exception.getMessage(), exception.getMessage()); + } + + /** + * 方法参数校验异常。 属于 HTTP 中的 400 错误 + *

+ * 使用 @Validated 注解的参数校验 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public R handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + List fieldErrorList = exception.getFieldErrors(); + StringBuilder buffer = new StringBuilder(); + if (CollectionUtil.isNotEmpty(fieldErrorList)) { + for (FieldError fieldError : fieldErrorList) { + fieldError.getField(); + buffer.append(fieldError.getDefaultMessage()).append(";"); + } + } + printExLog(exception, buffer.toString()); + return R.fault(RCode.BAD_REQUEST.getCode(), buffer.toString(), exception.getMessage()); + } + + + /** + * 405 错误 + *

+ * 1、请求对应的 ControlleR 方法支持 Post 请求,此时,用 GET 请求来访问。 + */ + @ExceptionHandler({HttpRequestMethodNotSupportedException.class}) + public R request405(HttpRequestMethodNotSupportedException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.BAD_REQUEST.getCode(), String.format("当前访问路径不支持 %s 请求", exception.getMethod()), exception.getMessage()); + } + + /** + * 空指针异常 + */ + @ExceptionHandler(NullPointerException.class) + public R nullPointerExceptionHandler(NullPointerException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "空引用异常", exception.getMessage()); + } + + + /** + * 类型转换异常 + */ + @ExceptionHandler(ClassCastException.class) + public R classCastExceptionHandler(ClassCastException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "类型转换错误", exception.getMessage()); + } + + /** + * IO 异常 + */ + @ExceptionHandler(IOException.class) + public R ioExceptionHandler(IOException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "IO 异常", exception.getMessage()); + } + + /** + * 方法不存在异常 + */ + @ExceptionHandler(NoSuchMethodException.class) + public R noSuchMethodExceptionHandler(NoSuchMethodException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "方法不存在", exception.getMessage()); + } + + /** + * 下标越界异常 + */ + @ExceptionHandler(IndexOutOfBoundsException.class) + public R indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "下标越界", exception.getMessage()); + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * 503 错误 + */ + @ExceptionHandler({AsyncRequestTimeoutException.class}) + public R request503(AsyncRequestTimeoutException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), "异步请求超时", exception.getMessage()); + } + + /** + * 500 错误 + *

+ * ConversionNotSupportedException:转换不支持 + * HttpMessageNotWritableException:返回的 pojo 在序列化成 json 过程失败了,那么抛该异常; + * MissingPathVariableException:未检测到路径参数:url为:/licence/{licenceId},参数签名包含 @PathVariable("licenceId") + * ,当请求的url为/licence,在没有明确定义url为/licence的情况下,会被判定为:缺少路径参数; + */ + @ExceptionHandler({ConversionNotSupportedException.class, HttpMessageNotWritableException.class}) + public R server500(RuntimeException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.INTERNAL_SERVER_ERROR.getCode(), RCode.INTERNAL_SERVER_ERROR.getMsg(), exception.getMessage()); + } + + /** + * 404 错误 + *

+ * NoHandlerFoundException:请求没找到 首先根据请求Url查找有没有对应的控制器,若没有则会抛该异常 + */ + @ExceptionHandler({NoHandlerFoundException.class}) + public R request404(NoHandlerFoundException exception) { + printExLog(exception, exception.getMessage()); + return R.fault(RCode.NOT_FOUND.getCode(), RCode.NOT_FOUND.getMsg(), exception.getMessage()); + } + + /** + * 上传文件大小的错误 + * @param exception 异常 + */ + @ExceptionHandler(value = MaxUploadSizeExceededException.class) + public R missingServletRequestParameterExceptionHandler(MaxUploadSizeExceededException exception) { + printExLog(exception, exception.getMessage()); + if (exception.getCause() != null + && exception.getCause() instanceof IllegalStateException + && exception.getCause().getCause() != null + && exception.getCause().getCause() instanceof SizeLimitExceededException) { + SizeLimitExceededException sizeEx = (SizeLimitExceededException) exception.getCause().getCause(); + return R.fault(RCode.BAD_REQUEST.getCode(), + String.format("上传文件过大, 文件大小不能超过 %s MB ", (sizeEx.getPermittedSize()) / 1024 / 1024)); + } + return R.fault(RCode.BAD_REQUEST.getCode(), String.format("文件上传错误, %s", exception.getMessage())); + } + + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByXz.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByXz.java new file mode 100644 index 0000000..05ad499 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/ExceptionAdviceByXz.java @@ -0,0 +1,63 @@ +package com.blossom.common.base.exception; + +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.pojo.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 系统自定义异常处理 + * + * @author xzzz + */ +@Slf4j +@Order(0) +@RestControllerAdvice +public class ExceptionAdviceByXz extends AbstractExceptionAdvice { + + public ExceptionAdviceByXz(BaseProperties baseProperties) { + super(baseProperties); + } + + @ExceptionHandler(XzException.class) + public R xzExceptionHandler(XzException exception) { + printExLog(exception, exception.getMessage(), true); + return R.fault(exception.getCode(), exception.getMessage()); + } + + /** + * 返回 400 响应码 + * + * @param exception 异常信息 + * @return 返回 R.fault(), Http 响应码为 400 + */ + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + @ExceptionHandler(XzException400HTTP.class) + public R xzException400HttpCodeHandler(XzException400HTTP exception) { + printExLog(exception, exception.getMessage()); + return R.fault(exception.getCode(), exception.getMessage()); + } + + + @ExceptionHandler(XzException400.class) + public R xzException400Handler(XzException400 exception) { + printExLog(exception, exception.getMessage(), true); + return R.fault(exception.getCode(), exception.getMessage()); + } + + @ExceptionHandler(XzException404.class) + public R xzException404Handler(XzException404 exception) { + printExLog(exception, exception.getMessage(), true); + return R.fault(exception.getCode(), exception.getMessage()); + } + + @ExceptionHandler(XzException500.class) + public R xzException500Handler(XzException500 exception) { + printExLog(exception, exception.getMessage()); + return R.fault(exception.getCode(), exception.getMessage()); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzAbstractException.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzAbstractException.java new file mode 100644 index 0000000..3837bc1 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzAbstractException.java @@ -0,0 +1,40 @@ +package com.blossom.common.base.exception; + +import com.blossom.common.base.pojo.IRCode; + +/** + * 异常抽象类 + * + * @author xzzz + */ +public abstract class XzAbstractException extends RuntimeException { + + /** + * 响应码 + */ + private final String code; + + /** + * 自定义响应码和响应信息 + * + * @param code 响应码 + * @param msg 响应信息 + */ + public XzAbstractException(String code, String msg) { + super(msg); + this.code = code; + } + + /** + * 直接返回响应码枚举 + */ + public XzAbstractException(IRCode irCode) { + super(irCode.getMsg()); + this.code = irCode.getCode(); + } + + public String getCode() { + return code; + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException.java new file mode 100644 index 0000000..d0b670f --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException.java @@ -0,0 +1,29 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.pojo.IRCode; + +/** + * 异常, 通常不使用该异常 + * + * @author xzzz + */ +public class XzException extends XzAbstractException { + + public XzException(IRCode code) { + super(code.getCode(), code.getMsg()); + } + + public XzException(String code, String msg) { + super(code, msg); + } + + public static void throwBy(boolean expression, String code, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = ""; + } + throw new XzException(code, msg); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400.java new file mode 100644 index 0000000..30e63f7 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400.java @@ -0,0 +1,27 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.pojo.RCode; + +/** + * @author xzzz + */ +public class XzException400 extends XzAbstractException { + + public XzException400(String message) { + super(RCode.BAD_REQUEST.getCode(), message); + } + + public XzException400(String message, String... args) { + super(RCode.BAD_REQUEST.getCode(), String.format(message, args)); + } + + public static void throwBy(boolean expression, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = RCode.BAD_REQUEST.getMsg(); + } + throw new XzException400(msg); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400HTTP.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400HTTP.java new file mode 100644 index 0000000..7bfa04b --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException400HTTP.java @@ -0,0 +1,25 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.pojo.RCode; + +/** + * 抛出异常, 如果是 Rest 接口, 则 Http 响应码为 404 + * + * @author xzzz + */ +public class XzException400HTTP extends XzAbstractException { + + public XzException400HTTP(String message) { + super(RCode.BAD_REQUEST.getCode(), message); + } + + public static void throwBy(boolean expression, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = RCode.BAD_REQUEST.getMsg(); + } + throw new XzException400HTTP(msg); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException404.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException404.java new file mode 100644 index 0000000..6e2a497 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException404.java @@ -0,0 +1,23 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.pojo.RCode; + +/** + * @author xzzz + */ +public class XzException404 extends XzAbstractException { + + public XzException404(String message) { + super(RCode.NOT_FOUND.getCode(), message); + } + + public static void throwBy(boolean expression, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = RCode.NOT_FOUND.getMsg(); + } + throw new XzException404(msg); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException500.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException500.java new file mode 100644 index 0000000..f052b52 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/exception/XzException500.java @@ -0,0 +1,23 @@ +package com.blossom.common.base.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.pojo.RCode; + +/** + * @author xzzz + */ +public class XzException500 extends XzAbstractException { + + public XzException500(String message) { + super(RCode.INTERNAL_SERVER_ERROR.getCode(), message); + } + + public static void throwBy(boolean expression, String msg) { + if (expression) { + if (StrUtil.isBlank(msg)) { + msg = RCode.INTERNAL_SERVER_ERROR.getMsg(); + } + throw new XzException404(msg); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/CustomLogbackColor.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/CustomLogbackColor.java new file mode 100644 index 0000000..1da8fec --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/CustomLogbackColor.java @@ -0,0 +1,34 @@ +package com.blossom.common.base.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.color.ANSIConstants; +import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; + +/** + * 日志级别颜色 + * + * @author xzzz + */ +public class CustomLogbackColor extends ForegroundCompositeConverterBase { + + @Override + protected String getForegroundColorCode(ILoggingEvent event) { + Level level = event.getLevel(); + switch (level.toInt()) { + case Level.ERROR_INT: + return ANSIConstants.RED_FG; + case Level.WARN_INT: + return ANSIConstants.YELLOW_FG; + case Level.INFO_INT: + return ANSIConstants.WHITE_FG; + case Level.DEBUG_INT: + return ANSIConstants.GREEN_FG; + //DEBUG等级为绿色 + case Level.TRACE_INT: + return ANSIConstants.GREEN_FG; + default: + return ANSIConstants.DEFAULT_FG; + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepository.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepository.java new file mode 100644 index 0000000..4ccb241 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepository.java @@ -0,0 +1,45 @@ +package com.blossom.common.base.log; + +import lombok.Data; + +/** + * 动态日志级别的存储方式 + * + * @author xzzz + */ +public interface DynamicLogRepository { + + /** + * 保存日志级别 + * + * @param levelWrapper 日志封装 + */ + void save(LevelWrapper levelWrapper); + + + @Data + class LevelWrapper { + /** + * 日志路径 + */ + private String path; + /** + * 过期时间 + */ + private Long expire; + /** + * 该路径所要执行的日志级别 + */ + private String level; + + public LevelWrapper() { + } + + public LevelWrapper(String path, String level, Long expire) { + this.path = path; + this.expire = expire; + this.level = level; + } + + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepositoryCaffeine.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepositoryCaffeine.java new file mode 100644 index 0000000..98aaeba --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/DynamicLogRepositoryCaffeine.java @@ -0,0 +1,63 @@ +package com.blossom.common.base.log; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.blossom.common.base.BaseProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.stereotype.Component; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 动态日志级别配置, 使用 caffeine 作为缓存实现。无法在集群中同步日志级别, 只能单机使用。 + * + * @author xzzz + */ +@Slf4j +@Component("dynamicLogRepositoryCaffeine") +@ConditionalOnMissingClass("org.springframework.data.redis.core.RedisTemplate") +public class DynamicLogRepositoryCaffeine implements DynamicLogRepository { + + private final LoggingSystem loggingSystem; + private final Cache cache; + private final ScheduledExecutorService clearUpScheduled = Executors.newScheduledThreadPool(1); + + public DynamicLogRepositoryCaffeine(LoggingSystem loggingSystem, BaseProperties properties) { + log.info("[ BASE] 日志级别存储 : Caffeine, 配置持续时长[{}ms], 刷新间隔[{}ms]", + properties.getLog().getDuration(), properties.getLog().getRestoreDuration()); + cache = Caffeine.newBuilder() + .initialCapacity(100) + .expireAfterWrite(properties.getLog().getDuration(), TimeUnit.SECONDS) + .removalListener((String key, String value, RemovalCause cause) -> this.restore(key)) + .build(); + clearUpScheduled.scheduleWithFixedDelay(this::clear, 10, properties.getLog().getRestoreDuration(), TimeUnit.SECONDS); + this.loggingSystem = loggingSystem; + } + + private void restore(String path) { + loggingSystem.setLogLevel(path, LogLevel.INFO); + } + + private void clear() { + cache.cleanUp(); + } + + /** + * @param appName 应用名称, 非必填 + * @param path 日志路径 + * @param level 级别 + */ + @Override + public void save(LevelWrapper levelWrapper) { + String path = levelWrapper.getPath(); + LogLevel level = LogLevel.valueOf(levelWrapper.getLevel().toUpperCase()); + loggingSystem.setLogLevel(path, level); + cache.put(path, level.name()); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/HostNameConfig.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/HostNameConfig.java new file mode 100644 index 0000000..02fd9bb --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/HostNameConfig.java @@ -0,0 +1,23 @@ +package com.blossom.common.base.log; + +import ch.qos.logback.core.PropertyDefinerBase; +import com.blossom.common.base.util.SystemUtil; + +/** + * 使用在 logback-spring.xml 中, 作为日志收集时的机器名 + * + * @author xzzz + */ +public class HostNameConfig extends PropertyDefinerBase { + + private static final String HOST_NAME; + + static { + HOST_NAME = SystemUtil.getHostName(); + } + + @Override + public String getPropertyValue() { + return HOST_NAME; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogDownloadUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogDownloadUtil.java new file mode 100644 index 0000000..a4d3766 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogDownloadUtil.java @@ -0,0 +1,67 @@ +package com.blossom.common.base.log; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; +import cn.hutool.extra.servlet.ServletUtil; +import com.blossom.common.base.util.SystemUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.system.ApplicationHome; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 下载日志文件 + * + * @author xzzz + */ +@Slf4j +public class LogDownloadUtil { + + private static final List downloadLogFileNames = new ArrayList() {{ + this.add("info.log"); + this.add("error.log"); + this.add("slowsql.log"); + }}; + + public static void download(HttpServletResponse response, Class clazz) { + ApplicationHome home = new ApplicationHome(clazz); + File dir = home.getDir(); + log.info("jar 包所在路径:{}", dir); + String logsPath = null; + + if (SystemUtil.isWindows()) { + logsPath = dir.getAbsolutePath(); + for (int i = 0; i < 3; i++) { + logsPath = StrUtil.sub(logsPath, 0, StrUtil.lastIndexOf(logsPath, "\\", logsPath.length(), true)); + } + logsPath = logsPath + File.separator + "logs"; + } else if (SystemUtil.isLinux()) { + logsPath = dir.getAbsolutePath() + File.separator + "logs"; + } + + if (StrUtil.isBlank(logsPath)) { + return; + } + + File file = FileUtil.newFile(logsPath); + File[] files = FileUtil.ls(file.getAbsolutePath()); + + List downloadLogs = new ArrayList<>(); + for (File logFile : files) { + if (downloadLogFileNames.contains(logFile.getName())) { + downloadLogs.add(logFile); + } + } + if (CollUtil.isNotEmpty(downloadLogs)) { + File zip = new File(logsPath + File.separator + "logzip.zip"); + ZipUtil.zip(zip, false, ArrayUtil.toArray(downloadLogs, File.class)); + ServletUtil.write(response, zip); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogIpConfig.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogIpConfig.java new file mode 100644 index 0000000..a4962dd --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/LogIpConfig.java @@ -0,0 +1,23 @@ +package com.blossom.common.base.log; + +import ch.qos.logback.core.PropertyDefinerBase; +import com.blossom.common.base.util.SystemUtil; + +/** + * 使用在 logback-spring.xml 中, 作为日志收集时的机器IP + * + * @author xzzz + */ +public class LogIpConfig extends PropertyDefinerBase { + + private static final String IP; + + static { + IP = SystemUtil.getIp(); + } + + @Override + public String getPropertyValue() { + return IP; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/pojo/DynamicLog.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/pojo/DynamicLog.java new file mode 100644 index 0000000..95d8e53 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/log/pojo/DynamicLog.java @@ -0,0 +1,26 @@ +package com.blossom.common.base.log.pojo; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * 动态日志级别 + * + * @author xzzz + */ +@Data +public class DynamicLog { + + /** + * 日志路径 + */ + @NotEmpty(message = "日志路径为必填项") + private String path; + + /** + * 日志级别 + */ + @NotEmpty(message = "日志级别为必填项") + private String level; +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractEntity.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractEntity.java new file mode 100644 index 0000000..9768eaa --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractEntity.java @@ -0,0 +1,38 @@ +package com.blossom.common.base.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * 抽象 entity + * + * @author xzzz + */ +@Data +public class AbstractEntity extends AbstractPOJO { + /** + * 创建名称,创建人ID + */ + private String creBy; + /** + * 创建时间 + */ + private Date creTime; + /** + * 修改人名称,修改人ID + */ + private String updBy; + /** + * 修改时间 + */ + private Date updTime; + /** + * 删除人名称,删除人ID + */ + private String delBy; + /** + * 删除时间,0 为未删除,非 0 为已删除 + */ + private Long delTime; +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractPOJO.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractPOJO.java new file mode 100644 index 0000000..c669484 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/AbstractPOJO.java @@ -0,0 +1,14 @@ +package com.blossom.common.base.pojo; + +import com.blossom.common.base.util.BeanUtil; + +/** + * @author xzzz + */ +public class AbstractPOJO { + + public T to(Class clazz) { + return BeanUtil.toObj(this, clazz); + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/DelReq.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/DelReq.java new file mode 100644 index 0000000..5ccffc5 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/DelReq.java @@ -0,0 +1,24 @@ +package com.blossom.common.base.pojo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * 删除参数 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DelReq extends AbstractPOJO { + + /** + * ID + */ + @NotNull(message = "ID为必填项") + @Min(value = 0, message = "ID不能小于0") + private Long id; +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/IRCode.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/IRCode.java new file mode 100644 index 0000000..13462b4 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/IRCode.java @@ -0,0 +1,25 @@ +package com.blossom.common.base.pojo; + +/** + * 响应码 + * + * @author xzzz + */ +public interface IRCode { + + /** + * 响应码 + * + * @return 响应码 + */ + String getCode(); + + /** + * 响应码信息 + * + * @return 响应码信息 + */ + String getMsg(); +} + + diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/PageRes.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/PageRes.java new file mode 100644 index 0000000..4243e1f --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/PageRes.java @@ -0,0 +1,51 @@ +package com.blossom.common.base.pojo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 共用分页对象 + * + * @author xzzz + */ +@Getter +@Setter +public class PageRes { + + /** + * 页码,0或1均表示第一页 + */ + private int pageNum; + /** + * 每页结果数 + */ + private int pageSize; + /** + * 总页数 + */ + private int totalPage; + /** + * 总行数 + */ + private long total; + /** + * 是否有下一页 + */ + private Boolean hasNextPage; + /** + * 分页数据 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private List datas; + + + public PageRes() { + } + + public PageRes(List datas) { + this.datas = datas; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/R.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/R.java new file mode 100644 index 0000000..f85c94d --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/R.java @@ -0,0 +1,201 @@ +package com.blossom.common.base.pojo; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.util.BeanUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.pagehelper.PageInfo; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +/** + * 返回对象 + * + * @author : xzzz + */ +@Getter +@Setter +public class R implements Serializable { + /** + * [公共响应] 自定义响应码, [20000]为正常, 其他请参阅错误码列表 + * @mock 20000 + */ + private String code; + /** + * [公共响应] 说明, 正常时返回 "成功" + * @mock 成功 + */ + private String msg; + /** + * [公共响应] 异常信息, 无后台错误时无该字段 + * @mock Exception by xxx + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String ex; + /** + * [公共响应] 请求的唯一后台标识, 方便接口联调。生产环境不会返回该字段。 + * @mock FAE58E61DD814D3D98DD6191051E8A6E + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String traceId; + /** + * [公共响应] 请求的用户信息, 方便接口联调。生产环境不会返回该字段。 + * @mock 123 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String userMetadata; + /** + * [公共响应] 请求的接口, 方便接口联调。生产环境不会返回该字段。 + * @mock /test + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String api; + /** + * [公共响应] 响应内容, 响应类如果为[?]则代表无 data + */ + private T data; + + private R() { + } + + // region 返回成功 + + /** + * 返回成功 + */ + public static R ok() { + return R.initR(RCode.SUCCESS.getCode(), RCode.SUCCESS.getMsg(), null, null); + } + + /** + * 返回成功 + * + * @param data 对象类型的data数据 + */ + public static R ok(T data) { + return R.initR(RCode.SUCCESS.getCode(), RCode.SUCCESS.getMsg(), null, data); + } + + /** + * 返回成功,并转换成 clazz 类型对象 + * + * @param data 数据 + * @param clazz 转换结果 + * @return clazz 类型对象 + */ + public static R ok(Object data, Class clazz) { + return R.initR(RCode.SUCCESS.getCode(), RCode.SUCCESS.getMsg(), null, BeanUtil.toObj(data, clazz)); + } + + /** + * 返回成功,并将集合中的对象转换成 clazz 对象 + * + * @param data 集合数据 + * @param clazz 转换结果 + * @param 集合泛型 + * @return 集合结果 + */ + public static R> ok(List data, Class clazz) { + return R.initR(RCode.SUCCESS.getCode(), RCode.SUCCESS.getMsg(), null, BeanUtil.toList(data, clazz)); + } + + /** + * 返回成功分页对象 + * + * @param page 分页对象 + * @param 分页对象泛型 + * @return 分页成功结果 + */ + public static R> ok(PageInfo page) { + PageRes pageRes = new PageRes<>(); + pageRes.setPageNum(page.getPageNum()); + pageRes.setPageSize(page.getPageSize()); + pageRes.setTotal(page.getTotal()); + pageRes.setTotalPage(page.getPages()); + pageRes.setDatas(page.getList()); + return R.ok(pageRes); + } + + /** + * 返回成功分页对象,并将分页集合中的对象转换成 clazz 对象 + * + * @param page 集合数据 + * @param clazz 转换结果 + * @param 转换后的分页数据泛型 + * @return 分页成功结果 + */ + public static R> ok(PageInfo page, Class clazz) { + PageRes pageRes = new PageRes<>(); + pageRes.setPageNum(page.getPageNum()); + pageRes.setPageSize(page.getPageSize()); + pageRes.setTotal(page.getTotal()); + pageRes.setTotalPage(page.getPages()); + pageRes.setDatas(BeanUtil.toList(page.getList(), clazz)); + pageRes.setHasNextPage(page.isHasNextPage()); + return R.ok(pageRes); + } + + // endregion + + // region 返回失败 + + /** + * 50000 默认响应信息 + */ + public static R fault() { + return R.initR(RCode.INTERNAL_SERVER_ERROR.getCode(), RCode.INTERNAL_SERVER_ERROR.getMsg(), null, null); + } + + /** + * 50000/自定义响应信息 + */ + public static R fault(String msg) { + return R.initR(RCode.INTERNAL_SERVER_ERROR.getCode(), msg, null, null); + } + + /** + * 失败, 自定义响应码和响应信息 + * + * @param code 响应码 + * @param msg 响应信息 + */ + public static R fault(String code, String msg) { + return R.initR(code, msg != null ? clearStr(msg) : RCode.byCode(code).getMsg(), null, null); + } + + /** + * 自定义返回值/自定义返回信息 + */ + public static R fault(String code, String msg, String ex) { + return R.initR(code, msg != null ? clearStr(msg) : RCode.byCode(code).getMsg(), ex, null); + } + + // endregion + + /** + * 初始化返回对象 + */ + private static R initR(String code, String msg, String ex, T data) { + R r = new R<>(); + r.setCode(code); + r.setMsg(msg); + r.setEx(ex); + r.setData(data); + return r; + } + + /** + * 对返回字符串进行一些格式处理 + * - 例如: + * - gateway返回的错误信息都包含双引号,序列化后会有多引号的情况 : ""内容"" + * + * @param str 字符串对象 + * @return 处理后的对象 + */ + private static String clearStr(String str) { + return StrUtil.replace(str, "\"", ""); + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/RCode.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/RCode.java new file mode 100644 index 0000000..b18d06b --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/pojo/RCode.java @@ -0,0 +1,82 @@ +package com.blossom.common.base.pojo; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * httpCode : httpCode返回码 + * code : 系统自定义返回码 + * msg : 异常信息 + * @author : xzzz + */ +@SuppressWarnings("ALL") +public enum RCode implements IRCode { + + /* 成功状态码 */ + SUCCESS ("20000", "成功"), + + /* ──────────────────────────────────────────── 400 ────────────────────────────────────────────────*/ + BAD_REQUEST ("40000", "错误的请求"), + + TOO_MANY_REQUESTS ("42901", "请求频繁, 请稍后再试"), + TOO_MANY_SEND_SMS ("42902", "短信发送频繁"), + + /* ──────────────────────────────────────────── 403 ────────────────────────────────────────────────*/ + SERVER_DENIED ("40301", "服务器拒绝访问"), + SERVER_DENIED_CUR_ENV ("40302", "当前环境无法访问该资源"), + + /* ──────────────────────────────────────────── 404 ────────────────────────────────────────────────────────────*/ + NOT_FOUND ("40400", "找不到您的请求"), + + /* ──────────────────────────────────────────── 500 ────────────────────────────────────────*/ + INTERNAL_SERVER_ERROR ("50000", "服务器处理错误"), + INTERNAL_SQL_ERROR ("50001", "服务器语句执行错误"), + + /* ──────────────────────────────────────────── 503 ────────────────────────────────────────*/ + SERVER_UNAVAILABLE ("50300", "服务器暂时无法处理请求, 请稍后再试"), + ; + + @Getter + private String code; + @Getter + private String msg; + + RCode(String code, String message) { + this.code = code; + this.msg = message; + } + + /** + * 根据code返回信息 + * @param code + * @return + */ + public static RCode byCode(String code) { + for (RCode item : RCode.values()) { + if (item.code.equals(code)) { + return item; + } + } + return RCode.INTERNAL_SERVER_ERROR; + } + + @Override + public String toString() { + return this.name(); + } + + //校验重复的code值 + public static void main(String[] args) { + RCode[] apiResultCodes = RCode.values(); + List codeList = new ArrayList<>(); + for (RCode apiResultCode : apiResultCodes) { + if (codeList.contains(apiResultCode.code)) { + System.out.println(apiResultCode.code); + } else { + codeList.add(apiResultCode.getCode()); + } + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/BeanUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/BeanUtil.java new file mode 100644 index 0000000..6451ac6 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/BeanUtil.java @@ -0,0 +1,97 @@ +package com.blossom.common.base.util; + +import com.blossom.common.base.exception.XzException500; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author xzzz + * @since 0.0.1 + */ +@SuppressWarnings("all") +public class BeanUtil extends org.springframework.beans.BeanUtils { + + private static final Logger log = LoggerFactory.getLogger(BeanUtil.class); + + /** + * 对象拷贝 (数据源为空返回空) + * + * @param from 数据源 + * @param to 目标对象 + * @param 范型 + * @return 数据源为空返回空 + */ + public static T toObj(Object from, Class to) throws BeansException { + if (null == from) { + return null; + } + Object dest; + try { + dest = to.newInstance(); + copyProperties(from, dest); + } catch (Exception e) { + throw new XzException500("BeanUtil.copyObject 转换异常: " + e.getMessage()); + } + return (T) dest; + } + + /** + * Spirng 原生的 bean copy, 比{@link BeanUtil#toObj(Object, Class)} 快, 但麻烦的是需要在外部创建对象 + * + * @param from 源文件的, 需要先创建对象 + * @param to 目标文件, 需要先创建对象 + */ + public static void toObj(Object from, Object to) throws BeansException { + if (null == from || to == null) { + return; + } + try { + copyProperties(from, to); + } catch (Exception e) { + throw new XzException500("BeanUtil.copyObject 转换异常: " + e.getMessage()); + } + } + + + /** + * BeanCopier的copy, 父类字段无法复制, 但是速度最快 + * + * @param from 源文件的, 需要先创建对象 + * @param to 目标文件, 需要先创建对象 + */ + public static void toObjByCopier(Object from, Object to) { + BeanCopier beanCopier = BeanCopier.create(from.getClass(), to.getClass(), false); + beanCopier.copy(from, to, null); + } + + /** + * 拷贝集合 + * + * @param from 数据源 + * @param to 目标 + * @param 范型 + * @return 数据源为空返回空 + */ + public static List toList(List from, Class to) { + if (null == from || from.isEmpty()) { + return null; + } + + List dest = new ArrayList<>(); + from.forEach((Object fromItem) -> { + try { + T toObject = (T) to.newInstance(); + copyProperties(fromItem, toObject); + dest.add(toObject); + } catch (Exception e) { + throw new XzException500("BeanUtil.copyObject 转换异常: " + e.getMessage()); + } + }); + return dest; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/DateUtils.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/DateUtils.java new file mode 100644 index 0000000..938c5ad --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/DateUtils.java @@ -0,0 +1,73 @@ +package com.blossom.common.base.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; + +import java.util.Calendar; +import java.util.Date; + + +/** + * @author xzzz + * @since 0.0.1 + */ +public class DateUtils extends DateUtil { + + public static String PATTERN_YYYYMM = DatePattern.NORM_MONTH_PATTERN; + public static String PATTERN_YYYYMMDD = DatePattern.NORM_DATE_PATTERN; + public static String PATTERN_YYYYMMDDHHMMSS = DatePattern.NORM_DATETIME_PATTERN; + public static String PATTERN_YYYYMMDDHHMMSS_SSS = DatePattern.NORM_DATETIME_MS_PATTERN; + + /** + * 将字符串格式的日期转为时间戳(毫秒) + * + * @param date 日期 + * @return 时间戳 + */ + public static long toTimestamp(String date) { + return toTimestamp(DateUtils.parse(date)); + } + + /** + * 将Date格式的日期转为时间戳(毫秒) + * + * @param date 日期 + * @return 时间戳 + */ + public static long toTimestamp(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.getTimeInMillis(); + } + + /** + * 将日期转换为 yyyy-MM-dd + * + * @param date 日期 + * @return yyyy-MM-dd + */ + public static String toYMD(Date date) { + return DateUtils.format(date, DatePattern.NORM_DATE_PATTERN); + } + + + /** + * 将日期转换为 yyyy-MM-dd HH:mm:ss + * + * @param date 日期 + * @return yyyy-MM-dd HH:mm:ss + */ + public static String toYMDHMS(Date date) { + return DateUtils.format(date, DatePattern.NORM_DATETIME_PATTERN); + } + + /** + * 时间戳换为 yyyy-MM-dd HH:mm:ss + * + * @param timestamp 时间戳 + * @return yyyy-MM-dd HH:mm:ss + */ + public static String toYMDHMS_SSS(Long timestamp) { + return DateUtils.format(date(timestamp), DatePattern.NORM_DATETIME_MS_PATTERN); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ExceptionUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ExceptionUtil.java new file mode 100644 index 0000000..84bf020 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ExceptionUtil.java @@ -0,0 +1,166 @@ +package com.blossom.common.base.util; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzException500; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; +import java.util.stream.Collectors; + +/** + * 异常工具包 + * + * @author xzzz + */ +@Slf4j +public class ExceptionUtil { + + /** + * 过滤的基础包。只输出与该包相关的异常信息 + */ + private static final String FILTER_PACKAGE = "com.blossom"; + + /** + * 过滤的堆栈信息 + * + * @param throwable 异常信息 + * @return String 堆栈信息 + */ + public static String printStackTrace(Throwable throwable) { + return printStackTrace(throwable, false, false); + } + + /** + * 不过滤的堆栈信息 + * + * @param throwable 异常信息 + * @return String 堆栈信息 + */ + public static String printStackTrace(Throwable throwable, boolean oneLine) { + return printStackTrace(throwable, false, oneLine); + } + + /** + * 堆栈信息追踪 + * + * @param throwable 异常信息 + * @param filter 是否过滤【true:只输出与 FILTER_PACKAGE 相关的异常信息, false:输出所有的异常信息】 + * @param oneLine 是否将堆栈输出为一行 + * @return String 堆栈信息 + */ + public static String printStackTrace(Throwable throwable, boolean filter, boolean oneLine) { + if (throwable == null) { + return ""; + } + if (throwable.getStackTrace() == null || throwable.getStackTrace().length == 0) { + return throwable.toString(); + } + if (throwable instanceof InvocationTargetException) { + return printStackTrace(((InvocationTargetException) throwable).getTargetException(), filter, oneLine); + } + List stackTraceElements; + if (filter) { + stackTraceElements = Arrays.stream(throwable.getStackTrace()) + .filter(element -> element.getClassName().startsWith(FILTER_PACKAGE)) + .collect(Collectors.toList()); + } else { + stackTraceElements = Arrays.stream(throwable.getStackTrace()).collect(Collectors.toList()); + } + + // 对堆栈信息各项格式化 + StringBuilder sb = new StringBuilder(); + if (StrUtil.isNotEmpty(throwable.getMessage())) { + sb.append(oneLine ? "" : "\n"); + sb.append("异常类型: ").append(throwable.getClass().getCanonicalName()); + sb.append(oneLine ? "。" : "\n"); + // 去除换行符 + sb.append("异常内容: ").append(removeTNRF(throwable.getMessage())); + sb.append(oneLine ? "。" : "\n"); + } + sb.append("堆栈信息: "); + sb.append(oneLine ? "" : "\n"); + stackTraceElements.forEach(element -> { + sb.append(" > ") + .append(element.getClassName()).append(".").append(element.getMethodName())// (类名.方法名) + .append("(").append(element.getFileName()).append(":").append(element.getLineNumber()).append(")");// (文件名:行数) + if (!oneLine) { + sb.append("\n"); + } + } + ); + return sb.toString(); + } + + /** + * 拼接堆栈信息 + * + * @param stackTraceElements 堆栈追踪元素列表 + * @param parameterJson 参数的 Json 信息 + * @return String 堆栈信息 + */ + private static String getStackMessage(String detailMessage, List stackTraceElements, + String parameterJson) { + StringBuilder sb = new StringBuilder("--error message detail:"); + sb.append(detailMessage); + if (null != parameterJson && parameterJson.trim().length() > 0) { + sb.append("\r\nParameters:").append(parameterJson).append("\n"); + } + stackTraceElements.forEach( + element -> sb.append("\tat ").append(element.getClassName()).append(".").append(element.getMethodName()) + .append("(").append(element.getFileName()).append(":").append(element.getLineNumber()) + .append(")\n")); + return sb.toString(); + } + + /** + * 获取由指定异常类引起的异常 + * + * @param throwable 异常 + * @param causeClasses 定义的引起异常的类 + * @return 是否由指定异常类引起 + */ + public static Throwable getCausedBy(Throwable throwable, Class... causeClasses) { + Throwable cause = throwable; + while (cause != null) { + for (Class causeClass : causeClasses) { + if (causeClass.isInstance(cause)) { + return cause; + } + } + cause = cause.getCause(); + } + return null; + } + + /** + * 去除多余的换行, 空格【"\t\n\r\f"】 + * + * @param original 原始字符串 + * @return String 去除换行后的字符串 + */ + public static String removeTNRF(String original) { + StringTokenizer tokenizer = new StringTokenizer(original, "\t\n\r\f"); + StringBuilder builder = new StringBuilder(); + boolean hasMoreTokens = tokenizer.hasMoreTokens(); + while (hasMoreTokens) { + builder.append(tokenizer.nextToken()); + hasMoreTokens = tokenizer.hasMoreTokens(); + if (hasMoreTokens) { + builder.append(' '); + } + } + return builder.toString(); + } + + public static void main(String[] args) { + try { + throw new XzException500("123123"); + } catch (Exception e) { + e.printStackTrace(); + log.error("发生异常, {}", ExceptionUtil.printStackTrace(e, false)); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/PrimaryKeyUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/PrimaryKeyUtil.java new file mode 100644 index 0000000..3edd564 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/PrimaryKeyUtil.java @@ -0,0 +1,149 @@ +package com.blossom.common.base.util; + + +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Date; + +/** + * 主键工具类 + * 使用前提:IP 地址的,前两个网段重复,后两个网段不能重复。 + * 可用时间:139 年 + * 可用机器数:128 * 254 = 32512 + * 每秒产生的 Id 数量:64000 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class PrimaryKeyUtil { + + /** + * 起始的时间戳 2021-04-15 00:00:00 + */ + private final static long START_STAMP = 1618416000000L; + + /** + * 起始的时间戳 2021-04-01 00:00:00 + */ + // private final static long START_STAMP = 1617206400000L; + + /** + * 每一部分占用的位数 + *

+ * SEQUENCE_BIT:序列号占用的位数 + * DATACENTER_BIT:数据中心占用的位数 + * MACHINE_BIT:机器标识占用的位数 + */ + private final static long SEQUENCE_BIT = 6; + private final static long DATACENTER_BIT = 8; + private final static long MACHINE_BIT = 8; + + /** + * 每一部分的最大值 + */ + private final static long MAX_DATACENTER_NUM = ~(-1L << DATACENTER_BIT); + private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT); + private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT); + + /** + * 每一部分向左的位移 + */ + private final static long MACHINE_LEFT = SEQUENCE_BIT; + private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; + private final static long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; + + /** + * 数据中心 ID + */ + private static final long datacenterId; + /** + * 机器标识 ID + */ + private static final long machineId; + /** + * 序列号 + */ + private static long sequence = 0L; + /** + * 上一次时间戳 + */ + private static long lastStamp = -1L; + + static { + try { + String ipAddr = InetAddress.getLocalHost().getHostAddress(); + String[] ipAddrArr = ipAddr.split("\\."); + datacenterId = Long.parseLong(ipAddrArr[2]); + machineId = Long.parseLong(ipAddrArr[3]); + + if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { + throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); + } + if (machineId > MAX_MACHINE_NUM || machineId < 0) { + throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); + } + } catch (UnknownHostException e) { + throw new RuntimeException("生成唯一 Id 时,获取机器 Ip 异常"); + } + } + + /** + * 产生下一个ID + * + * @return long + */ + public static synchronized long nextId() { + long currStamp = getNewStamp(); + if (currStamp < lastStamp) { + throw new RuntimeException("时钟事件出现倒退,拒绝生成 ID"); + } + + if (currStamp == lastStamp) { + //相同毫秒内,序列号自增 + sequence = (sequence + 1) & MAX_SEQUENCE; + //同一毫秒的序列数已经达到最大 + if (sequence == 0L) { + currStamp = getNextMill(); + } + } else { + //不同毫秒内,序列号置为0 + sequence = 0L; + } + + lastStamp = currStamp; + + // 时间戳部分 + return (currStamp - START_STAMP) << TIMESTAMP_LEFT + // 数据中心部分 + | datacenterId << DATACENTER_LEFT + // 机器标识部分 + | machineId << MACHINE_LEFT + // 序列号部分 + | sequence; + } + + private static long getNextMill() { + long mill = getNewStamp(); + while (mill <= lastStamp) { + mill = getNewStamp(); + } + return mill; + } + + private static long getNewStamp() { + return System.currentTimeMillis(); + } + + private static int getHostId(String s, int max) { + byte[] bytes = s.getBytes(); + int sums = 0; + for (int b : bytes) { + sums += b; + } + return sums % (max + 1); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ProxyUtils.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ProxyUtils.java new file mode 100644 index 0000000..d7a0b42 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ProxyUtils.java @@ -0,0 +1,26 @@ +package com.blossom.common.base.util; + +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.aop.framework.ProxyFactory; + +/** + * 代理辅助方法 + * + * @author xzzz + */ +public final class ProxyUtils { + + /** + * 创建指定对象的代理类 + * + * @param obj 对象 + * @param interceptor 代理方法 + * @return 代理类 + */ + public static Object getProxy(Object obj, MethodInterceptor interceptor) { + ProxyFactory proxy = new ProxyFactory(obj); + proxy.setProxyTargetClass(true); + proxy.addAdvice(interceptor); + return proxy.getProxy(); + } +} \ No newline at end of file diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ServletUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ServletUtil.java new file mode 100644 index 0000000..397b504 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/ServletUtil.java @@ -0,0 +1,229 @@ +package com.blossom.common.base.util; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.Objects; + +/** + * @author xzzz + */ +public class ServletUtil extends cn.hutool.extra.servlet.ServletUtil { + + private static final String UNKNOWN = "unknown"; + + private static final String HEADER_USER_AGENT = "User-Agent"; + + /** + * 获取 HttpServletRequest + */ + public static HttpServletRequest getRequest() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes == null) { + return null; + } + return servletRequestAttributes.getRequest(); + } + + /** + * 获取 HttpServletResponse + */ + public static HttpServletResponse getResponse() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); + } + + /** + * 判断是否ajax请求 + * spring ajax 返回含有 ResponseBody 或者 RestController注解 + * + * @param handlerMethod HandlerMethod + * @return 是否ajax请求 + */ + public static boolean isBody(HandlerMethod handlerMethod) { + ResponseBody responseBody = handlerMethod.getMethodAnnotation(ResponseBody.class); + return responseBody != null; + } + + /** + * 获取请求IP + */ + public static String getIP() { + return getIP(getRequest()); + } + + /** + * 获取请求IP + */ + public static String getIP(HttpServletRequest request) { + Assert.notNull(request, "HttpServletRequest is null"); + String ip = request.getHeader("X-Requested-For"); + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return StrUtil.isBlank(ip) ? null : ipWrapper(ip.split(",")[0]); + } + + /** + * 获取请求头 user-agent + */ + public static String getUserAgent() { + return getRequest().getHeader(HEADER_USER_AGENT); + } + + /** + * 获取请求头 user-agent + */ + public static String getUserAgent(HttpServletRequest request) { + return request.getHeader(HEADER_USER_AGENT); + } + + /** + * 获取请求方法 + */ + public static String getMethod() { + return methodWrapper(Objects.requireNonNull(getRequest()).getMethod()); + } + + /** + * 根据传入的文件名, 获取对应的图片 content-type, 文件不为图片文件, 则返回 "" + * + * @param filename 文件名 + * @return content-type + */ + public static String getContentTypeImage(String filename) { + String suffix = FileUtil.getSuffix(filename); + if ("jpg".equals(suffix) || "jpeg".equals(suffix)) { + return "image/jpeg"; + } + if ("png".equals(suffix)) { + return "image/png"; + } + if ("gif".equals(suffix)) { + return "image/gif"; + } + if ("tiff".equals(suffix) || "tif".equals(suffix)) { + return "image/tiff"; + } + if ("jfif".equals(suffix)) { + return "image/jpeg"; + } + if ("ico".equals(suffix)) { + return "image/x-icon"; + } + return suffix; + } + + /** + * 获取请求URI + *

URL:GET http://localhost:8080/user + *

URI:/user + */ + public static String getUri() { + return getRequest().getRequestURI().toLowerCase(); + } + + /** + * 将请求方法按统一长度返回 + *

只支持GET,POST,PUT,DELETE + */ + public static String methodWrapper(String method) { + switch (method) { + case "GET": + return "GET "; + case "POST": + return "POST"; + case "PUT": + return "PUT "; + case "DELETE": + return "DEL "; + default: + return method; + } + } + + /** + * 将IP按15位长度返回 + *

{@code
+     * "255.255.255.255" 将返回 "255.255.255.255"
+     * "192.168.0.1"     将返回 "192.168.0.1    "
+     * }
+ */ + public static String ipWrapper(String ip) { + int ipLength = 15; + if (ip.length() >= ipLength) { + return ip; + } + int difference = ipLength - ip.length(); + StringBuilder ipBuilder = new StringBuilder(ip); + for (int i = 0; i < difference; i++) { + ipBuilder.append(" "); + } + ip = ipBuilder.toString(); + return ip; + } + + /** + * 获取请求Body, 需要谨慎使用, Body只能获取一次 + */ + public static String getBodyString(ServletRequest request) { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = request.getInputStream(); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return sb.toString(); + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SortUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SortUtil.java new file mode 100644 index 0000000..3c80982 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SortUtil.java @@ -0,0 +1,111 @@ +package com.blossom.common.base.util; + +import cn.hutool.core.util.StrUtil; + +import java.util.*; + +/** + * @author xzzz + */ +public class SortUtil { + + /** + * int 类型的排序, 也可以直接使用 Integer::compareTo + */ + public static Comparator intSort = Integer::compareTo; + + public static Comparator dateSort = Date::compareTo; + + /** + * 字符串排序功能 + */ + public static Comparator strSort = (str1, str2) -> { + // 处理数据为null的情况 + if (str1 == null && str2 == null) { + return 0; + } + if (str1 == null) { + return -1; + } + if (str2 == null) { + return 1; + } + // 比较字符串中的每个字符 + char c1; + char c2; + // 逐字比较返回结果 + for (int i = 0; i < str1.length(); i++) { + c1 = str1.charAt(i); + try { + c2 = str2.charAt(i); + } catch (StringIndexOutOfBoundsException e) { // 如果在该字符前,两个串都一样,str2更短,则str1较大 + return 1; + } + // 如果都是数字的话,则需要考虑多位数的情况,取出完整的数字字符串,转化为数字再进行比较 + if (Character.isDigit(c1) && Character.isDigit(c2)) { + StringBuilder numStr1 = new StringBuilder(); + StringBuilder numStr2 = new StringBuilder(); + // 获取数字部分字符串 + for (int j = i; j < str1.length(); j++) { + c1 = str1.charAt(j); + if (!Character.isDigit(c1)) { // 不是数字则直接退出循环 + break; + } + numStr1.append(c1); + } + for (int j = i; j < str2.length(); j++) { + c2 = str2.charAt(j); + if (!Character.isDigit(c2) && c2 != '.') { // 考虑可能带小数的情况 + break; + } + numStr2.append(c2); + } + // 转换成数字数组进行比较 适配 1.25.3.5 这种情况 + String[] numberArray1 = numStr1.toString().split("\\."); + String[] numberArray2 = numStr2.toString().split("\\."); + return compareNumberArray(numberArray1, numberArray2); + } + + // 不是数字的比较方式 + if (c1 != c2) { + return c1 - c2; + } + } + return 0; + }; + + public static int compareNumberArray(String[] numberArray1, String[] numberArray2) { + for (int i = 0; i < numberArray1.length; i++) { + // 此时数字数组2比1短,直接返回 + if (numberArray2.length < i + 1) { + return 1; + } + int compareResult = Integer.valueOf(numberArray1[i]).compareTo(Integer.valueOf(numberArray2[i])); + if (compareResult != 0) { + return compareResult; + } + } + // 说明数组1比数组2短,返回小于 + return -1; + } + + /** + * 返回的是 SortedMap,一个排序后的 treeMap + * + * @param paramMap 参数集合 + * @return Map 排序后的参数集合 + */ + public static Map sortMap(Map paramMap) { + SortedMap map = new TreeMap<>(); + String value; + for (String key : paramMap.keySet()) { + if (key != null) { + value = paramMap.get(key); + if (StrUtil.isNotEmpty(value)) { + map.put(key, value); + } + } + } + return map; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SystemUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SystemUtil.java new file mode 100644 index 0000000..e794886 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/SystemUtil.java @@ -0,0 +1,305 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.common.base.util; + +import com.sun.management.OperatingSystemMXBean; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.lang.management.ManagementFactory; +import java.net.*; +import java.util.Enumeration; + +/** + * Get host name and ip of the host. + * + * @author xzzz + */ +@Slf4j +public final class SystemUtil { + + private static String ip; + private static String hostName; + + static { + try { + // Init the host information. + resolveHost(); + } catch (Exception e) { + log.info("Failed to get local host", e); + } + } + + private static void resolveHost() throws Exception { + InetAddress addr = InetAddress.getLocalHost(); + hostName = addr.getHostName(); + ip = addr.getHostAddress(); + if (addr.isLoopbackAddress()) { + // find the first IPv4 Address that not loopback + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface in = interfaces.nextElement(); + Enumeration addrs = in.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress address = addrs.nextElement(); + if (!address.isLoopbackAddress() && address instanceof Inet4Address) { + ip = address.getHostAddress(); + } + } + } + } + } + + /** + * 获取本地IP + */ + public static String getIp() { + return ip; + } + + /** + * 获取本地名称 + */ + public static String getHostName() { + return hostName; + } + + + /** + * Resolve and get current process ID. + * + * @return current process ID + */ + public static int getPid() { + // Note: this will trigger local host resolve, which might be slow. + String name = ManagementFactory.getRuntimeMXBean().getName(); + return Integer.parseInt(name.split("@")[0]); + } + + /** + * 逗号 + */ + private static final String COMMA = ","; + private static final String UNKNOWN = "unknown"; + private static final String LOCAL_IPV4 = "unknown"; + private static final String LOCAL_IPV6 = "unknown"; + + /** + * 获取远程客户端的真实 IP 地址 + * @param request 请求对象 + * @return String 真实的 IP 地址 + */ + public static String getRemoteIp(HttpServletRequest request) { + // X-Forwarded-For 记录一个请求从客户端出发到目标服务器过程中经历的代理,或者负载平衡设备的IP。 + String ip = request.getHeader("x-forwarded-for"); + if (null != ip && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) { + // 多次反向代理后会有多个IP值,第一个IP才是真实IP,它们按照英文逗号','分割 + if (ip.contains(COMMA)) { + ip = ip.split(COMMA)[0]; + } + } + // Proxy-Client-IP / WL-Proxy-Client-IP 这个一般是经过apache http服务器的请求才会有, + // 用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。 + if (isInvalidIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (isInvalidIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + // HTTP_CLIENT_IP 有些代理服务器会加上此请求头。 + if (isInvalidIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (isInvalidIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + // X-Real-IP nginx代理一般会加上此请求头。 + if (isInvalidIp(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (isInvalidIp(ip)) { + ip = request.getRemoteAddr(); + // 127.0.0.1 ipv4, 0:0:0:0:0:0:0:1 ipv6 + if (LOCAL_IPV4.equals(ip) || LOCAL_IPV6.equals(ip)) { + // 根据网卡取本机配置的IP + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + if(null != inetAddress) { + ip = inetAddress.getHostAddress(); + } + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + } + return ip; + } + + /** + * 判断 IP 是否无效 + * @param ip ip 地址 + * @return IP 地址为 null、空字符串、unknown 是: true + */ + private static boolean isInvalidIp(String ip) { + return null == ip || ip.trim().length() == 0 || "unknown".equalsIgnoreCase(ip); + } + + /** + * 获取本机 IP 地址 + * + * @return String ip地址 + * @throws UnknownHostException,SocketException 未知名称或服务异常,网络异常 + */ + public static String getLocalIpAddress() throws UnknownHostException, SocketException { + // 判断为windows系统 + if (isWindows()) { + return InetAddress.getLocalHost().getHostAddress(); + } else { + return getLinuxLocalIpAddress(); + } + } + + /** + * 获取 Linux下的 IP 地址 + * + * @return String linux系统下的Ip地址 + * @throws UnknownHostException,SocketException 未知名称或服务异常,网络异常 + */ + private static String getLinuxLocalIpAddress() throws UnknownHostException, SocketException { + InetAddress inetAddress = InetAddress.getLocalHost(); + if (!inetAddress.isLoopbackAddress()) { + return inetAddress.getHostAddress(); + } + String ip = ""; + NetworkInterface networkInterface; + String name; + Enumeration enumIpAddr; + String ipaddress; + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + networkInterface = en.nextElement(); + name = networkInterface.getName(); + if (!name.contains("docker") && !name.contains("lo")) { + for (enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress()) { + ipaddress = inetAddress.getHostAddress(); + if (!ipaddress.contains("::") && !ipaddress.contains("0:0:") && !ipaddress.contains("fe80")) { + ip = ipaddress; + } + } + } + } + } + return ip; + } + + /** + * WINDOWS系统 + */ + private static final String WINDOWS = "windows"; + + /** + * Linux系统 + */ + private static final String LINUX = "linux"; + + /** + * 操作系统属性名称 + */ + private static final String OS_NAME = "os.name"; + + /** + * 判断是否是windows操作系统 + * + * @return boolean 【true:是】 + */ + public static boolean isWindows() { + return System.getProperty(OS_NAME).toLowerCase().contains(WINDOWS); + } + + /** + * 判断是否是Linux操作系统 + * + * @return boolean 【true:是】 + */ + public static boolean isLinux() { + return System.getProperty(OS_NAME).toLowerCase().contains(LINUX); + } + + /** + * 获取服务器的硬盘使用情况 + * + * @return String 硬盘的使用信息 + */ + public static String getDiskInfo() { + StringBuilder sb = new StringBuilder(); + // 获取磁盘分区列表 + File[] roots = File.listRoots(); + for (File file : roots) { + long totalSpace = file.getTotalSpace(); + long usableSpace = file.getUsableSpace(); + if (totalSpace > 0) { + sb.append(file.getPath()).append("(总计:"); + sb.append(Math.round(((double) totalSpace / (1024 * 1024 * 1024)) * 100 / 100.0)).append("GB "); + if (Math.round((((double) usableSpace / (1024 * 1024 * 1024)) * 100) / 100.0) <= 1) { + sb.append("剩余:").append(Math.round((((double) usableSpace / (1024 * 1024)) * 100) / 100.0)).append("MB)
"); + } else { + sb.append("剩余:").append(Math.round((((double) usableSpace / (1024 * 1024 * 1024)) * 100) / 100.0)).append("GB)
"); + } + } + } + return sb.toString(); + } + + /** + * 获取服务器的内存使用情况 + * + * @return String 内存的使用信息 + */ + public static String getMemoryInfo() { + StringBuilder sb = new StringBuilder(); + OperatingSystemMXBean osmb = (OperatingSystemMXBean) ManagementFactory + .getOperatingSystemMXBean(); + sb.append("&&&&内存情况:系统内存总计:").append(osmb.getTotalPhysicalMemorySize() / 1024 / 1024).append("MB,"); + sb.append("可用内存:").append(osmb.getFreePhysicalMemorySize() / 1024 / 1024).append("MB"); + return sb.toString(); + } + + /** + * 获取服务器硬盘分区下的目录和文件列表 + * + * @return String 服务器硬盘分区下的目录和文件列表 + */ + public static String getDiskFileList() { + StringBuilder sb = new StringBuilder(); + String[] fileList; + // 获取硬盘分区列表; + File[] roots = File.listRoots(); + long totalSpace; + for (File file : roots) { + totalSpace = file.getTotalSpace(); + fileList = file.list(); + if (totalSpace > 0 && fileList != null && fileList.length > 0) { + sb.append(file.getPath()).append("下目录和文件:"); + for (String s : fileList) { + sb.append(s).append("/n"); + } + } + } + return sb.toString(); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/TimeUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/TimeUtil.java new file mode 100644 index 0000000..4d5ebb8 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/TimeUtil.java @@ -0,0 +1,34 @@ +package com.blossom.common.base.util; + +import java.util.concurrent.TimeUnit; + +/** + * Provides millisecond-level time of OS. + * + * @author xzzz + */ +public final class TimeUtil { + + private static volatile long currentTimeMillis; + + static { + currentTimeMillis = System.currentTimeMillis(); + Thread daemon = new Thread(() -> { + while (true) { + currentTimeMillis = System.currentTimeMillis(); + try { + TimeUnit.MILLISECONDS.sleep(1); + } catch (Throwable ignored) { + + } + } + }); + daemon.setDaemon(true); + daemon.setName("blossom-time-tick-thread"); + daemon.start(); + } + + public static long currentTimeMillis() { + return currentTimeMillis; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/json/JsonUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/json/JsonUtil.java new file mode 100644 index 0000000..701a09f --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/json/JsonUtil.java @@ -0,0 +1,361 @@ +package com.blossom.common.base.util.json; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * JSON工具类 + * + * @author : xzzz + */ +@Slf4j +public class JsonUtil { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; + + static { + SimpleDateFormat myDateFormat = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT); + mapper.setDateFormat(myDateFormat); + // 设置输入时忽略JSON字符串中存在而Java对象实际没有的属性 + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // 允许下换线和驼峰之间的转换 +// mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + // 允许出现单引号 + mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + // 解决:序列化 map 时,有 key 为 null 的情况,jackson序列化会报 Null key for a Map not allowed in JSON (use a converting NullKeySerializer?) + mapper.getSerializerProvider().setNullKeySerializer(new NullKeySerializer()); + mapper.registerModule(getJavaTimeModule()); + // 忽视为null的字段 +// mapper.setSerializationInclusion(Include.NON_NULL); + } + + + /** + * 返回一个可以对任何属性进行序列化, 且在 Json 中会携带类型的 ObjectMapper + * + * @return ObjectMapper + */ + public static ObjectMapper typeObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + /* + * 所有字段都会被序列化, 无论这个字段是否有 getter/setter 方法, 以及方法是否是 public 或其他 + */ + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + /* + * 将在 Json 中带有被序列化的类型, 用于反序列化时的类型推断 + */ + objectMapper.activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY + ); + objectMapper.registerModule(getJavaTimeModule()); + return objectMapper; + } + + /** + * Key为null是的序列化方式 + */ + public static class NullKeySerializer extends StdSerializer { + + public NullKeySerializer() { + this(null); + } + + public NullKeySerializer(Class t) { + super(t); + } + + @Override + public void serialize(Object nullKey, JsonGenerator jsonGenerator, SerializerProvider unused) throws IOException { + jsonGenerator.writeFieldName("null"); + } + } + + /** + * LocalDateTime系列序列化和反序列化模块,继承自jsr310 + */ + public static Module getJavaTimeModule() { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))); + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))); + javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))); + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))); + javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); + return javaTimeModule; + } + + + /** + * 判断json格式是否合法 + * + * @param str 字符串 + * @return true:为Json,false:不为Json + */ + public static boolean isJson(String str) { + try { + mapper.readTree(str); + return true; + } catch (IOException e) { + return false; + } + } + + + // region 字符串转其他 + + + /** + * 字符串转对象 + */ + public static T toObj(String str, Class c) { + if (StrUtil.isBlank(str)) { + return null; + } + T t = null; + try { + t = mapper.readValue(str, c); + } catch (Exception e) { + e.printStackTrace(); + } + return t; + } + + /** + * JSON字符串转为指定的类 + * + * @param str 字符串 + * @param tr 指定类 + */ + public static T toObj(String str, TypeReference tr) { + if (StrUtil.isBlank(str)) { + return null; + } + + T t = null; + try { + t = (T) mapper.readValue(str, tr); + } catch (Exception e) { + e.printStackTrace(); + } + return (T) t; + } + + /** + * 将JSON转为MAP + */ + public static Map toMap(String str) { + try { + return mapper.readValue(str, HashMap.class); + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalStateException("转换错误:" + e.getMessage()); + } + } + + public static Map toMap(Object str) { + try { + return mapper.readValue(toJson(str), HashMap.class); + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalStateException("转换错误:" + e.getMessage()); + } + } + + /** + * json字符串转JsonNode + */ + public static JsonNode toJsonNode(String jsonStr) { + try { + return mapper.readTree(jsonStr); + } catch (Exception e) { + throw new IllegalStateException("转换错误:" + e.getMessage()); + } + } + + /** + * json字符串集合转对象集合 + * + * @param strings 字符串集合 + * @param clazz 对象类型 + * @param 对象泛型 + * @return 对象集合 + */ + public static List toObjList(Collection strings, Class clazz) { + if (CollUtil.isEmpty(strings)) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + for (String string : strings) { + E e = toObj(string, clazz); + list.add(e); + } + + return list; + } + // endregion + + + // region 对象转其他 + + /** + * 对象转为JSON + */ + public static String toJson(Object obj) { + if (obj == null) { + return null; + } + String s; + try { + s = mapper.writeValueAsString(obj); + } catch (Exception e) { + e.printStackTrace(); + throw new IllegalArgumentException("对象无法转换为JSON:" + e.getMessage()); + } + return s; + } + + public static String toPrettyJson(Object obj) { + if (obj == null) { + return null; + } + String s; + try { + s = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj); + } catch (Exception e) { + e.printStackTrace(); + throw new IllegalArgumentException("对象无法转换为JSON:" + e.getMessage()); + } + return s; + } + + /** + * 对象转JsonNode + */ + public static JsonNode toJsonNode(Object obj) { + try { + return mapper.readTree(toJson(obj)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * 对象转byte数组 + */ + public static byte[] toByte(Object obj) { + try { + return mapper.writeValueAsBytes(obj); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + // endregion + + + // region Map转其他 + + /** + * 将Map转成指定的Bean + */ + public static T toObj(Map map, Class clazz) { + try { + return mapper.readValue(toJson(map), clazz); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + // endregion + + + // region Byte数组转其他 + + + /** + * 数组转JsonNode + */ + public static JsonNode toJsonNode(byte[] bytes) { + try { + return mapper.readValue(bytes, JsonNode.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * 数组转JsonNode + */ + public static Object toObj(byte[] bytes) { + try { + return mapper.readValue(bytes, Object.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static T toObj(byte[] bytes, Class clazz) { + try { + return mapper.readValue(bytes, clazz); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * byte数组转Json + */ + public static String toJson(byte[] bytes) { + return new String(bytes); + } + // endregion + + + // region JsonNode 转其他 + + /** + * JsonNode转byte[] + */ + public static byte[] toByte(JsonNode node) { + try { + return mapper.writeValueAsBytes(node); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + // endregion + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/HttpUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/HttpUtil.java new file mode 100644 index 0000000..41e01fb --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/HttpUtil.java @@ -0,0 +1,508 @@ +package com.blossom.common.base.util.okhttp; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.base.util.SortUtil; +import com.blossom.common.base.util.spi.ServiceLoaderUtil; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.TimeUnit; + +/** + * Http 请求工具类 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +public class HttpUtil { + + public static final OkHttpClient OK_HTTP_CLIENT; + public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + public static final MediaType MULTIPART_FORM_DATA = MediaType.parse("multipart/form-data; charset=utf-8"); + + static { + OkHttpClient.Builder okHttpBuilder = new OkHttpClient() + .newBuilder() + .sslSocketFactory(getSslSocketFactory(), getX509TrustManager()) + // 连接池大小 + // maxIdleConnections : 空闲连接的最大数量 + // keepAliveDuration : 连接空闲时间最多为 300 秒 + .connectionPool(new ConnectionPool(200, 300, TimeUnit.SECONDS)) + .connectTimeout(5L, TimeUnit.SECONDS) + .readTimeout(5L, TimeUnit.SECONDS) + .writeTimeout(5L, TimeUnit.SECONDS) + .retryOnConnectionFailure(true); + + try { + // 获取实现类 + ServiceLoader interceptors = ServiceLoaderUtil.getServiceLoader(OkHttpInterceptor.class); + // 通过SPI获取 + if (CollUtil.isNotEmpty(interceptors)) { + for (OkHttpInterceptor interceptor : interceptors) { + interceptor.instructions(); + okHttpBuilder.addInterceptor(interceptor); + } + } + } catch (Exception ignored) { + } + + OK_HTTP_CLIENT = okHttpBuilder.build(); + + } + + private static X509TrustManager getX509TrustManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } + + private static SSLSocketFactory getSslSocketFactory() { + String algorithm = "TLS"; + try { + //信任任何链接 + SSLContext sslContext = SSLContext.getInstance(algorithm); + sslContext.init(null, new TrustManager[]{getX509TrustManager()}, new SecureRandom()); + return sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException e) { + throw new XzException500("没有" + algorithm + "这样的算法"); + } catch (KeyManagementException e) { + throw new XzException500("秘钥管理异常:" + e.getMessage()); + } + } + + // ----------------------------------- Get ----------------------------------- + + /** + * 同步 Get 请求 + * + * @param url 请求路径 + * @return String 结果 + */ + public static String get(String url) { + return newCall(buildRequest(url)); + } + + /** + * 同步 Get 请求 + * + * @param url 请求路径 + * @param basicAuth 基本认证 basicAuth[0]:username、basicAuth[1]:password + * @return String 请求结果 + */ + public static String get(String url, String[] basicAuth) { + String credential = Credentials.basic(basicAuth[0], basicAuth[1]); + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", credential); + return newCall(buildRequest(url, headerMap)); + } + + /** + * 同步 Get 请求 + * + * @param url 请求路径 + * @param paramMap 参数集合 + * @return String 请求结果 + */ + public static String get(String url, Map paramMap) { + return newCall(buildRequest(spliceGetRequestUrl(url, paramMap))); + } + + /** + * 同步 Get 请求 + * + * @param url 请求路径 + * @param headerMap 请求头参数集合 + * @param paramMap 参数集合 + * @return String 请求结果 + */ + public static String get(String url, Map headerMap, Map paramMap) { + return newCall(buildRequest(spliceGetRequestUrl(url, paramMap), headerMap)); + } + + /** + * 异步 Get 方式请求 + * + * @param url 请求路径 + * @param callback 回调响应处理对象 + */ + public static void getAsync(String url, Callback callback) { + enqueueNewCall(OK_HTTP_CLIENT.newCall(buildRequest(url)), callback); + } + + // ----------------------------------- Post ----------------------------------- + + /** + * 同步 Post 方式请求 json 提交参数 + * + * @param url 请求路径 + * @param jsonBody 请求体 body 字符串 + * @return String 结果 + */ + public static String post(String url, String jsonBody) { + return newCall(buildRequest(url, new HashMap<>(), createBody(jsonBody))); + } + + /** + * 同步 Post 方式请求 json 提交参数 + * + * @param url 请求路径 + * @param jsonBody 请求体 body 字符串 + * @return byte数组结果 + */ + public static byte[] postResponseBytes(String url, String jsonBody) { + return newCallResponseBytes(buildRequest(url, new HashMap<>(), createBody(jsonBody))); + } + + /** + * 同步 Post 请求 + * + * @param url 请求路径 + * @param paramMap 请求参数集合 + * @param headerMap 对象头参数集合 + * @return String 响应结果 + */ + public static String post(String url, Map headerMap, Map paramMap) { + return newCall(buildRequest(url, headerMap, paramMap)); + } + + /** + * 同步 Post 方式请求 json 提交参数 + * + * @param url 请求路径 + * @param jsonStr 请求参数 json 字符串 + * @param basicAuth 基本认证 basicAuth[0]:username、basicAuth[1]:password + * @return String 结果 + */ + public static String post(String url, String jsonStr, String[] basicAuth) { + String credential = Credentials.basic(basicAuth[0], basicAuth[1]); + Map headerMap = new HashMap<>(); + headerMap.put("Authorization", credential); + return post(url, jsonStr, headerMap); + } + + /** + * 同步 Post 请求 + * + * @param url 请求路径 + * @param jsonStr 请求参数 json 字符串 + * @param headerMap 对象头参数集合 + * @return String 响应结果 + */ + public static String post(String url, String jsonStr, Map headerMap) { + return newCall(buildRequest(url, new HashMap<>(), createBody(jsonStr))); + } + + /** + * 同步 Post 方式请求 json 提交参数 + * + * @param url 请求路径 + * @param paramMap 请求参数集合 + * @param appcode APP 编码 + * @return String 结果 + */ + public static String post(String url, Map paramMap, String appcode) { + Map headerMap = new HashMap<>(); + headerMap.put("Authorization", "APPCODE " + appcode); + return post(url, paramMap, headerMap); + } + + /** + * 同步 Post 请求【文件上传】 + * + * @param url 请求路径 + * @param paramMap 请求参数集合 + * @param headerMap 对象头参数集合 + * @return String 响应结果 + */ + public static String postFile(String url, Map headerMap, Map paramMap, + String fileParam, File file) { + MultipartBody.Builder builder = new MultipartBody.Builder(); + builder.setType(MultipartBody.FORM); + if (MapUtil.isNotEmpty(paramMap)) { + String value; + for (String key : paramMap.keySet()) { + value = paramMap.get(key); + if (StrUtil.isNotEmpty(value)) { + builder.addFormDataPart(key, value); + } + } + } + if (file.exists()) { + builder.addFormDataPart(fileParam, file.getName(), RequestBody.create(MULTIPART_FORM_DATA, file)); + } + + MultipartBody body = builder.build(); + return newCall(buildRequest(url, headerMap, body)); + } + + /** + * 异步post方式请求-json提交参数 + * + * @param url 请求路径 + * @param jsonStr 请求参数 + */ + public static void postAsync(String url, String jsonStr, Callback callback) { + enqueueNewCall(OK_HTTP_CLIENT.newCall(buildRequest(url, null, createBody(jsonStr))), callback); + } + + /** + * 异步post方式请求-form表单提交参数 + * + * @param url 请求路径 + * @param paramMap 请求参数 + */ + public static void postAsync(String url, Map paramMap, Callback callback) { + enqueueNewCall(OK_HTTP_CLIENT.newCall(buildRequest(url, null, createBody(paramMap))), callback); + } + + /** + * 拼接 GET 请求的路径 + * + * @param url 请求地址 + * @param paramMap 参数集合 + * @return String 请求地址 + */ + private static String spliceGetRequestUrl(String url, Map paramMap) { + if (MapUtil.isEmpty(paramMap)) { + return url; + } + paramMap = SortUtil.sortMap(paramMap); + StringBuilder builder = new StringBuilder(); + String value; + for (String key : paramMap.keySet()) { + value = paramMap.get(key); + builder.append(key); + builder.append('='); + builder.append(value); + builder.append('&'); + } + String text = builder.toString(); + String paramStr = text.substring(0, text.length() - 1); + if (StrUtil.isEmpty(paramStr)) { + return url; + } + return url + "?" + paramStr; + } + + /** + * 执行请求 + * + * @param request 请求 + * @return String 请求的结果 + */ + private static String newCall(Request request) { + try (Response response = OK_HTTP_CLIENT.newCall(request).execute()) { + if (response.isSuccessful()) { + ResponseBody body = response.body(); + if (null == body) { + return ""; + } + return body.string(); + } else { + throw new XzException500("请求[" + request.url() + "]失败, 结果: " + response); + } + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500("请求[" + request.url() + "]异常"); + } + } + + /** + * 执行请求 + * + * @param request 请求 + * @return String 请求的结果 + */ + private static byte[] newCallResponseBytes(Request request) { + try (Response response = OK_HTTP_CLIENT.newCall(request).execute()) { + if (response.isSuccessful()) { + ResponseBody body = response.body(); + if (null == body) { + return "".getBytes(); + } + return body.bytes(); + } else { + throw new XzException500("请求[" + request.url() + "]失败, 结果: " + response); + } + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500("请求[" + request.url() + "]异常"); + } + } + + /** + * 将请求调用入队 + * + * @param call 调用对象 + * @param callback 回调响应处理对象 + */ + private static void enqueueNewCall(Call call, Callback callback) { + call.enqueue(callback); + } + + /** + * 封装 请求信息 + * + * @param url 请求路径 + * @param multipartBody 多文本参数 + * @param headerMap 对象头参数 + * @return Request 请求对象 + */ + private static Request buildRequest(String url, Map headerMap, MultipartBody multipartBody) { + Request.Builder builder = new Request.Builder(); + builder.url(url); + // 设置通用请求属性为默认浏览器编码类型 + builder.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + if (MapUtil.isNotEmpty(headerMap)) { + String value; + for (String key : headerMap.keySet()) { + value = headerMap.get(key); + if (StrUtil.isNotEmpty(value)) { + builder.header(key, value); + } + } + } + if (null != multipartBody) { + builder.post(multipartBody); + } + return builder.build(); + } + + /** + * 封装 请求信息 + * + * @param url 请求路径 + * @return Request 请求对象 + */ + private static Request buildRequest(String url) { + return buildRequest(url, null); + } + + /** + * 封装 请求信息 + * + * @param url 请求路径 + * @param headerMap 请求头参数集合 + * @return Request 请求对象 + */ + private static Request buildRequest(String url, Map headerMap) { + Request.Builder builder = new Request.Builder(); + builder.url(url); + // 设置通用请求属性为默认浏览器编码类型 + builder.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + if (MapUtil.isNotEmpty(headerMap)) { + String value; + for (String key : headerMap.keySet()) { + value = headerMap.get(key); + if (StrUtil.isNotEmpty(value)) { + builder.header(key, value); + } + } + } + return builder.build(); + } + + /** + * 拼接 Post 请求参数 + * + * @param body 参数 Json 字符串 + * @return String 文本 + */ + private static RequestBody createBody(String body) { + if (StrUtil.isEmpty(body)) { + return null; + } + return RequestBody.create(JSON, body); + } + + /** + * 拼接 Post 请求参数 + * + * @param paramMap 参数集合 + * @return String 文本 + */ + private static RequestBody createBody(Map paramMap) { + if (MapUtil.isEmpty(paramMap)) { + return null; + } + FormBody.Builder formBodyBuilder = new FormBody.Builder(); + String value; + for (String key : paramMap.keySet()) { + value = paramMap.get(key); + if (StrUtil.isNotEmpty(value)) { + formBodyBuilder.add(key, value); + } + } + return formBodyBuilder.build(); + } + + /** + * 封装 请求信息 + * + * @param url 请求路径 + * @param paramMap 请求参数集合 + * @param headerMap 对象头参数集合 + * @return Request 请求对象 + */ + private static Request buildRequest(String url, Map headerMap, Map paramMap) { + return buildRequest(url, headerMap, createBody(paramMap)); + } + + /** + * 封装 请求信息 + * + * @param url 请求路径 + * @param requestBody 请求参数 + * @param headerMap 对象头参数 + * @return Request 请求对象 + */ + private static Request buildRequest(String url, Map headerMap, RequestBody requestBody) { + Request.Builder builder = new Request.Builder(); + builder.url(url); + // 设置通用请求属性为默认浏览器编码类型 + builder.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + if (MapUtil.isNotEmpty(headerMap)) { + String value; + for (String key : headerMap.keySet()) { + value = headerMap.get(key); + if (StrUtil.isNotEmpty(value)) { + builder.header(key, value); + } + } + } + if (null != requestBody) { + builder.post(requestBody); + } + return builder.build(); + } + +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/OkHttpInterceptor.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/OkHttpInterceptor.java new file mode 100644 index 0000000..a34c0e0 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/okhttp/OkHttpInterceptor.java @@ -0,0 +1,16 @@ +package com.blossom.common.base.util.okhttp; + +import okhttp3.Interceptor; + +/** + * OKHTTP 拦截器, 由于 OKHTTP 为工具类, 拦截器不好拓展, 所以使用 SPI 方式由引用方实现 + * + * @author xzzz + */ +public interface OkHttpInterceptor extends Interceptor { + + /** + * 说明该拦截器实现的作用 + */ + void instructions(); +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCrypt.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCrypt.java new file mode 100644 index 0000000..38f414d --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCrypt.java @@ -0,0 +1,950 @@ +// Copyright (c) 2006 Damien Miller +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +package com.blossom.common.base.util.security; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * BCrypt implements OpenBSD-style Blowfish password hashing using + * the scheme described in "A Future-Adaptable Password Scheme" by + * Niels Provos and David Mazieres. + *

+ * This password hashing system tries to thwart off-line password + * cracking using a computationally-intensive hashing algorithm, + * based on Bruce Schneier's Blowfish cipher. The work factor of + * the algorithm is parameterised, so it can be increased as + * computers get faster. + *

+ * Usage is really simple. To hash a password for the first time, + * call the hashpw method with a random salt, like this: + *

+ * + * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ *
+ *

+ * To check whether a plaintext password matches one that has been + * hashed previously, use the checkpw method: + *

+ * + * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
+ *
+ *

+ * The gensalt() method takes an optional parameter (log_rounds) + * that determines the computational complexity of the hashing: + *

+ * + * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
+ *
+ *

+ * The amount of work increases exponentially (2**log_rounds), so + * each increment is twice as much work. The default log_rounds is + * 10, and the valid range is 4 to 31. + * + * @author Damien Miller + * @version 0.3 + */ +@SuppressWarnings("all") +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + }; + private static final int S_orig[] = { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + // bcrypt IV: "OrpheanBeholderScryDoubt" + static private final int bf_crypt_ciphertext[] = { + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 + }; + + // Table for Base64 encoding + static private final char base64_code[] = { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + // Table for Base64 decoding + static private final byte index_64[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, + -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + -1, -1, -1, -1, -1, -1, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, -1, -1, -1, -1, -1 + }; + public static final int MIN_LOG_ROUNDS = 4; + public static final int MAX_LOG_ROUNDS = 31; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note + * that this is not compatible with the standard MIME-base64 + * encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @param rs the destination buffer for the base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + static void encode_base64(byte d[], int len, StringBuilder rs) + throws IllegalArgumentException { + int off = 0; + int c1, c2; + + if (len <= 0 || len > d.length) { + throw new IllegalArgumentException("Invalid len"); + } + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + } + + /** + * Look up the 3 bits base64-encoded by the specified character, + * range-checking againt conversion table + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int) x < 0 || (int) x >= index_64.length) { + return -1; + } + return index_64[(int) x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a + * byte array. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + public static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + StringBuilder rs = new StringBuilder(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) { + throw new IllegalArgumentException ("Invalid maxolen"); + } + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) { + break; + } + o = (byte) (c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) { + break; + } + c3 = char64(s.charAt(off++)); + if (c3 == -1) { + break; + } + o = (byte) ((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) { + break; + } + c4 = char64(s.charAt(off++)); + o = (byte) ((c3 & 0x03) << 6); + o |= c4; + rs.append((char) o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) { + ret[off] = (byte) rs.charAt(off); + } + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as + * two 32-bit halves + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @param signp a "pointer" (as a one-entry array) to the + * cumulative flag for non-benign sign extension + * @return correct and buggy next word of material from data as int[2] + */ + private static int[] streamtowords(byte data[], int offp[], int signp[]) { + int i; + int words[] = { 0, 0 }; + int off = offp[0]; + int sign = signp[0]; + + for (i = 0; i < 4; i++) { + words[0] = (words[0] << 8) | (data[off] & 0xff); + words[1] = (words[1] << 8) | (int) data[off]; // sign extension bug + if (i > 0) { + sign |= words[1] & 0x80; + } + off = (off + 1) % data.length; + } + + offp[0] = off; + signp[0] = sign; + return words; + } + + /** + * Cycically extract a word of key material + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int signp[] = { 0 }; + return streamtowords(data, offp, signp)[0]; + } + + /** + * Cycically extract a word of key material, with sign-extension bug + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @return the next word of material from data + */ + private static int streamtoword_bug(byte data[], int offp[]) { + int signp[] = { 0 }; + return streamtowords(data, offp, signp)[1]; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = P_orig.clone(); + S = S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * @param key an array containing the key + * @param sign_ext_bug true to implement the 2x bug + * @param safety bit 16 is set when the safety measure is requested + */ + private void key(byte key[], boolean sign_ext_bug, int safety) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) { + if (!sign_ext_bug) { + P[i] = P[i] ^ streamtoword(key, koffp); + } else { + P[i] = P[i] ^ streamtoword_bug(key, koffp); + } + } + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by + * Provos and Mazieres in "A Future-Adaptable Password Scheme" + * https://www.openbsd.org/papers/bcrypt-paper.ps + * @param data salt information + * @param key password information + * @param sign_ext_bug true to implement the 2x bug + * @param safety bit 16 is set when the safety measure is requested + */ + private void ekskey(byte data[], byte key[], + boolean sign_ext_bug, int safety) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + int signp[] = { 0 }; // non-benign sign-extension flag + int diff = 0; // zero iff correct and buggy are same + + for (i = 0; i < plen; i++) { + int words[] = streamtowords(key, koffp, signp); + diff |= words[0] ^ words[1]; + P[i] = P[i] ^ words[sign_ext_bug ? 1 : 0]; + } + + int sign = signp[0]; + + /* + * At this point, "diff" is zero iff the correct and buggy algorithms produced + * exactly the same result. If so and if "sign" is non-zero, which indicates + * that there was a non-benign sign extension, this means that we have a + * collision between the correctly computed hash for this password and a set of + * passwords that could be supplied to the buggy algorithm. Our safety measure + * is meant to protect from such many-buggy to one-correct collisions, by + * deviating from the correct algorithm in such cases. Let's check for this. + */ + diff |= diff >> 16; /* still zero iff exact match */ + diff &= 0xffff; /* ditto */ + diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */ + sign <<= 9; /* move the non-benign sign extension flag to bit 16 */ + sign &= ~diff & safety; /* action needed? */ + + /* + * If we have determined that we need to deviate from the correct algorithm, + * flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but + * let's stick to it now. It came out of the approach we used above, and it's + * not any worse than any other choice we could make.) + * + * It is crucial that we don't do the same to the expanded key used in the main + * Eksblowfish loop. By doing it to only one of these two, we deviate from a + * state that could be directly specified by a password to the buggy algorithm + * (and to the fully correct one as well, but that's a side-effect). + */ + P[0] ^= sign; + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + public static long roundsForLogRounds(int log_rounds) { + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException("Bad number of rounds"); + } + return 1L << log_rounds; + } + + /** + * Perform the central password hashing step in the + * bcrypt scheme + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number + * of rounds of hashing to apply + * @param sign_ext_bug true to implement the 2x bug + * @param safety bit 16 is set when the safety measure is requested + * @return an array containing the binary hashed password + */ + private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, + boolean sign_ext_bug, int safety) { + int rounds, i, j; + int cdata[] = bf_crypt_ciphertext.clone(); + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException ("Bad number of rounds"); + } + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) { + throw new IllegalArgumentException ("Bad salt length"); + } + + init_key(); + ekskey(salt, password, sign_ext_bug, safety); + for (i = 0; i < rounds; i++) { + key(password, sign_ext_bug, safety); + key(salt, false, safety); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) { + encipher(cdata, j << 1); + } + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); + ret[j++] = (byte) (cdata[i] & 0xff); + } + return ret; + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated + * using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(String password, String salt) { + byte passwordb[]; + + passwordb = password.getBytes(StandardCharsets.UTF_8); + + return hashpw(passwordb, salt); + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * @param passwordb the password to hash, as a byte array + * @param salt the salt to hash with (perhaps generated + * using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(byte passwordb[], String salt) { + BCrypt B; + String real_salt; + byte saltb[], hashed[]; + char minor = (char) 0; + int rounds, off; + StringBuilder rs = new StringBuilder(); + + if (salt == null) { + throw new IllegalArgumentException("salt cannot be null"); + } + + int saltLength = salt.length(); + + if (saltLength < 28) { + throw new IllegalArgumentException("Invalid salt"); + } + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') { + throw new IllegalArgumentException ("Invalid salt version"); + } + if (salt.charAt(2) == '$') { + off = 3; + } else { + minor = salt.charAt(2); + if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') + || salt.charAt(3) != '$') { + throw new IllegalArgumentException ("Invalid salt revision"); + } + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') { + throw new IllegalArgumentException ("Missing salt rounds"); + } + + if (off == 4 && saltLength < 29) { + throw new IllegalArgumentException("Invalid salt"); + } + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + if (minor >= 'a') // add null terminator + { + passwordb = Arrays.copyOf(passwordb, passwordb.length + 1); + } + + B = new BCrypt(); + hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0); + + rs.append("$2"); + if (minor >= 'a') { + rs.append(minor); + } + rs.append("$"); + if (rounds < 10) { + rs.append("0"); + } + rs.append(rounds); + rs.append("$"); + encode_base64(saltb, saltb.length, rs); + encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param prefix the prefix value (default $2a) + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + * @exception IllegalArgumentException if prefix or log_rounds is invalid + */ + public static String gensalt(String prefix, int log_rounds, SecureRandom random) + throws IllegalArgumentException { + StringBuilder rs = new StringBuilder(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + if (!prefix.startsWith("$2") || + (prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' && + prefix.charAt(2) != 'b')) { + throw new IllegalArgumentException ("Invalid prefix"); + } + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException ("Invalid log_rounds"); + } + + random.nextBytes(rnd); + + rs.append("$2"); + rs.append(prefix.charAt(2)); + rs.append("$"); + if (log_rounds < 10) { + rs.append("0"); + } + rs.append(log_rounds); + rs.append("$"); + encode_base64(rnd, rnd.length, rs); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param prefix the prefix value (default $2a) + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + * @exception IllegalArgumentException if prefix or log_rounds is invalid + */ + public static String gensalt(String prefix, int log_rounds) + throws IllegalArgumentException { + return gensalt(prefix, log_rounds, new SecureRandom()); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + * @exception IllegalArgumentException if log_rounds is invalid + */ + public static String gensalt(int log_rounds, SecureRandom random) + throws IllegalArgumentException { + return gensalt("$2a", log_rounds, random); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + * @exception IllegalArgumentException if log_rounds is invalid + */ + public static String gensalt(int log_rounds) + throws IllegalArgumentException { + return gensalt(log_rounds, new SecureRandom()); + } + + public static String gensalt(String prefix) { + return gensalt(prefix, GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method, + * selecting a reasonable default for the number of hashing + * rounds to apply + * @return an encoded salt value + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Check that a plaintext password matches a previously hashed + * one + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + */ + public static boolean checkpw(String plaintext, String hashed) { + return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)); + } + + /** + * Check that a password (as a byte array) matches a previously hashed + * one + * @param passwordb the password to verify, as a byte array + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + * @since 5.3 + */ + public static boolean checkpw(byte[] passwordb, String hashed) { + return equalsNoEarlyReturn(hashed, hashpw(passwordb, hashed)); + } + + public static boolean equalsNoEarlyReturn(String a, String b) { + return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8)); + } +} + diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptPasswordEncoder.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptPasswordEncoder.java new file mode 100644 index 0000000..2437036 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptPasswordEncoder.java @@ -0,0 +1,170 @@ +package com.blossom.common.base.util.security; + +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.security.SecureRandom; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients + * can optionally supply a "version" ($2a, $2b, $2y) and a "strength" (a.k.a. log rounds in BCrypt) + * and a SecureRandom instance. The larger the strength parameter the more work will have to be done + * (exponentially) to hash the passwords. The default value is 10. + * + * @author Dave Syer + */ +@SuppressWarnings("all") +public class BCryptPasswordEncoder { + private Pattern BCRYPT_PATTERN = Pattern + .compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); + private final Log logger = LogFactory.getLog(getClass()); + + private final int strength; + private final BCryptVersion version; + + private final SecureRandom random; + + + public BCryptPasswordEncoder() { + this(-1); + } + + /** + * @param strength the log rounds to use, between 4 and 31 + */ + public BCryptPasswordEncoder(int strength) { + this(strength, null); + } + + /** + * @param version the version of bcrypt, can be 2a,2b,2y + */ + public BCryptPasswordEncoder(BCryptVersion version) { + this(version, null); + } + + /** + * @param version the version of bcrypt, can be 2a,2b,2y + * @param random the secure random instance to use + */ + public BCryptPasswordEncoder(BCryptVersion version, SecureRandom random) { + this(version, -1, random); + } + + /** + * @param strength the log rounds to use, between 4 and 31 + * @param random the secure random instance to use + */ + public BCryptPasswordEncoder(int strength, SecureRandom random) { + this(BCryptVersion.$2A, strength, random); + } + + /** + * @param version the version of bcrypt, can be 2a,2b,2y + * @param strength the log rounds to use, between 4 and 31 + */ + public BCryptPasswordEncoder(BCryptVersion version, int strength) { + this(version, strength, null); + } + + /** + * @param version the version of bcrypt, can be 2a,2b,2y + * @param strength the log rounds to use, between 4 and 31 + * @param random the secure random instance to use + */ + public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) { + if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { + throw new IllegalArgumentException("Bad strength"); + } + this.version = version; + this.strength = strength == -1 ? 10 : strength; + this.random = random; + } + + public String encode(CharSequence rawPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("rawPassword cannot be null"); + } + + String salt; + if (random != null) { + salt = BCrypt.gensalt(version.getVersion(), strength, random); + } else { + salt = BCrypt.gensalt(version.getVersion(), strength); + } + return BCrypt.hashpw(rawPassword.toString(), salt); + } + + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("rawPassword cannot be null"); + } + + if (encodedPassword == null || encodedPassword.length() == 0) { + logger.warn("Empty encoded password"); + return false; + } + + if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { + logger.warn("Encoded password does not look like BCrypt"); + return false; + } + + return BCrypt.checkpw(rawPassword.toString(), encodedPassword); + } + + public boolean upgradeEncoding(String encodedPassword) { + if (encodedPassword == null || encodedPassword.length() == 0) { + logger.warn("Empty encoded password"); + return false; + } + + Matcher matcher = BCRYPT_PATTERN.matcher(encodedPassword); + if (!matcher.matches()) { + throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword); + } + else { + int strength = Integer.parseInt(matcher.group(2)); + return strength < this.strength; + } + } + + /** + * Stores the default bcrypt version for use in configuration. + * + * @author Lin Feng + */ + public enum BCryptVersion { + $2A("$2a"), + $2Y("$2y"), + $2B("$2b"); + + private final String version; + + BCryptVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptUtil.java new file mode 100644 index 0000000..dede7e6 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/BCryptUtil.java @@ -0,0 +1,69 @@ +package com.blossom.common.base.util.security; + +/** + * BCrypt 加密, 每次加密密文不同, 需要原文和密文进行比较 + * + * @author xzzz + * @since 0.0.1 + */ +@SuppressWarnings("all") +public class BCryptUtil { + + private static BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + + /** + * 加密 + * + * @param plaintext 明文 + * @param salt 盐值 + * @return 密文 + */ + public static String encode(CharSequence plaintext, String salt) { + return encode(plaintext + salt); + } + + /** + * 加密 + * + * @param plaintext 明文 + * @return 密文 + */ + public static String encode(CharSequence plaintext) { + return bCryptPasswordEncoder.encode(plaintext); + } + + /** + * 匹配密文与明文 + * + * @param plaintext 密码明文 + * @param salt 盐值 + * @param ciphetext 密码密文 + * @return 密码是否匹配 + */ + public static boolean matches(CharSequence plaintext, String salt, String ciphetext) { + return matches(plaintext + salt, ciphetext); + } + + /** + * 匹配密文与明文 + * + * @param plaintext 密码明文 + * @param ciphetext 密码密文 + * @return 密码是否匹配 + */ + public static boolean matches(CharSequence plaintext, String ciphetext) { + return bCryptPasswordEncoder.matches(plaintext, ciphetext); + } + + public static void main(String[] args) { + System.out.println("begin"); + String content = "abc"; + // admin 存储在数据库中都密码 + String adminPassword = "$2a$10$gnX5RJhJ/mKbR4slN/8aWOor0/wX1UafXtrbFJyliYa6B7eCyq1wa"; + Long startBCryptMatches = System.currentTimeMillis(); + // 方法1:判断密码是否正确,row不需要是加密后都密文 + boolean correct1 = BCryptUtil.matches(content, adminPassword); + Long endBCryptMatches = System.currentTimeMillis(); + System.out.println("解密耗时:" + (endBCryptMatches - startBCryptMatches)); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/Base64Util.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/Base64Util.java new file mode 100644 index 0000000..2aaf43a --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/Base64Util.java @@ -0,0 +1,20 @@ +package com.blossom.common.base.util.security; + +import java.io.IOException; +import java.util.Base64; + +/** + * 消息编码算法 + * @author xzzz + * @since 0.0.1 + */ +public class Base64Util { + + public static String encrypt(byte[] data) { + return Base64.getEncoder().encodeToString(data); + } + + public static String decrypt(String data) throws IOException { + return new String(Base64.getDecoder().decode(data)); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/RSAUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/RSAUtil.java new file mode 100644 index 0000000..37529af --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/RSAUtil.java @@ -0,0 +1,436 @@ +package com.blossom.common.base.util.security; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * RSA 是非对称的密码算法,密钥分公钥和私钥 + *

公钥用来加密,或验签 + *

私钥用于解密,或加签 + * + * @since 0.0.1 + * @author xzzz + */ +public class RSAUtil { + + /** + * 密钥算法 + */ + public static final String KEY_ALGORITHM = "RSA"; + + /** + * 签名算法:MD2withRSA, SHA1WithRSA, SHA256withRSA, SHA384withRSA, SHA512withRSA + */ + public static final String SIGN_ALGORITHM = "MD5withRSA"; + + public static final String PUBLIC_KEY = "publicKey"; + public static final String PRIVATE_KEY = "privateKey"; + + + public static void main(String[] args) { + // 生成公钥私钥对 + Map map = RSAUtil.getKeyMap(String.valueOf(System.currentTimeMillis())); + // 公钥, 提供给外部 + String publicKey = map.get(RSAUtil.PUBLIC_KEY); + // 私钥, 服务器使用 + String privateKey = map.get(RSAUtil.PRIVATE_KEY); + + // 加密 + byte[] byteArr = RSAUtil.encrypt("test123!!!!", publicKey); + System.out.println("加密后的密文:\n" + Arrays.toString(byteArr)); + System.out.println("解密后的明文:\n" + RSAUtil.decrypt(byteArr, privateKey)); + + // 签名 验证签名 + String sign = RSAUtil.sign("sign_test_123", privateKey); + System.out.println("使用私钥进行签名:" + sign); + System.out.println("使用公钥验证签名:" + RSAUtil.verifySign("sign_test_123", sign, publicKey)); + } + + /** + * 获取秘钥对 + * + * @param seedStr 种子字符串 + * @return Map 秘钥集合 + */ + public static Map getKeyMap(String seedStr) { + return getKeyMap(seedStr, KEY_ALGORITHM); + } + + /** + * 获取秘钥对 + * + * @param seedStr 种子字符串 + * @param algorithm 算法 + * @return Map 秘钥集合 + */ + public static Map getKeyMap(String seedStr, String algorithm) { + Map keyMap = new HashMap<>(4); + KeyPair keyPair = getKeyPair(seedStr, algorithm); + keyMap.put(PUBLIC_KEY, getPublicKey(keyPair)); + keyMap.put(PRIVATE_KEY, getPrivateKey(keyPair)); + return keyMap; + } + + /** + * 将数据使用公钥加密 + * + * @param content 待加密内容 + * @param publicKeyStr 公钥字符串 + * @return byte[] 加密后的数据 + */ + public static byte[] encrypt(String content, String publicKeyStr) { + return encrypt(content, publicKeyStr, KEY_ALGORITHM, StandardCharsets.UTF_8); + } + + /** + * 将数据使用公钥加密 + * + * @param content 待加密的数据 + * @param publicKeyStr 公钥字符串 + * @param algorithm 算法 + * @param charset 字符集 + * @return String 加密后的数据 + */ + public static byte[] encrypt(String content, String publicKeyStr, String algorithm, Charset charset) { + if (null == charset) { + charset = StandardCharsets.UTF_8; + } + PublicKey publicKey = getPublicKey(publicKeyStr, algorithm); + return publicKeyEncrypt(content.getBytes(charset), publicKey, algorithm); + } + + /** + * 使用私钥数据进行解密 + * + * @param content 待解密的数据 + * @param privateKeyStr 私钥字符串 + * @return String 解密后的数据 + */ + public static String decrypt(byte[] content, String privateKeyStr) { + return decrypt(content, privateKeyStr, KEY_ALGORITHM, StandardCharsets.UTF_8); + } + + /** + * 使用私钥数据进行解密 + * + * @param content 待解密的数据 + * @param privateKeyStr 私钥字符串 + * @param algorithm 算法 + * @param charset 字符集 + * @return String 解密后的数据 + */ + public static String decrypt(byte[] content, String privateKeyStr, String algorithm, Charset charset) { + if (null == charset) { + charset = StandardCharsets.UTF_8; + } + PrivateKey privateKey = getPrivateKey(privateKeyStr); + byte[] byteArr = privateKeyDecrypt(content, privateKey, algorithm); + return new String(byteArr, charset); + } + + /** + * 生成密钥对:密钥对中包含公钥和私钥 + * + * @param seedStr 种子字符串 + * @return KeyPair 包含 RSA 公钥与私钥的 keyPair + */ + private static KeyPair getKeyPair(String seedStr) { + return getKeyPair(seedStr, KEY_ALGORITHM); + } + + /** + * 生成密钥对:密钥对中包含公钥和私钥 + * + * @param seedStr 种子字符串 + * @param algorithm 算法 + * @return KeyPair 包含 RSA 公钥与私钥的 keyPair + */ + private static KeyPair getKeyPair(String seedStr, String algorithm) { + if (null == algorithm || "".equals(algorithm)) { + algorithm = KEY_ALGORITHM; + } + + try { + // 获得RSA密钥对的生成器实例 + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm); + if (null == seedStr || "".equals(seedStr)) { + seedStr = String.valueOf(System.currentTimeMillis()); + } + // 说的一个安全的随机数 + SecureRandom secureRandom = new SecureRandom(seedStr.getBytes(StandardCharsets.UTF_8)); + // 初始化密钥对生成器,密钥大小为512、1024、2048 + keyPairGenerator.initialize(512, secureRandom); + // 获得密钥对 + return keyPairGenerator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("生成秘钥对时,不存在 %s 这种算法", algorithm)); + } catch (Exception e) { + throw new RuntimeException("生成秘钥对失败", e); + } + } + + /** + * 获取公钥 (并进行 Base64 编码,返回一个 Base64 编码后的字符串) + * + * @param keyPair 秘钥对 + * @return 返回一个 Base64 编码后的公钥字符串 + */ + private static String getPublicKey(KeyPair keyPair) { + PublicKey publicKey = keyPair.getPublic(); + byte[] bytes = publicKey.getEncoded(); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * 获取私钥(并进行 Base64 编码,返回一个 Base64 编码后的字符串) + * + * @param keyPair 秘钥对 + * @return 返回一个 Base64 编码后的私钥字符串 + */ + private static String getPrivateKey(KeyPair keyPair) { + PrivateKey privateKey = keyPair.getPrivate(); + byte[] bytes = privateKey.getEncoded(); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * 将 Base64 编码后的公钥转换成 PublicKey 对象 + * + * @param publicKeyStr 公钥字符串 + * @return PublicKey 公钥对象 + */ + public static PublicKey getPublicKey(String publicKeyStr) { + return getPublicKey(publicKeyStr, KEY_ALGORITHM); + } + + /** + * 将 Base64 编码后的公钥转换成 PublicKey 对象 + * + * @param publicKeyStr 公钥字符串 + * @param algorithm 算法 + * @return PublicKey 公钥对象 + */ + private static PublicKey getPublicKey(String publicKeyStr, String algorithm) { + if (null == algorithm || "".equals(algorithm)) { + algorithm = KEY_ALGORITHM; + } + try { + byte[] bytes = Base64.getDecoder().decode(publicKeyStr); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("获取公钥时,不存在 %s 这种算法", algorithm)); + } catch (InvalidKeySpecException e) { + throw new RuntimeException(String.format("获取公钥时,公钥 %s 非法", publicKeyStr)); + } catch (Exception e) { + throw new RuntimeException("获取公钥失败", e); + } + } + + /** + * 将 Base64 编码后的私钥转换成 PrivateKey 对象 + * + * @param privateKeyStr 私钥字符串 + * @return PrivateKey 私钥对象 + */ + public static PrivateKey getPrivateKey(String privateKeyStr) { + return getPrivateKey(privateKeyStr, KEY_ALGORITHM); + } + + /** + * 将 Base64 编码后的私钥转换成 PrivateKey 对象 + * + * @param privateKeyStr 私钥字符串 + * @return PrivateKey 私钥对象 + */ + private static PrivateKey getPrivateKey(String privateKeyStr, String algorithm) { + if (null == algorithm || "".equals(algorithm)) { + algorithm = KEY_ALGORITHM; + } + try { + byte[] bytes = Base64.getDecoder().decode(privateKeyStr); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("获取公钥时,不存在 %s 这种算法", algorithm)); + } catch (InvalidKeySpecException e) { + throw new RuntimeException(String.format("获取公钥时,公钥 %s 非法", privateKeyStr)); + } catch (Exception e) { + throw new RuntimeException("获取公钥失败", e); + } + } + + /** + * 公钥加密 + * + * @param content 待加密的内容 byte[] + * @param publicKey 加密所需的公钥对象 PublicKey + * @return 加密后的字节数组 byte[] + */ + private static byte[] publicKeyEncrypt(byte[] content, PublicKey publicKey) { + return publicKeyEncrypt(content, publicKey, KEY_ALGORITHM); + } + + /** + * 公钥加密 + * + * @param content 待加密的内容 byte[] + * @param publicKey 加密所需的公钥对象 PublicKey + * @param algorithm 算法 + * @return 加密后的字节数组 byte[] + */ + private static byte[] publicKeyEncrypt(byte[] content, PublicKey publicKey, String algorithm) { + if (null == algorithm || "".equals(algorithm)) { + algorithm = KEY_ALGORITHM; + } + try { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(content); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("公钥加密时,不存在 %s 这种算法", algorithm)); + } catch (NoSuchPaddingException | BadPaddingException e) { + throw new RuntimeException("公钥加密时,传入的待加密内容无效"); + } catch (IllegalBlockSizeException e) { + throw new RuntimeException("公钥加密时,传入的待加密内容长度不会"); + } catch (InvalidKeyException e) { + throw new RuntimeException(String.format("公钥加密时,秘钥 %s 无效", publicKey)); + } + } + + /** + * 私钥解密 + * + * @param content 待解密的内容 byte[] + * @param privateKey 解密需要的私钥对象 PrivateKey + * @return 解密后的字节数组 byte[] + */ + private static byte[] privateKeyDecrypt(byte[] content, PrivateKey privateKey) { + return privateKeyDecrypt(content, privateKey, KEY_ALGORITHM); + } + + /** + * 私钥解密 + * + * @param content 待解密的内容 byte[] + * @param privateKey 解密需要的私钥对象 PrivateKey + * @param algorithm 算法 + * @return 解密后的字节数组 byte[] + */ + private static byte[] privateKeyDecrypt(byte[] content, PrivateKey privateKey, String algorithm) { + if (null == algorithm || "".equals(algorithm)) { + algorithm = KEY_ALGORITHM; + } + try { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(content); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("私钥解密时,不存在 %s 这种算法", algorithm)); + } catch (NoSuchPaddingException | BadPaddingException e) { + throw new RuntimeException("私钥解密时,传入的待加密内容无效"); + } catch (IllegalBlockSizeException e) { + throw new RuntimeException("私钥解密时,传入的待加密内容长度不会"); + } catch (InvalidKeyException e) { + throw new RuntimeException(String.format("私钥解密时,秘钥 %s 无效", privateKey)); + } + } + + /** + * 签名 + * + * @param content 内容 + * @param privateKey 私钥字符串 + * @return String 签名数据 + */ + public static String sign(String content, String privateKey) { + return sign(content, privateKey, StandardCharsets.UTF_8, SIGN_ALGORITHM); + } + + /** + * 签名 + * + * @param content 内容 + * @param privateKey 私钥字符串 + * @param charset 字符集 + * @param algorithm 签名算法 + * @return String 签名数据 + */ + public static String sign(String content, String privateKey, Charset charset, String algorithm) { + try { + PrivateKey priKey = RSAUtil.getPrivateKey(privateKey); + Signature signature = Signature.getInstance(algorithm); + + signature.initSign(priKey); + if (charset != null) { + signature.update(content.getBytes(charset)); + } else { + signature.update(content.getBytes()); + } + byte[] signed = signature.sign(); + return Base64.getEncoder().encodeToString(signed); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("签名时,不存在 %s 这种算法", algorithm)); + } catch (SignatureException e) { + throw new RuntimeException("签名时,获取签名异常", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("签名时,秘钥无效", e); + } + } + + /** + * 验证签名 + * + * @param content 内容 + * @param sign 签名 + * @param publicKey 公钥 + * @return boolean true:成功、false:失败 + */ + public static boolean verifySign(String content, String sign, String publicKey) { + return verifySign(content, sign, publicKey, StandardCharsets.UTF_8, SIGN_ALGORITHM); + } + + /** + * 验证签名 + * + * @param content 内容 + * @param sign 签名 + * @param publicKey 公钥 + * @param charset 字符集 + * @param algorithm 签名算法 + * @return boolean true:成功、false:失败 + */ + public static boolean verifySign(String content, String sign, String publicKey, Charset charset, String algorithm) { + try { + PublicKey pubKey = RSAUtil.getPublicKey(publicKey); + Signature signature = Signature.getInstance(algorithm); + signature.initVerify(pubKey); + if (charset != null) { + signature.update(content.getBytes(charset)); + } else { + signature.update(content.getBytes()); + } + return signature.verify(Base64.getDecoder().decode(sign.getBytes())); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("验证签名时,不存在 %s 这种算法", algorithm)); + } catch (SignatureException e) { + throw new RuntimeException("验证签名时,获取签名异常", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("签名时,秘钥无效", e); + } + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SHA256Util.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SHA256Util.java new file mode 100644 index 0000000..f768b7b --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SHA256Util.java @@ -0,0 +1,45 @@ +package com.blossom.common.base.util.security; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * SHA256 本质是 hash 算法,结果不可逆 + * + * @author xzzz + */ +public class SHA256Util { + + /** + * 加密 + * + * @param str 被加密的内容 + * @return 返回密文 + */ + public static String encode(String str) { + MessageDigest messageDigest; + String encodeStr = ""; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); + encodeStr = byte2Hex(messageDigest.digest()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return encodeStr; + } + + private static String byte2Hex(byte[] bytes) { + StringBuilder stringBuffer = new StringBuilder(); + String temp; + for (byte aByte : bytes) { + temp = Integer.toHexString(aByte & 0xFF); + if (temp.length() == 1) { + stringBuffer.append("0"); + } + stringBuffer.append(temp); + } + return stringBuffer.toString(); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SaltUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SaltUtil.java new file mode 100644 index 0000000..17837a8 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/security/SaltUtil.java @@ -0,0 +1,27 @@ +package com.blossom.common.base.util.security; + +import java.security.SecureRandom; + +/** + * 获取随机盐值 + * + * @author xzzz + * @since 0.0.1 + */ +public class SaltUtil { + + /** + * 安全随机类 + */ + private static final SecureRandom RANDOM = new SecureRandom(); + + /** + * 获取随机盐值 + * @return 加盐 + */ + public static String randomSalt() { + byte[] bytes = new byte[15]; + RANDOM.nextBytes(bytes); + return Base64Util.encrypt(bytes); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spi/ServiceLoaderUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spi/ServiceLoaderUtil.java new file mode 100644 index 0000000..a0ed77d --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spi/ServiceLoaderUtil.java @@ -0,0 +1,21 @@ +package com.blossom.common.base.util.spi; + +import java.util.ServiceLoader; + +/** + * 根据接口或抽象类获取该类的全部实现类 + * + * @author xzzz + */ +public final class ServiceLoaderUtil { + + private static final String CLASSLOADER_DEFAULT = "default"; + private static final String CLASSLOADER_CONTEXT = "context"; + private static final String CLASSLOADER_PATH = ""; + + public static ServiceLoader getServiceLoader(Class clazz) { + return ServiceLoader.load(clazz, clazz.getClassLoader()); + } + + private ServiceLoaderUtil() {} +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/AntPathMatcherUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/AntPathMatcherUtil.java new file mode 100644 index 0000000..68fd239 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/AntPathMatcherUtil.java @@ -0,0 +1,28 @@ +package com.blossom.common.base.util.spring; + +import org.springframework.util.AntPathMatcher; + +/** + * 路径匹配工具类,依赖 Spring 的工具类 + * + * @author xzzz + * @since 0.0.1 + */ +public class AntPathMatcherUtil { + + private static AntPathMatcher antPathMatcher = new AntPathMatcher(); + + public static AntPathMatcher getAntPathMatcher() { + return antPathMatcher; + } + + /** + * 匹配请求路径是否在模板中 + * @param pattern 模板路径 + * @param path 路径 + * @return true:在模板中, false:不在模板中 + */ + public static boolean match (String pattern,String path) { + return antPathMatcher.match(pattern,path); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/SpringUtil.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/SpringUtil.java new file mode 100644 index 0000000..fb2dbc8 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/spring/SpringUtil.java @@ -0,0 +1,134 @@ +package com.blossom.common.base.util.spring; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotNull; + +/** + * Spring 环境配置工具类 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +@Component +@Lazy(false) +public class SpringUtil implements EnvironmentAware, ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 实现ApplicationContextAware接口, 注入Context到静态变量中. + */ + @Override + public void setApplicationContext(@NotNull ApplicationContext applicationContext) { + SpringUtil.applicationContext = applicationContext; + } + + /** + * 从静态变量 applicationContext 中取得 Bean, 自动转型为所赋值对象的类型. + */ + public static Object getBean(String name) { + return applicationContext.getBean(name); + } + + /** + * 从静态变量 applicationContext 中取得 Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + return applicationContext.getBean(requiredType); + } + + /** + * 发布事件 + */ + public static void publishEvent(ApplicationEvent event) { + if (applicationContext == null) { + return; + } + applicationContext.publishEvent(event); + } + + /** + * 实现DisposableBean接口, 在Context关闭时清理静态变量. + */ + @Override + public void destroy() { + try { + applicationContext = null; + } catch (Exception ignored) { + } + } + + private static Environment environment; + + /** + * 当前应用名称 + */ + public static final String APP_NAME = "spring.application.name"; + /** + * 当前环境 + */ + public static final String PROFILE_ACTION = "spring.profiles.active"; + /** + * 应用基础路径 + */ + public static final String SERVLET_CONTEXT_PATH = "server.servlet.context-path"; + /** + * 端口 + */ + public static final String SERVER_PORT = "server.port"; + /** + * 本项目说明 + */ + private static final String ENV_LEARNING_DESCRIPTION = "learning.description"; + /** + * 本项目框架版本 + */ + private static final String ENV_LEARNING_FRAMEWORK_VERSION = "learning.frameworkVersion"; + + @Override + public void setEnvironment(@NotNull Environment environment) { + SpringUtil.environment = environment; + } + + public static String get(String key) { + return environment.getProperty(key); + } + + public static String getPort() { + return environment.getProperty(SERVER_PORT); + } + + public static String getDesc() { + return environment.getProperty(ENV_LEARNING_DESCRIPTION); + } + + public static String getFrameworkVersion() { + return environment.getProperty(ENV_LEARNING_FRAMEWORK_VERSION); + } + + public static String getAppName() { + return getApplicationContext().getEnvironment().getProperty(APP_NAME); + } + + public static String getProfileAction() { + return getApplicationContext().getEnvironment().getProperty(PROFILE_ACTION); + } + + public static String getServletContextPath() { + return getApplicationContext().getEnvironment().getProperty(SERVLET_CONTEXT_PATH); + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/thread/NamedThreadFactory.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/thread/NamedThreadFactory.java new file mode 100644 index 0000000..0b59278 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/thread/NamedThreadFactory.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.common.base.util.thread; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * InternalThreadFactory. + */ +public class NamedThreadFactory implements ThreadFactory { + + protected static final AtomicInteger POOL_SEQ = new AtomicInteger(1); + + protected final AtomicInteger mThreadNum = new AtomicInteger(1); + + protected final String mPrefix; + + protected final boolean mDaemon; + + protected final ThreadGroup mGroup; + + public NamedThreadFactory() { + this("pool-" + POOL_SEQ.getAndIncrement(), false); + } + + public NamedThreadFactory(String prefix) { + this(prefix, false); + } + + public NamedThreadFactory(String prefix, boolean daemon) { + mPrefix = prefix + "-thread-"; + mDaemon = daemon; + SecurityManager s = System.getSecurityManager(); + mGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup(); + } + + @Override + public Thread newThread(Runnable runnable) { + String name = mPrefix + mThreadNum.getAndIncrement(); + Thread ret = new Thread(mGroup, runnable, name, 0); + ret.setDaemon(mDaemon); + return ret; + } + + public ThreadGroup getThreadGroup() { + return mGroup; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThread.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThread.java new file mode 100644 index 0000000..b667d47 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThread.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blossom.common.base.util.threadlocal; + +/** + * InternalThread + */ +public class InternalThread extends Thread { + + private InternalThreadLocalMap threadLocalMap; + + public InternalThread() { + } + + public InternalThread(Runnable target) { + super(target); + } + + public InternalThread(ThreadGroup group, Runnable target) { + super(group, target); + } + + public InternalThread(String name) { + super(name); + } + + public InternalThread(ThreadGroup group, String name) { + super(group, name); + } + + public InternalThread(Runnable target, String name) { + super(target, name); + } + + public InternalThread(ThreadGroup group, Runnable target, String name) { + super(group, target, name); + } + + public InternalThread(ThreadGroup group, Runnable target, String name, long stackSize) { + super(group, target, name, stackSize); + } + + /** + * Returns the internal data structure that keeps the threadLocal variables bound to this thread. + * Note that this method is for internal use only, and thus is subject to change at any time. + */ + public final InternalThreadLocalMap threadLocalMap() { + return threadLocalMap; + } + + /** + * Sets the internal data structure that keeps the threadLocal variables bound to this thread. + * Note that this method is for internal use only, and thus is subject to change at any time. + */ + public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) { + this.threadLocalMap = threadLocalMap; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocal.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocal.java new file mode 100644 index 0000000..1cb2629 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocal.java @@ -0,0 +1,198 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.blossom.common.base.util.threadlocal; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * InternalThreadLocal + * A special variant of {@link ThreadLocal} that yields higher access performance when accessed from a + * {@link InternalThread}. + *

+ * Internally, a {@link InternalThread} uses a constant index in an array, instead of using hash code and hash table, + * to look for a variable. Although seemingly very subtle, it yields slight performance advantage over using a hash + * table, and it is useful when accessed frequently. + *

+ * This design is learning from {@see io.netty.util.concurrent.FastThreadLocal} which is in Netty. + */ +public class InternalThreadLocal { + + private static final int VARIABLES_TO_REMOVE_INDEX = InternalThreadLocalMap.nextVariableIndex(); + + private final int index; + + public InternalThreadLocal() { + index = InternalThreadLocalMap.nextVariableIndex(); + } + + /** + * Removes all {@link InternalThreadLocal} variables bound to the current thread. This operation is useful when you + * are in a container environment, and you don't want to leave the thread local variables in the threads you do not + * manage. + */ + @SuppressWarnings("unchecked") + public static void removeAll() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap == null) { + return; + } + + try { + Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX); + if (v != null && v != InternalThreadLocalMap.UNSET) { + Set> variablesToRemove = (Set>) v; + InternalThreadLocal[] variablesToRemoveArray = + variablesToRemove.toArray(new InternalThreadLocal[variablesToRemove.size()]); + for (InternalThreadLocal tlv : variablesToRemoveArray) { + tlv.remove(threadLocalMap); + } + } + } finally { + InternalThreadLocalMap.remove(); + } + } + + /** + * Returns the number of thread local variables bound to the current thread. + */ + public static int size() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap == null) { + return 0; + } else { + return threadLocalMap.size(); + } + } + + public static void destroy() { + InternalThreadLocalMap.destroy(); + } + + @SuppressWarnings("unchecked") + private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, InternalThreadLocal variable) { + Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX); + Set> variablesToRemove; + if (v == InternalThreadLocalMap.UNSET || v == null) { + variablesToRemove = Collections.newSetFromMap(new IdentityHashMap, Boolean>()); + threadLocalMap.setIndexedVariable(VARIABLES_TO_REMOVE_INDEX, variablesToRemove); + } else { + variablesToRemove = (Set>) v; + } + + variablesToRemove.add(variable); + } + + @SuppressWarnings("unchecked") + private static void removeFromVariablesToRemove(InternalThreadLocalMap threadLocalMap, InternalThreadLocal variable) { + + Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX); + + if (v == InternalThreadLocalMap.UNSET || v == null) { + return; + } + + Set> variablesToRemove = (Set>) v; + variablesToRemove.remove(variable); + } + + /** + * Returns the current value for the current thread + */ + @SuppressWarnings("unchecked") + public final V get() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; + } + + return initialize(threadLocalMap); + } + + private V initialize(InternalThreadLocalMap threadLocalMap) { + V v = null; + try { + v = initialValue(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + threadLocalMap.setIndexedVariable(index, v); + addToVariablesToRemove(threadLocalMap, this); + return v; + } + + /** + * Sets the value for the current thread. + */ + public final void set(V value) { + if (value == null || value == InternalThreadLocalMap.UNSET) { + remove(); + } else { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); + if (threadLocalMap.setIndexedVariable(index, value)) { + addToVariablesToRemove(threadLocalMap, this); + } + } + } + + /** + * Sets the value to uninitialized; a proceeding call to get() will trigger a call to initialValue(). + */ + @SuppressWarnings("unchecked") + public final void remove() { + remove(InternalThreadLocalMap.getIfSet()); + } + + /** + * Sets the value to uninitialized for the specified thread local map; + * a proceeding call to get() will trigger a call to initialValue(). + * The specified thread local map must be for the current thread. + */ + @SuppressWarnings("unchecked") + public final void remove(InternalThreadLocalMap threadLocalMap) { + if (threadLocalMap == null) { + return; + } + + Object v = threadLocalMap.removeIndexedVariable(index); + removeFromVariablesToRemove(threadLocalMap, this); + + if (v != InternalThreadLocalMap.UNSET) { + try { + onRemoval((V) v); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Returns the initial value for this thread-local variable. + */ + protected V initialValue() throws Exception { + return null; + } + + /** + * Invoked when this thread local variable is removed by {@link #remove()}. + */ + protected void onRemoval(@SuppressWarnings("unused") V value) throws Exception { + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocalMap.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocalMap.java new file mode 100644 index 0000000..2a102fb --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/InternalThreadLocalMap.java @@ -0,0 +1,167 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.blossom.common.base.util.threadlocal; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The internal data structure that stores the threadLocal variables for Netty and all {@link InternalThread}s. + * Note that this class is for internal use only. Use {@link InternalThread} + * unless you know what you are doing. + */ +public final class InternalThreadLocalMap { + + private Object[] indexedVariables; + + private static ThreadLocal slowThreadLocalMap = new ThreadLocal(); + + private static final AtomicInteger NEXT_INDEX = new AtomicInteger(); + + public static final Object UNSET = new Object(); + + public static InternalThreadLocalMap getIfSet() { + Thread thread = Thread.currentThread(); + if (thread instanceof InternalThread) { + return ((InternalThread) thread).threadLocalMap(); + } + return slowThreadLocalMap.get(); + } + + public static InternalThreadLocalMap get() { + Thread thread = Thread.currentThread(); + if (thread instanceof InternalThread) { + return fastGet((InternalThread) thread); + } + return slowGet(); + } + + public static void remove() { + Thread thread = Thread.currentThread(); + if (thread instanceof InternalThread) { + ((InternalThread) thread).setThreadLocalMap(null); + } else { + slowThreadLocalMap.remove(); + } + } + + public static void destroy() { + slowThreadLocalMap = null; + } + + public static int nextVariableIndex() { + int index = NEXT_INDEX.getAndIncrement(); + if (index < 0) { + NEXT_INDEX.decrementAndGet(); + throw new IllegalStateException("Too many thread-local indexed variables"); + } + return index; + } + + public static int lastVariableIndex() { + return NEXT_INDEX.get() - 1; + } + + private InternalThreadLocalMap() { + indexedVariables = newIndexedVariableTable(); + } + + public Object indexedVariable(int index) { + Object[] lookup = indexedVariables; + return index < lookup.length ? lookup[index] : UNSET; + } + + /** + * @return {@code true} if and only if a new thread-local variable has been created + */ + public boolean setIndexedVariable(int index, Object value) { + Object[] lookup = indexedVariables; + if (index < lookup.length) { + Object oldValue = lookup[index]; + lookup[index] = value; + return oldValue == UNSET; + } else { + expandIndexedVariableTableAndSet(index, value); + return true; + } + } + + public Object removeIndexedVariable(int index) { + Object[] lookup = indexedVariables; + if (index < lookup.length) { + Object v = lookup[index]; + lookup[index] = UNSET; + return v; + } else { + return UNSET; + } + } + + public int size() { + int count = 0; + for (Object o : indexedVariables) { + if (o != UNSET) { + ++count; + } + } + + //the fist element in `indexedVariables` is a set to keep all the InternalThreadLocal to remove + //look at method `addToVariablesToRemove` + return count - 1; + } + + private static Object[] newIndexedVariableTable() { + Object[] array = new Object[32]; + Arrays.fill(array, UNSET); + return array; + } + + private static InternalThreadLocalMap fastGet(InternalThread thread) { + InternalThreadLocalMap threadLocalMap = thread.threadLocalMap(); + if (threadLocalMap == null) { + thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap()); + } + return threadLocalMap; + } + + private static InternalThreadLocalMap slowGet() { + ThreadLocal slowThreadLocalMap = InternalThreadLocalMap.slowThreadLocalMap; + InternalThreadLocalMap ret = slowThreadLocalMap.get(); + if (ret == null) { + ret = new InternalThreadLocalMap(); + slowThreadLocalMap.set(ret); + } + return ret; + } + + private void expandIndexedVariableTableAndSet(int index, Object value) { + Object[] oldArray = indexedVariables; + final int oldCapacity = oldArray.length; + int newCapacity = index; + newCapacity |= newCapacity >>> 1; + newCapacity |= newCapacity >>> 2; + newCapacity |= newCapacity >>> 4; + newCapacity |= newCapacity >>> 8; + newCapacity |= newCapacity >>> 16; + newCapacity++; + + Object[] newArray = Arrays.copyOf(oldArray, newCapacity); + Arrays.fill(newArray, oldCapacity, newArray.length, UNSET); + newArray[index] = value; + indexedVariables = newArray; + } +} diff --git a/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/NamedInternalThreadFactory.java b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/NamedInternalThreadFactory.java new file mode 100644 index 0000000..53b082a --- /dev/null +++ b/blossom-backend/common/common-base/src/main/java/com/blossom/common/base/util/threadlocal/NamedInternalThreadFactory.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blossom.common.base.util.threadlocal; + + +import com.blossom.common.base.util.thread.NamedThreadFactory; + +/** + * NamedInternalThreadFactory + * This is a threadFactory which produce {@link InternalThread} + */ +public class NamedInternalThreadFactory extends NamedThreadFactory { + + public NamedInternalThreadFactory() { + super(); + } + + public NamedInternalThreadFactory(String prefix) { + super(prefix, false); + } + + public NamedInternalThreadFactory(String prefix, boolean daemon) { + super(prefix, daemon); + } + + @Override + public Thread newThread(Runnable runnable) { + String name = mPrefix + mThreadNum.getAndIncrement(); + InternalThread ret = new InternalThread(mGroup, runnable, name, 0); + ret.setDaemon(mDaemon); + return ret; + } +} diff --git a/blossom-backend/common/common-base/src/main/resources/META-INF/spring-configuration-metadata.json b/blossom-backend/common/common-base/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..6335955 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,60 @@ +{ + "groups": [ + { + "name": "project.base", + "type": "com.blossom.common.base.BaseProperties", + "sourceType": "com.blossom.common.base.BaseProperties" + }, + { + "name": "project.base.ex", + "type": "com.blossom.common.base.BaseProperties$Ex", + "sourceType": "com.blossom.common.base.BaseProperties" + }, + { + "name": "project.base.log", + "type": "com.blossom.common.base.BaseProperties$Log", + "sourceType": "com.blossom.common.base.BaseProperties" + } + ], + "properties": [ + { + "name": "project.base.ex.format", + "type": "com.blossom.common.base.enums.ExFormat", + "description": "{@link AbstractExceptionAdvice} 中是否将异常信息进行格式化打印;

如果是 line, 则异常堆栈信息按照一行记录在日志中;

如果是 project, 则异常堆栈信息按照格式化方式记录在日志中", + "sourceType": "com.blossom.common.base.BaseProperties$Ex" + }, + { + "name": "project.base.ex.stack-trace", + "type": "com.blossom.common.base.enums.ExStackTrace", + "description": "{@link AbstractExceptionAdvice} 中记录异常堆栈信息的内容;

如果是 all ,则记录全部堆栈信息;

如果是 project, 堆栈信息只包含项目包下的类;", + "sourceType": "com.blossom.common.base.BaseProperties$Ex" + }, + { + "name": "project.base.log.duration", + "type": "java.lang.Long", + "description": "日志级别的缓存时间, 单位毫秒, 超过该时间将会重置为 INFO, 用来防止日志过大。

仅针对通过动态日志配置的日志路径, 如果日志级别已在配置文件中配置, 则会 被覆盖并在超时后重置为 INFO, 但项目重启后会重置为", + "sourceType": "com.blossom.common.base.BaseProperties$Log" + }, + { + "name": "project.base.log.dynamic-repo", + "type": "java.lang.String", + "description": "动态日志存储方式

caffeine: caffeine, 该配置仅针对本地缓存, 并不会在集群间同步日志级 别配置。如果项目以集群方式部署, 则使用 caffeine 只会修改接收请求的服务器 的日志级别。

redis: redis,", + "sourceType": "com.blossom.common.base.BaseProperties$Log", + "defaultValue": "caffeine" + }, + { + "name": "project.base.log.restore-duration", + "type": "java.lang.Long", + "description": "每 restoreDuration 秒, 会重新刷新日志级别。如果日志级别设置的时间已超时, 则会重置为 INFO", + "sourceType": "com.blossom.common.base.BaseProperties$Log" + }, + { + "name": "project.base.version", + "type": "java.lang.String", + "description": "系统版本, 可以使用 @project.version@ 获取 pom 中版本", + "sourceType": "com.blossom.common.base.BaseProperties", + "defaultValue": "xyz" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/blossom-backend/common/common-base/src/main/resources/META-INF/spring.factories b/blossom-backend/common/common-base/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..811da86 --- /dev/null +++ b/blossom-backend/common/common-base/src/main/resources/META-INF/spring.factories @@ -0,0 +1,11 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.common.base.config.JacksonConfig,\ + com.blossom.common.base.BaseProperties,\ + com.blossom.common.base.BaseConfig,\ + com.blossom.common.base.config.WebMvcConfig,\ + com.blossom.common.base.exception.ExceptionAdviceByGlobal,\ + com.blossom.common.base.exception.ExceptionAdviceByXz,\ + com.blossom.common.base.util.spring.SpringUtil,\ + com.blossom.common.base.log.DynamicLogRepositoryCaffeine + + diff --git a/blossom-backend/common/common-cache/pom.xml b/blossom-backend/common/common-cache/pom.xml new file mode 100644 index 0000000..256a4d3 --- /dev/null +++ b/blossom-backend/common/common-cache/pom.xml @@ -0,0 +1,35 @@ + + + + common + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + common-cache + Redis 相关功能模块 + + + + com.blossom + common-base + + + + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + \ No newline at end of file diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/CommonCacheProperties.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/CommonCacheProperties.java new file mode 100644 index 0000000..b9e1573 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/CommonCacheProperties.java @@ -0,0 +1,40 @@ +package com.blossom.common.cache; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * 缓存配置 + * + * @author xzzz + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "project.cache") +public class CommonCacheProperties { + + /** + * 缓存的 key 配置 + */ + private List namesConfig = new ArrayList<>(); + + @Data + public static class CacheNamesConfig { + /** + * 缓存的 key + */ + private String name; + /** + * 缓存的超时时间, 单位毫秒 + */ + private Integer seconds; + /** + * 最大缓存数量, 通常是指 caffeine + */ + private Integer maximumSize = 1000; + } +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/caffeine/CaffeineCacheConfig.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/caffeine/CaffeineCacheConfig.java new file mode 100644 index 0000000..771f5ba --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/caffeine/CaffeineCacheConfig.java @@ -0,0 +1,58 @@ +package com.blossom.common.cache.caffeine; + +import com.blossom.common.cache.CommonCacheProperties; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; + +import java.util.concurrent.TimeUnit; + +/** + * caffeine 配置 + * + * @author xzzz + */ +@Slf4j +@EnableCaching +@ConditionalOnMissingClass("org.springframework.data.redis.core.RedisTemplate") +public class CaffeineCacheConfig { + + /** + * 配置缓存管理器 + * + * @param properties 公共缓存配置 + * @return 缓存管理器 + */ + @Bean("caffeineCacheManager") + public CacheManager cacheManager(CommonCacheProperties properties) { + log.info("[ CACHE] 缓存注解使用 : Caffeine"); + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + + // 默认的过期策略 + cacheManager.setCaffeine(Caffeine.newBuilder() + // 设置最后一次写入或访问后经过固定时间过期 + .expireAfterAccess(120, TimeUnit.MINUTES) + // 初始的缓存空间大小 + .initialCapacity(2000) + // 缓存的最大条数 + .maximumSize(10000)); + + for (CommonCacheProperties.CacheNamesConfig cacheNamesConfig : properties.getNamesConfig()) { + Cache cache = Caffeine.newBuilder() + .maximumSize(cacheNamesConfig.getMaximumSize()) + .expireAfterAccess(cacheNamesConfig.getSeconds(), TimeUnit.SECONDS) + .removalListener((Object key, Object value, RemovalCause cause) -> + log.info("缓存被删除: {}:{}", key, value)) + .build(); + + cacheManager.registerCustomCache(cacheNamesConfig.getName(), cache); + } + return cacheManager; + } +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoEnum.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoEnum.java new file mode 100644 index 0000000..ad8a6a0 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoEnum.java @@ -0,0 +1,85 @@ +package com.blossom.common.cache.pojo; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; + +@SuppressWarnings("all") +public enum RedisInfoEnum { + + active_defrag_hits ("[碎片整理] 活动碎片整理进程每分钟执行的值重新分配数。"), + active_defrag_key_hits ("[碎片整理] 主动碎片整理的键数"), + active_defrag_key_misses ("[碎片整理] 活动碎片整理进程跳过的键数"), + active_defrag_misses ("[碎片整理] "), + active_defrag_running ("[碎片整理] "), + + rdb_changes_since_last_save("[RDB备份] 自上次转储以来的更改数"), + rdb_bgsave_in_progress ("[RDB备份] 是否正在进行RDB保存; 0:否; 1:是;"), + rdb_last_save_time ("[RDB备份] 上次创建RDB文件时间"), + rdb_last_bgsave_status ("[RDB备份] 上次创建RDB文件的结果, ok:成功"), + rdb_last_bgsave_time_sec ("[RDB备份] 上次创建RDB文件用时, 单位:秒"), + rdb_current_bgsave_time_sec("[RDB备份] 正在创建RDB文件的持续时间"), + rdb_last_cow_size ("[RDB备份] 上次创建RDB文件大小, 单位:byte"), + rdb_last_load_keys_expired ("[RDB备份] 7.0属性"), + rdb_last_load_keys_loaded ("[RDB备份] 7.0属性"), + + os ("宿主系统版本"), + arch_bits ("[64/32]位"), + run_id ("随机标识符"), + tcp_port ("端口"), + uptime_in_seconds ("启动秒数"), + uptime_in_days ("启动天数"), + connected_clients ("客户端数"), + blocked_clients ("等待阻塞命令"), + redis_mode ("Redis模式"), + role ("服务器角色,如果该实例不是任何实例的副本,则值为“master”; 如果该实例是某个主实例的副本,则值为“slave”.; 注意: 一个副本可以是另一个副本的主副本(链式复制)."), + connected_slaves ("从服务器数量"), + + used_memory_overhead ("[内存] 服务器为管理其内部数据结构而分配的所有开销(以字节为单位)的总和"), + used_memory ("[内存] Redis分配的内存总量"), + used_memory_human ("[内存] Redis分配的内存总量"), + used_memory_dataset ("[内存] 数据集的大小, 单位:byte; (used_memory - used_memory_overhead)"), + used_memory_dataset_perc ("[内存] used_memory_dataset 占净内存使用量的百分比 (used_memory_dataset / (used_memory - used_memory_startup))"), + used_memory_peak ("[内存] Redis内存消耗峰值"), + used_memory_peak_human ("[内存] Redis内存消耗峰值"), + used_memory_peak_perc ("[内存] 当前内存占峰值的百分比, 即[used_memory/used_memory_peak]"), + used_memory_rss ("[内存] 从操作系统的角度,返回Redis已分配的内存总量(俗称常驻集大小), 这个值和[top/ps]等命令的输出一致."), + used_memory_rss_human ("[内存] 从操作系统的角度,返回Redis已分配的内存总量(俗称常驻集大小), 这个值和[top/ps]等命令的输出一致."), + used_memory_lua ("[内存] Lua脚本占用的内存"), + used_memory_lua_human ("[内存] Lua脚本占用的内存"), + used_memory_scripts ("[内存] 缓存的Lua脚本使用的内存, 单位:byte"), + used_memory_scripts_human ("[内存] 缓存的Lua脚本使用的内存, 单位:byte"), + used_memory_startup ("[内存] Redis在启动时消耗的初始内存量, 单位:byte"), + + total_system_memory_human ("系统总内存"), + total_commands_processed ("服务器已执行的命令数"), + instantaneous_ops_per_sec ("每秒执行的命令数"), + expired_keys ("过期自定删除的键数量"), + evicted_keys ("因为容量限制而驱逐的键数量"), + keyspace_hits ("命中成功的数量"), + keyspace_misses ("命中失败的数量"), + + pubsub_channels("目前被订阅的频道数"), + ; + + @Getter + private String desc; + + RedisInfoEnum(String desc) { + this.desc = desc; + } + + public static String getDesc(String key) { + if (StrUtil.isBlank(key)) { + return ""; + } + + for (RedisInfoEnum value : RedisInfoEnum.values()) { + if (value.name().equals(key)) { + return value.desc; + } + } + + return ""; + } +} + diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoRes.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoRes.java new file mode 100644 index 0000000..e1ea0c3 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/pojo/RedisInfoRes.java @@ -0,0 +1,25 @@ +package com.blossom.common.cache.pojo; + +import lombok.Data; + +/** + * redis 信息 + * + * @author xzzz + */ +@Data +public class RedisInfoRes { + + /** + * redis 键 + */ + private String key; + /** + * redis 值 + */ + private String value; + /** + * redis 说明 + */ + private String desc; +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/DynamicLogRepositoryRedis.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/DynamicLogRepositoryRedis.java new file mode 100644 index 0000000..98c2122 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/DynamicLogRepositoryRedis.java @@ -0,0 +1,141 @@ +package com.blossom.common.cache.redis; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.log.DynamicLogRepository; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 监听 redis + * + * @author xzzz + */ +@Slf4j +@Component("DynamicLogRepositoryRedis") +@ConditionalOnClass(RedisTemplate.class) +public class DynamicLogRepositoryRedis implements DynamicLogRepository { + + private final StringRedisTemplate redisTemplate; + private final LoggingSystem loggingSystem; + private final ScheduledExecutorService restore = Executors.newScheduledThreadPool(1); + private final String TOPIC_NAME = "topic_dynamic_log"; + + @Autowired + private SimpleLock simpleLock; + + public DynamicLogRepositoryRedis(LoggingSystem loggingSystem, + StringRedisTemplate redisTemplate, + RedisMessageListenerContainer listenerContainer, + BaseProperties properties) { + log.info("[ BASE] 日志级别存储 : Redis, 配置持续时长[{}ms], 刷新间隔[{}ms]", + properties.getLog().getDuration(), properties.getLog().getRestoreDuration()); + this.redisTemplate = redisTemplate; + this.loggingSystem = loggingSystem; + listenerContainer.addMessageListener(new DynamicLogListener(this), new ChannelTopic(TOPIC_NAME)); + restore.scheduleWithFixedDelay(this::restore, 5000, properties.getLog().getRestoreDuration(), TimeUnit.MILLISECONDS); + } + + /** + * 保存日志级别 + */ + @Override + public void save(LevelWrapper levelWrapper) { + log.info("[ BASE] 刷新日志级别 {}:{}", levelWrapper.getPath(), levelWrapper.getLevel()); + LogLevel logLevel = LogLevel.valueOf(levelWrapper.getLevel().toUpperCase()); + // 1. 设置日志级别 + loggingSystem.setLogLevel(levelWrapper.getPath(), logLevel); + // 2. 如果设置的不是INFO级别, 则需要发布消息让集群同步日志级别 + redisTemplate.opsForHash().put(SpringUtil.getAppName(), levelWrapper.getPath(), JsonUtil.toJson(levelWrapper)); + redisTemplate.convertAndSend(TOPIC_NAME, JsonUtil.toJson(levelWrapper)); + } + + /** + * 重新加载, 集群中只有一台会从Redis获取数据 + *

1. 如果手动修改了 Redis 配置, 该方法会重新同步, 未发生改变的也会再次同步 + *

2. 如果级别过去, 该方法会将集群重置为 INFO + */ + public void restore() { + boolean isLock = simpleLock.tryLock(SpringUtil.getAppName()); + if (isLock) { + try { + Map levelMap = redisTemplate.opsForHash().entries(SpringUtil.getAppName()); + List expireLevels = new ArrayList<>(); + levelMap.forEach((pathObj, levelObj) -> { + LevelWrapper levelWrapper = JsonUtil.toObj(levelObj.toString(), LevelWrapper.class); + if (levelWrapper == null) { + return; + } + if (levelWrapper.getExpire() < System.currentTimeMillis()) { + expireLevels.add(levelWrapper); + } else { + this.save(levelWrapper); + } + }); + + // 重置为 INFO + for (LevelWrapper levelWrapper : expireLevels) { + redisTemplate.opsForHash().delete(SpringUtil.getAppName(), levelWrapper.getPath()); + levelWrapper.setLevel(LogLevel.INFO.name()); + log.info("[ BASE] 重置日志级别 {}:{}", levelWrapper.getPath(), levelWrapper.getLevel()); + redisTemplate.convertAndSend(TOPIC_NAME, JsonUtil.toJson(levelWrapper)); + } + } finally { + simpleLock.release(SpringUtil.getAppName()); + } + } + } + + /** + * 监听事件 + * + * @param levelWrapper 事件 + */ + public void listener(LevelWrapper levelWrapper) { + if (levelWrapper == null) { + return; + } + loggingSystem.setLogLevel(levelWrapper.getPath(), LogLevel.valueOf(levelWrapper.getLevel().toUpperCase())); + } + + + /** + * 监听器 + */ + private static class DynamicLogListener implements MessageListener { + private final DynamicLogRepositoryRedis repositoryRedis; + + public DynamicLogListener(DynamicLogRepositoryRedis repositoryRedis) { + this.repositoryRedis = repositoryRedis; + } + + @Override + public void onMessage(Message message, byte[] pattern) { + String body = new String(message.getBody()); + if (StrUtil.isBlank(body)) { + return; + } + repositoryRedis.listener(JsonUtil.toObj(body, LevelWrapper.class)); + } + } + +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisCacheConfig.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisCacheConfig.java new file mode 100644 index 0000000..643d7a7 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisCacheConfig.java @@ -0,0 +1,85 @@ +package com.blossom.common.cache.redis; + +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.cache.CommonCacheProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * Redis 缓存设置了key缓存名后加 "::" + * + * @author : xzzz + * @see org.springframework.data.redis.cache.CacheKeyPrefix#simple + */ +@Slf4j +@EnableCaching +@ConditionalOnClass(RedisTemplate.class) +public class RedisCacheConfig { + + private static final Integer DEFAULT_TIME_OUT = 60 * 60 * 4; + + /** + * 配置 + * + * @param redisConnectionFactory redis配置 + * @return redis管理器 + */ + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, CommonCacheProperties properties) { + log.info("[ CACHE] 缓存注解使用 : Redis"); + return new RedisCacheManager( + RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), + // 默认策略 + this.getDefaultCacheConfiguration(DEFAULT_TIME_OUT), + // 自定义Key缓存策略 + this.initCacheConfigurations(properties) + ); + } + + /** + * redis缓存默认配置 + * 修改缓存序列化方式,修改过期时间 + * + * @return RedisCacheConfiguration Redis配置类 + */ + private RedisCacheConfiguration getDefaultCacheConfiguration(Integer seconds) { + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); + // 修改序列化方式 + jackson2JsonRedisSerializer.setObjectMapper(JsonUtil.typeObjectMapper()); + + return RedisCacheConfiguration + .defaultCacheConfig() + .computePrefixWith(name -> RedisConstant.CACHE_PREFIX_WITH + name + RedisConstant.CACHE_SUFFIX_WITH) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) + .entryTtl(Duration.ofSeconds(seconds)); + } + + /** + * 自定义配置, 不使用公共的缓存有效时间 + * + * @return 配置对象 + */ + private Map initCacheConfigurations(CommonCacheProperties properties) { + Map configMap = new HashMap<>(1); + for (CommonCacheProperties.CacheNamesConfig cacheNamesConfig : properties.getNamesConfig()) { + configMap.put(cacheNamesConfig.getName(), this.getDefaultCacheConfiguration(cacheNamesConfig.getSeconds())); + } + // ========== 自定义设置缓存时间 =========== + return configMap; + } + + +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConfiguration.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConfiguration.java new file mode 100644 index 0000000..7667046 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConfiguration.java @@ -0,0 +1,72 @@ +package com.blossom.common.cache.redis; + +import com.blossom.common.base.config.JacksonConfig; +import com.blossom.common.base.util.json.JsonUtil; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +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.StringRedisTemplate; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * RedisTemplate 配置, 在 RedisTemplate 自动装载之前配置 + * + * @author : xzzz + */ +@Configuration +@ConditionalOnClass(RedisTemplate.class) +@AutoConfigureBefore({RedisAutoConfiguration.class, JacksonConfig.class}) +public class RedisConfiguration { + + private final RedisConnectionFactory factory; + + public RedisConfiguration(RedisConnectionFactory factory) { + this.factory = factory; + } + + @Bean + public RedisTemplate redisTemplate() { + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); + // 修改序列化方式 + jackson2JsonRedisSerializer.setObjectMapper(JsonUtil.typeObjectMapper()); + + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 序列化 key + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); + redisTemplate.setConnectionFactory(factory); + return redisTemplate; + } + + /** + * 将 RedisMessageListenerContainer 注入到IOC, 方便使用 + */ + @Bean + public RedisMessageListenerContainer listenerContainer() { + RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer(); + listenerContainer.setConnectionFactory(factory); + return listenerContainer; + } + +// @Bean(name = "dynamicLogRepositoryRedis") +// public DynamicLogRepository dynamicLogRepositoryRedis(LoggingSystem loggingSystem, +// StringRedisTemplate redisTemplate, +// RedisMessageListenerContainer listenerContainer, +// BaseProperties properties) { +// return new DynamicLogRepositoryRedis(loggingSystem, redisTemplate, listenerContainer, properties); +// } + + @Bean + public SimpleLock simpleLock(StringRedisTemplate stringRedisTemplate) { + return new SimpleLock(stringRedisTemplate); + } +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConstant.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConstant.java new file mode 100644 index 0000000..81a8206 --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisConstant.java @@ -0,0 +1,17 @@ +package com.blossom.common.cache.redis; + +import com.blossom.common.base.BaseConstants; + +/** + * @author xzzz + * @since 0.0.1 + */ +public final class RedisConstant { + + /** + * cache 前后缀 + */ + public static final String CACHE_PREFIX_WITH = BaseConstants.PROJECT_NAME + ":"; + + public static final String CACHE_SUFFIX_WITH = ":"; +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisProperties.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisProperties.java new file mode 100644 index 0000000..919b60e --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/RedisProperties.java @@ -0,0 +1,15 @@ +package com.blossom.common.cache.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * redis 配置 + * + * @author xzzz + */ +@Configuration +@ConfigurationProperties(prefix = "project.redis") +public class RedisProperties { + +} diff --git a/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/SimpleLock.java b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/SimpleLock.java new file mode 100644 index 0000000..72a2e7a --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/java/com/blossom/common/cache/redis/SimpleLock.java @@ -0,0 +1,44 @@ +package com.blossom.common.cache.redis; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.util.spring.SpringUtil; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 最简单的分布式锁(setnx), 适用于非严格场景. + *

不可重入锁 + *

不考虑锁续期 + *

锁超时由 redis 自动释放 + * + * @author xzzz + */ +public class SimpleLock { + + private final String LOCK_PREFIX = ":simple_lock:"; + private final long TIMEOUT = 30 * 1000; + private final StringRedisTemplate redisTemplate; + + public SimpleLock(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public boolean tryLock(String key) { + if (StrUtil.isBlank(key)) { + return false; + } + Boolean result = redisTemplate.opsForValue() + .setIfAbsent(buildKey(key), "lock", TIMEOUT, TimeUnit.MILLISECONDS); + return result != null && result; + } + + public boolean release(String key) { + Boolean result = redisTemplate.delete(buildKey(key)); + return result != null && result; + } + + private String buildKey(String key) { + return SpringUtil.getAppName() + LOCK_PREFIX + key; + } +} diff --git a/blossom-backend/common/common-cache/src/main/resources/META-INF/spring.factories b/blossom-backend/common/common-cache/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a4fa2ec --- /dev/null +++ b/blossom-backend/common/common-cache/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.common.cache.CommonCacheProperties,\ + com.blossom.common.cache.caffeine.CaffeineCacheConfig,\ + com.blossom.common.cache.redis.RedisProperties,\ + com.blossom.common.cache.redis.RedisCacheConfig,\ + com.blossom.common.cache.redis.RedisConfiguration,\ + com.blossom.common.cache.redis.DynamicLogRepositoryRedis + diff --git a/blossom-backend/common/common-db/pom.xml b/blossom-backend/common/common-db/pom.xml new file mode 100644 index 0000000..d50d39e --- /dev/null +++ b/blossom-backend/common/common-db/pom.xml @@ -0,0 +1,80 @@ + + + + common + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + common-db + + + + 8.0.21 + + 5.3.2 + 1.4.6 + + 3.5.9 + 2.2.2 + 3.5.3.1 + + + + + + com.blossom + common-base + + + + + org.mybatis + mybatis + ${mybatis.version} + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.springboot.version} + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis.plus.version} + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-springboot.version} + + + + + + + + src/main/resources + + /administrative/** + + + + + \ No newline at end of file diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/AddColumnUtil.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/AddColumnUtil.java new file mode 100644 index 0000000..8de5958 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/AddColumnUtil.java @@ -0,0 +1,92 @@ +package com.blossom.common.db; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; + +/** + * 新增字段时, 生成相关代码块 + * + * @author xzzz + */ +public class AddColumnUtil { + + private static AddColumnUtil.Field[] column = new AddColumnUtil.Field[]{ + new AddColumnUtil.Field("open_state", "是否开放", Integer.class), + }; + + public static void main(String[] args) { + genFiled(); + genColumn(); + genSelectWhere(); + genUpdateSet(); + } + + private static void genFiled() { + System.out.println("\n\n========= field ======================================================\n"); + for (AddColumnUtil.Field f : column) { + System.out.println(String.format("/** %s */", f.getComment())); + System.out.println(String.format("private %s %s;", f.getClazz().getSimpleName(), StrUtil.toCamelCase(f.getFiled()))); + } + } + + private static void genColumn() { + System.out.println("\n\n========= field ======================================================\n"); + for (AddColumnUtil.Field f : column) { + System.out.println(String.format("%s,", f.getFiled())); + } + } + + private static void genUpdateSet() { + System.out.println("\n\n========= UPDATE SET ======================================================\n"); + for (AddColumnUtil.Field f : column) { + if (f.getClazz() == String.class) { + System.out.println(String.format("%s = #{%s},", + StrUtil.toCamelCase(f.getFiled()), + StrUtil.toCamelCase(f.getFiled()), + f.getFiled(), + StrUtil.toCamelCase(f.getFiled()) + )); + } else { + System.out.println(String.format("%s = #{%s},", StrUtil.toCamelCase(f.getFiled()), f.getFiled(), StrUtil.toCamelCase(f.getFiled()))); + } + } + } + + private static void genSelectWhere() { + System.out.println("\n\n========= SELECT WHERE ======================================================\n"); + for (AddColumnUtil.Field f : column) { + if (f.getClazz() == String.class) { + System.out.println(String.format("and %s = #{%s}", + StrUtil.toCamelCase(f.getFiled()), + StrUtil.toCamelCase(f.getFiled()), + f.getFiled(), + StrUtil.toCamelCase(f.getFiled()) + )); + } else { + System.out.println(String.format("and %s = #{%s}", + StrUtil.toCamelCase(f.getFiled()), + f.getFiled(), + StrUtil.toCamelCase(f.getFiled()) + )); + } + } + } + + @Data + private static class Field { + String filed; + String comment; + Class clazz; + + public Field(String filed) { + this.filed = filed; + this.clazz = String.class; + } + + public Field(String filed, String comment, Class clazz) { + this.filed = filed; + this.comment = comment; + this.clazz = clazz; + } + } +} diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/DBProperties.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/DBProperties.java new file mode 100644 index 0000000..b436020 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/DBProperties.java @@ -0,0 +1,22 @@ +package com.blossom.common.db; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 数据库配置 + * + * @author xzzz + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "project.db") +public class DBProperties { + + /** + * 慢SQL指标, 单位毫秒 + */ + private long slowInterval = 2000L; + +} diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/PageAspect.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/PageAspect.java new file mode 100644 index 0000000..dff45db --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/PageAspect.java @@ -0,0 +1,61 @@ +package com.blossom.common.db.aspect; + +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.PageHelper; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.db.pojo.PageReq; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * 分页切面 + *

注意:该分页只对方法中的第一个查询语句生效,如果方法中包含多个查询语句需要分页,则人需要手动使用 {@link PageHelper#startPage(int, int)} + * + * @author xzzz + */ +@Slf4j +@Aspect +public class PageAspect { + + @Around("@annotation(com.blossom.common.db.aspect.Pages)") + public Object around(ProceedingJoinPoint point) throws Throwable { + Object[] args = point.getArgs(); + for (Object arg : args) { + if (arg instanceof PageReq) { + PageReq page = (PageReq) arg; + if (page.getPageNum() == 0) { + page.setPageNum(1); + } + if (page.getPageSize() == 0) { + page.setPageSize(10); + } + if (page.getPageSize() > 200) { + page.setPageSize(200); + } + String orderBy; + if (StrUtil.isNotBlank(page.getSortField())) { + orderBy = page.getSortField(); + if (StrUtil.isNotBlank(page.getOrder())) { + String order = page.getOrder().trim().toLowerCase(); + if (!"asc".equals(order) && !"desc".equals(order)) { + throw new XzException400("Order参数错误,请使用[asc]或[desc],当前传入值[" + order + "]"); + } + orderBy = orderBy + " " + order; + } + PageHelper.startPage(page.getPageNum(), page.getPageSize(), orderBy); + } else { + PageHelper.startPage(page.getPageNum(), page.getPageSize()); + } + } + } + try { + return point.proceed(args); + } finally { +// PageHelper.clearPage(); + } + } + + +} diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/Pages.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/Pages.java new file mode 100644 index 0000000..72dc033 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/aspect/Pages.java @@ -0,0 +1,15 @@ +package com.blossom.common.db.aspect; + +import java.lang.annotation.*; + +/** + * 分页注解, 仅对被注解方法的第一条sql语句生效, 多条语句分页时需显示调用 + * + * @author xzzz + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Pages { + +} diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/exception/ExceptionAdviceByDB.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/exception/ExceptionAdviceByDB.java new file mode 100644 index 0000000..ca27696 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/exception/ExceptionAdviceByDB.java @@ -0,0 +1,126 @@ +package com.blossom.common.db.exception; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.BaseProperties; +import com.blossom.common.base.exception.AbstractExceptionAdvice; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.pojo.RCode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.MyBatisSystemException; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Optional; + +/** + * 全局异常处理器 + *

+ * 统一以 R 对象格式返回给客服端 + * + * @author xzzz + * @since 0.0.1 + */ +@Slf4j +@Order(-1) +@RestControllerAdvice +public class ExceptionAdviceByDB extends AbstractExceptionAdvice { + + public ExceptionAdviceByDB(BaseProperties baseProperties) { + super(baseProperties); + } + + /** + * 没有填写必填项 + * 字段过长 + */ + @ExceptionHandler(DataIntegrityViolationException.class) + public R handleDataIntegrityViolationException(DataIntegrityViolationException exception) { + MsgTarget[] targets = { + new MsgTarget("Cause: java.sql.SQLException: Field '", "字段 [%s] 为必填项"), + new MsgTarget("Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column '", "[%s] 超出最大长度") + }; + String msg = getMsg(exception, targets); + printExLog(exception, msg, true); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + /** + * 数据库唯一键重复 + */ + @ExceptionHandler(DuplicateKeyException.class) + public R handleDuplicateKeyException(DuplicateKeyException exception) { + MsgTarget[] targets = { + new MsgTarget("Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '", "唯一键 [%s] 重复") + }; + String msg = getMsg(exception, targets); + printExLog(exception, msg, true); + return R.fault(RCode.BAD_REQUEST.getCode(), msg, exception.getMessage()); + } + + + /** + * SQL 语句执行有错误 + */ + @ExceptionHandler(BadSqlGrammarException.class) + public R handleSQLSyntaxErrorException(BadSqlGrammarException exception) { + MsgTarget[] targets = { + new MsgTarget("SQLSyntaxErrorException: Unknown column '", "语句中发现未知字段 [%s]"), + }; + String msg = getMsg(exception, targets); + printExLog(exception, msg, true); + return R.fault(RCode.INTERNAL_SQL_ERROR.getCode(), + RCode.INTERNAL_SQL_ERROR.getMsg() + + String.format("[%s-%s], %s", exception.getSQLException().getErrorCode(), exception.getSQLException().getSQLState(),msg), + exception.getSql() + ">>" + exception.getMessage()); + } + + /** + * mybatis 中出现的错误 + */ + @ExceptionHandler(MyBatisSystemException.class) + public R handleMyBatisSystemException(MyBatisSystemException exception) { + MsgTarget[] targets = { + new MsgTarget("There is no getter for property named '", + "未在 Mapper.xml 的请求或响应 Entity 中找到字段 [%s] 的 getter 方法, 请检查 Entity 与 XML 的字段映射关系."), + }; + String msg = getMsg(exception, targets); + printExLog(exception, msg, true); + return R.fault(RCode.INTERNAL_SQL_ERROR.getCode(), msg, exception.getMessage()); + } + + private String getMsg(Throwable exception, MsgTarget ... targets) { + String exMsg = Optional.ofNullable(exception.getMessage()).orElse(""); + if (targets == null) { + return exMsg; + } + for (MsgTarget msgTarget : targets) { + // 如果异常包含该内容 + if (exMsg.contains(msgTarget.getTarget())) { + String entry = exMsg.substring(exMsg.indexOf(msgTarget.getTarget()) + msgTarget.getTarget().length()); + entry = entry.substring(0, entry.indexOf("'")); + if (StrUtil.isNotBlank(entry)) { + return String.format(msgTarget.getErrMsg(), entry); + } + } + } + exception.printStackTrace(); + return exMsg; + } + + @Data + static class MsgTarget { + /** 异常内容 */ + private String target; + private String errMsg; + + public MsgTarget(String target, String errMsg) { + this.target = target; + this.errMsg = errMsg; + } + } +} \ No newline at end of file diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/interceptor/SlowSQLInterceptor.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/interceptor/SlowSQLInterceptor.java new file mode 100644 index 0000000..7433d3e --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/interceptor/SlowSQLInterceptor.java @@ -0,0 +1,169 @@ +package com.blossom.common.db.interceptor; + +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.util.StringUtil; +import com.blossom.common.db.DBProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 慢 sql 拦截 + * + * @author xzzz + */ +@Slf4j +@Component +@Intercepts({ + @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), +}) +public class SlowSQLInterceptor implements Interceptor { + + private static final ThreadLocal DATETIME_FORMATTER = ThreadLocal + .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + + private final Long slowInterval; + + public SlowSQLInterceptor(DBProperties dbProperties) { + if (dbProperties == null) { + this.slowInterval = 1000L; + } else { + this.slowInterval = dbProperties.getSlowInterval(); + } + log.info("[ BASE] 慢查询时长为 : {}ms", slowInterval); + } + + @Override + public Object intercept(Invocation invocation) throws Throwable { + long start = System.currentTimeMillis(); + try { + return invocation.proceed(); + } finally { + long ms = System.currentTimeMillis() - start; + if (ms >= slowInterval) { + MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; + Object parameter = null; + if (invocation.getArgs().length > 1) { + parameter = invocation.getArgs()[1]; + } + BoundSql boundSql = mappedStatement.getBoundSql(parameter); + Configuration configuration = mappedStatement.getConfiguration(); + String sql = this.getSql(configuration, boundSql); + log.error("SLOW_SQL >> [{}ms] {}", StrUtil.fillBefore(String.valueOf(ms), ' ', 4), sql); + } + } + } + + /** + * 获取完整的sql语句 + * + * @param configuration + * @param boundSql + * @return + */ + private String getSql(Configuration configuration, BoundSql boundSql) { + // 输入sql字符串空判断 + String sql = boundSql.getSql(); + if (StringUtil.isEmpty(sql)) { + return ""; + } + return formatSql(sql, configuration, boundSql); + } + + /** + * 将占位符替换成参数值 + * + * @param sql + * @param configuration + * @param boundSql + * @return + */ + private String formatSql(String sql, Configuration configuration, BoundSql boundSql) { + //美化sql + sql = clearSql(sql); + //填充占位符, 目前基本不用mybatis存储过程调用,故此处不做考虑 + Object parameterObject = boundSql.getParameterObject(); + List parameterMappings = boundSql.getParameterMappings(); + TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + List parameters = new ArrayList<>(); + if (parameterMappings != null) { + MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); + for (ParameterMapping parameterMapping : parameterMappings) { + if (parameterMapping.getMode() != ParameterMode.OUT) { + // 参数值 + Object value; + String propertyName = parameterMapping.getProperty(); + // 获取参数名称 + if (boundSql.hasAdditionalParameter(propertyName)) { + // 获取参数值 + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + // 如果是单个值则直接赋值 + value = parameterObject; + } else { + value = metaObject == null ? null : metaObject.getValue(propertyName); + } + + if (value instanceof Number) { + parameters.add(String.valueOf(value)); + } else { + StringBuilder builder = new StringBuilder(); + builder.append("'"); + if (value instanceof Date) { + builder.append(DATETIME_FORMATTER.get().format((Date) value)); + } else if (value instanceof String) { + builder.append(value); + } + builder.append("'"); + parameters.add(builder.toString()); + } + } + } + } + + for (String value : parameters) { + if (value.length() > 500) { + sql = sql.replaceFirst("\\?", "!!该参数超长,已忽略!!"); + } else { + sql = sql.replaceFirst("\\?", value); + } + + } + return sql; + } + + public static String clearSql(String sql) { + sql = sql.replaceAll("[\\s\n ]+", " "); + sql = sql.replace(" = ", "="); + return sql; + } + + + @Override + public Object plugin(Object target) { + if (target instanceof Executor) { + return Plugin.wrap(target, this); + } + return target; + } +} diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/P6SpyLogger.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/P6SpyLogger.java new file mode 100644 index 0000000..3039fe9 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/P6SpyLogger.java @@ -0,0 +1,26 @@ +//package com.blossom.common.db.p6spy; +// +//import cn.hutool.core.util.StrUtil; +// +//public class P6SpyLogger implements MessageFormattingStrategy { +// +// /** +// * 格式化日志 +// * +// * @param connectionId 链接ID +// * @param now 当前时间 +// * @param elapsed 用时 +// * @param category 科目 +// * @param prepared error,info,batch,debug,statement,commit,rollback,result,resultset +// * @param sql sql +// * @param url 数据库连接 +// * @return 日志格式 +// */ +// @Override +// public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) { +// if (StrUtil.isNotBlank(sql)) { +// return String.format("SQL >> [% 4dms]%s", elapsed, sql.replaceAll("[\\s]+", " ")); +// } +// return ""; +// } +//} \ No newline at end of file diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/spy.properties b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/spy.properties new file mode 100644 index 0000000..d9b2259 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/p6spy/spy.properties @@ -0,0 +1,47 @@ +module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory +# +# +# ==================================================================================== +# ־Ĵӡʽ +# +# Զ־ӡʽ +logMessageFormat=com.blossom.common.db.p6spy.P6SpyLogger +# ֻڿ̨ʾ־ +# appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger +# ѡʹ־ϵͳ¼־ +appender=com.p6spy.engine.spy.appender.Slf4JLogger +# +# +# ==================================================================================== +# p6spy driver +deregisterdrivers=true +# +# +# ==================================================================================== +# ȡJDBC URLǰ׺ +useprefix=true +# +# +# ==================================================================================== +# ü¼ Log ,ȥĽerror,info,batch,debug,statement,commit,rollback,result,resultset. +excludecategories=info,debug,result,batch,resultset +# +# +# ==================================================================================== +# ڸʽ +dateformat=yyyy-MM-dd HH:mm:ss +# +# +# ==================================================================================== +# ʵɶ +#driverlist=org.h2.Driver +# +# +# ==================================================================================== +# ǷSQL¼ +outagedetection=true +# +# +# ==================================================================================== +# SQL¼׼ 1 +outagedetectioninterval=1 \ No newline at end of file diff --git a/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/pojo/PageReq.java b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/pojo/PageReq.java new file mode 100644 index 0000000..112bd57 --- /dev/null +++ b/blossom-backend/common/common-db/src/main/java/com/blossom/common/db/pojo/PageReq.java @@ -0,0 +1,37 @@ +package com.blossom.common.db.pojo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.blossom.common.base.pojo.AbstractPOJO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 分页参数 + * + * @author xzzz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PageReq extends AbstractPOJO { + + /** + * [分页参数] 页码, 0与1都表示第一页, 超过最大页时只显示最后一页 + */ + @TableField(exist = false) + private int pageNum; + /** + * [分页参数] 每页结果数, 不传则为10, 最大为200, 超过200会自动替换为200 + */ + @TableField(exist = false) + private int pageSize; + /** + * [分页参数] 排序字段, 需将驼峰类型字段改为下换线分隔字段,如 [userId > user_id] + */ + @TableField(exist = false) + private String sortField; + /** + * [分页参数] 排序方式 asc(升序) 或 desc(降序) + */ + @TableField(exist = false) + private String order; +} diff --git a/blossom-backend/common/common-db/src/main/resources/META-INF/spring.factories b/blossom-backend/common/common-db/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ae531dd --- /dev/null +++ b/blossom-backend/common/common-db/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.common.db.DBProperties,\ + com.blossom.common.db.exception.ExceptionAdviceByDB,\ + com.blossom.common.db.interceptor.SlowSQLInterceptor,\ + com.blossom.common.db.aspect.PageAspect + + diff --git a/blossom-backend/common/common-iaas/pom.xml b/blossom-backend/common/common-iaas/pom.xml new file mode 100644 index 0000000..f19f87b --- /dev/null +++ b/blossom-backend/common/common-iaas/pom.xml @@ -0,0 +1,64 @@ + + + + common + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + common-iaas + + + 3.11.2 + [3.21.11,) + 5.6.24 + [7.7.0, 7.7.99] + + + + + com.blossom + common-base + + + + + com.aliyun.oss + aliyun-sdk-oss + ${oss.version} + + + + + com.huaweicloud + esdk-obs-java-bundle + ${obs.version} + + + + + com.qcloud + cos_api + ${cos.version} + + + + + com.qiniu + qiniu-java-sdk + ${kodo.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + \ No newline at end of file diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/AbstractOSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/AbstractOSManager.java new file mode 100644 index 0000000..5cee419 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/AbstractOSManager.java @@ -0,0 +1,82 @@ +package com.blossom.common.iaas; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzException400; + +/** + * 抽象OSS处理类 + * + * @author xzzz + */ +public abstract class AbstractOSManager implements OSManager { + + protected IaasProperties prop; + + public AbstractOSManager(IaasProperties prop) { + this.prop = prop; + } + + @Override + public IaasProperties getProp() { + return prop; + } + + /** + * 清除文件名前的 "/", 以及拼接 DefaultPath + */ + protected String pathAndFilename(String filename) { + if (StrUtil.isBlank(filename)) { + throw new XzException400("文件名不得为空"); + } + // 对象存储中, 文件名不能以 "/" 开头 + if (filename.startsWith("/")) { + filename = filename.substring(1); + } + if (filename.startsWith(getDefaultPath())) { + return filename; + } + return getDefaultPath() + filename; + } + + protected String subPrefixSeparator(String filename) { + if (StrUtil.isBlank(filename)) { + throw new XzException400("文件名不得为空"); + } + // 对象存储中, 文件名不能以 "/" 开头 + if (filename.startsWith("/")) { + filename = filename.substring(1); + } + return filename; + } + + /** + * 处理最终的文件名 + * + * @param filename + * @return + */ + protected String finalUrl(String filename) { + return getDomain() + filename; + } + + protected String matchContentType(String fileName) { +// if (StrUtil.containsAny(fileName,".jpg",".png",".gif",".ico",".jpeg",".webp")) { +// return BjsConstants.CONTENT_TYPE_IMAGE_JPG; +// } else if (StrUtil.containsAny(fileName,".word",".doc",".docx")) { +// return "application/msword"; +// } + + if (StrUtil.containsAny(fileName, ".xls", ".xlsx")) { + return "application/vnd.ms-excel"; + } else if (StrUtil.containsAny(fileName, ".xls", ".xlsx")) { + return "application/pdf"; + } else if (StrUtil.containsAny(fileName, ".mp3")) { + return "audio/mp3"; + } else if (StrUtil.containsAny(fileName, ".mp4")) { + return "video/mpeg4"; + } else { + return "application/octet-stream"; + } + } + +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasConfiguration.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasConfiguration.java new file mode 100644 index 0000000..5abb903 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasConfiguration.java @@ -0,0 +1,52 @@ +package com.blossom.common.iaas; + +import com.blossom.common.iaas.oss.OSSManager; +import com.blossom.common.iaas.blos.BLOSManager; +import com.blossom.common.iaas.cos.COSManager; +import com.blossom.common.iaas.kodo.KODOManager; +import com.blossom.common.iaas.obs.OBSManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * IAAS 配置 + * + * @author xzzz + */ +@Configuration +public class IaasConfiguration { + + @Bean + @ConditionalOnProperty(name = "project.iaas.ostype", havingValue = "alibaba") + public OSManager ossManager(IaasProperties iaasProperties) { + return new OSSManager(iaasProperties); + } + + @Bean + @ConditionalOnProperty(name = "project.iaas.ostype", havingValue = "huawei") + public OSManager obsManager(IaasProperties iaasProperties) { + return new OBSManager(iaasProperties); + } + + @Bean + @ConditionalOnProperty(name = "project.iaas.ostype", havingValue = "tencent") + public OSManager cosManager(IaasProperties iaasProperties) { + return new COSManager(iaasProperties); + } + + @Bean + @ConditionalOnProperty(name = "project.iaas.ostype", havingValue = "qiniu") + public OSManager kodoManager(IaasProperties iaasProperties) { + return new KODOManager(iaasProperties); + } + + /** + * 默认 + */ + @Bean + @ConditionalOnProperty(name = "project.iaas.ostype", havingValue = "blossom", matchIfMissing = true) + public OSManager blosManager(IaasProperties iaasProperties) { + return new BLOSManager(iaasProperties); + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasEnum.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasEnum.java new file mode 100644 index 0000000..9b2ee42 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasEnum.java @@ -0,0 +1,20 @@ +package com.blossom.common.iaas; + +import lombok.Getter; + +@Getter +@SuppressWarnings("all") +public enum IaasEnum { + + ALIBABA("alibaba"), + HUAWEI("huawei"), + TENCENT("tencent"), + QINIU("qiniu"), + BLOSSOM("blossom"); + + private String type; + + IaasEnum(String type) { + this.type = type; + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasProperties.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasProperties.java new file mode 100644 index 0000000..becee6d --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/IaasProperties.java @@ -0,0 +1,235 @@ +package com.blossom.common.iaas; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + + +/** + * IAAS 配置项 + * + * @author xzzz + */ +@Slf4j +@Data +@Configuration +@ConfigurationProperties(prefix = "project.iaas") +public class IaasProperties { + /** + * 对象存储类型 + */ + private String osType; + /** + * 阿里 OSS 配置 + */ + private OSS oss; + /** + * 华为 OBS 配置 + */ + private OBS obs; + /** + * 腾讯 COS 配置 + */ + private COS cos; + /** + * 七牛 KODO 配置 + */ + private KODO kodo; + /** + * IRDA BLOS 配置 + */ + private BLOS blos; + + /** + * Ali OSS 配置 + */ + @Data + public static class OSS { + /** + * oss endpoint + */ + private String endpoint; + /** + * oss accessKeyId + */ + private String accessKeyId; + /** + * oss accessKeySecret + */ + private String secretAccessKey; + /** + * oss bucket名称 + */ + private String bucketName; + /** + * oss 的访问地址, 可以通过域名映射 + */ + private String domain; + /** + * oss 默认上传地址, 不填则默认上传至 bucket 根目录下, 以 / 结尾 + */ + private String defaultPath; + } + + /** + * 华为OBS配置 + */ + @Data + public static class OBS { + /** + * obs endpoint + */ + private String endpoint; + /** + * obs accessKeyId + */ + private String accessKey; + /** + * obs secretKey + */ + private String secretKey; + /** + * obs bucket名称 + */ + private String bucketName; + /** + * obs 的访问地址, 可以通过域名映射, 不要以 / 结尾 + */ + private String domain; + /** + * obs 默认上传地址, 不填则默认上传至 bucket 根目录下, 不要以 / 结尾 + */ + private String defaultPath; + } + + /** + * 腾讯COS配置 + */ + @Data + public static class COS { + /** + * cos regionName + */ + private String regionName; + /** + * cos accessKey + */ + private String accessKey; + /** + * cos secretKey + */ + private String secretKey; + /** + * cos bucket名称 + */ + private String bucketName; + /** + * cos 的访问地址, 可以通过域名映射, 不要以 / 结尾 + */ + private String domain; + /** + * cos 默认上传地址, 不填则默认上传至 bucket 根目录下, 不要以 / 结尾 + */ + private String defaultPath; + } + + /** + * 七牛KODO配置 + */ + @Data + public static class KODO { + // /** + // * kodo regionName + // */ + // private String regionName; + /** + * kodo accessKey + */ + private String accessKey; + /** + * kodo secretKey + */ + private String secretKey; + /** + * kodo bucket名称 + */ + private String bucketName; + /** + * kodo 的访问地址, 可以通过域名映射, 不要以 / 结尾 + */ + private String domain; + /** + * kodo 默认上传地址, 不填则默认上传至 bucket 根目录下, 不要以 / 结尾 + */ + private String defaultPath; + } + + /** + * IDRA blossom Object storage + */ + @Data + public static class BLOS { + /** + * BLOS 查看图片的接口的地址, 默认在 PictureController#getFile() 方法中, 末尾带有 "/" 会自动清除 + */ + private String domain; + /** + * BLOS 默认上传地址, 不能为空, 注意不同系统的区分, 末尾带有 "/" 会自动清除 + */ + private String defaultPath; + } + + + @PostConstruct + public void init() { + if (oss != null) { + oss.setDomain(formatDomain(oss.getDomain())); + oss.setDefaultPath(formatPath(oss.getDefaultPath())); + } + if (obs != null) { + obs.setDomain(formatDomain(obs.getDomain())); + obs.setDefaultPath(formatPath(obs.getDefaultPath())); + } + if (cos != null) { + cos.setDomain(formatDomain(cos.getDomain())); + cos.setDefaultPath(formatPath(cos.getDefaultPath())); + } + if (kodo != null) { + kodo.setDomain(formatDomain(kodo.getDomain())); + kodo.setDefaultPath(formatPath(kodo.getDefaultPath())); + } + if (blos != null) { + String domain = formatDomain(blos.getDomain()); + if (!StrUtil.endWith(domain,"/pic")) { + domain = domain + "/pic"; + } + blos.setDomain(domain); + blos.setDefaultPath(formatPath(blos.getDefaultPath())); + } + } + + /** + * 会将末尾的 "/" 删除 + */ + private String formatDomain(String str) { + if (StrUtil.isNotBlank(str) && StrUtil.endWith(str, "/")) { + return str.substring(0, str.length() - 1); + } + return str; + } + + private String formatPath(String str) { + if (StrUtil.isNotBlank(str) && StrUtil.endWith(str, "/")) { + str = str.substring(0, str.length() - 1); + } + if (StrUtil.isNotBlank(str) && !StrUtil.startWith(str, "/")) { + str = "/" + str; + } + return str; + } + +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/OSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/OSManager.java new file mode 100644 index 0000000..aa8bf53 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/OSManager.java @@ -0,0 +1,76 @@ +package com.blossom.common.iaas; + + +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.InputStream; + +/** + * OSS 处理 + * + * @author xzzz + */ +public interface OSManager { + + /** + * 获取配置 + * + * @return IAAS 配置对象 + */ + IaasProperties getProp(); + + /** + * 获取默认的上传路径, 返回结果结尾不包含 "/" + *

如: + *

{@code /home/bl/img }
+ * + * @return 上传路径 + */ + String getDefaultPath(); + + /** + * 获取访问地址, 返回结果结尾不包含 "/" + *

如: + *

{@code http://www.youdomain.com/ }
+ * + * @return 访问地址 + */ + String getDomain(); + + /** + * 上传文件 + * + * @param filename 文件名 + * @param inputStream 输入流 + * @return 文件路径 + */ + String put(String filename, InputStream inputStream); + + /** + * 上传文件 + * + * @param filename 文件名 + * @param file springboot 文件 + * @return 文件路径 + */ + String put(String filename, MultipartFile file); + + /** + * 获取文件 + * + * @param filename 文件名 + * @return 返回文件对象 + */ + default File get(String filename) { + return null; + } + + /** + * 删除文件 + * + * @param filename 文件名 + * @return 是否删除 + */ + boolean delete(String filename); +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/blos/BLOSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/blos/BLOSManager.java new file mode 100644 index 0000000..c578ed2 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/blos/BLOSManager.java @@ -0,0 +1,107 @@ +package com.blossom.common.iaas.blos; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.net.URLDecoder; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.iaas.AbstractOSManager; +import com.blossom.common.iaas.IaasEnum; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * blossom 本地图片存储 + * + * @author xzzz + */ +@Slf4j +@Component +public class BLOSManager extends AbstractOSManager implements OSManager { + + private final String domain; + private final String defaultPath; + + public BLOSManager(IaasProperties prop) { + super(prop); + if (prop == null) { + String msg = "初始化 [BLOS] 错误, 请检查配置文件 [iaas] 是否配置"; + log.error(msg); + throw new NullPointerException(msg); + } + if (!IaasEnum.BLOSSOM.getType().equals(prop.getOsType())) { + String msg = String.format("初始化 [BLOS] 错误, 请检查配置文件 [iaas.osType] 配置项是否为 \"blossom\", 当前为 \"%s\"", prop.getOsType()); + log.error(msg); + throw new IllegalArgumentException(msg); + } + if (prop.getBlos() == null) { + String msg = "初始化 [BLOS] 错误, 未配置 [BLOS] 参数, 请检查配置文件 [iaas.BLOS] 配置项"; + log.error(msg); + throw new IllegalArgumentException(msg); + } else { + domain = prop.getBlos().getDomain(); + defaultPath = prop.getBlos().getDefaultPath(); + } + } + + @Override + public String getDefaultPath() { + return defaultPath; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public String put(String filename, InputStream inputStream) { + File file = FileUtil.newFile(filename); + FileUtil.writeFromStream(inputStream, file); + return getDomain() + filename; + } + + @Override + public String put(String filename, MultipartFile file) { + try { + return put(filename, file.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500(e.getMessage()); + } + } + + /** + * 获取文件 + * + * @param filename 文件名 + * @return 返回文件 + */ + @Override + public File get(String filename) { + filename = URLDecoder.decode(filename, StandardCharsets.UTF_8); + File file = FileUtil.newFile(filename); + if (!file.exists()) { + throw new XzException500("文件[" + filename + "]不存在, 请检查文件名或文件路径"); + } + return file; + } + + /** + * 删除文件 + * + * @param filename 文件名 + * @return 是否删除 + */ + @Override + public boolean delete(String filename) { + File file = FileUtil.newFile(filename); + return file.delete(); + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/cos/COSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/cos/COSManager.java new file mode 100644 index 0000000..e2a241d --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/cos/COSManager.java @@ -0,0 +1,137 @@ +package com.blossom.common.iaas.cos; + +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.iaas.AbstractOSManager; +import com.blossom.common.iaas.IaasEnum; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.model.ObjectMetadata; +import com.qcloud.cos.model.PutObjectRequest; +import com.qcloud.cos.region.Region; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +/** + * 腾讯COS + * + * @author xzzz + */ +@Slf4j +@Component +public class COSManager extends AbstractOSManager implements OSManager { + + private final String regionName; + private final String accessKey; + private final String secretKey; + private final String bucketName; + private final String domain; + private final String defaultPath; + private final COSClient client; + + public static void main(String[] args) { + IaasProperties prop = new IaasProperties(); + IaasProperties.COS cos = new IaasProperties.COS(); + cos.setRegionName(""); + cos.setAccessKey(""); + cos.setSecretKey(""); + cos.setBucketName(""); + cos.setDomain(""); + cos.setDefaultPath(""); + prop.setOsType(""); + prop.setCos(cos); + COSManager cosManager = new COSManager(prop); + } + + public COSManager(IaasProperties prop) { + super(prop); + if (prop == null) { + String msg = "初始化 [COS] 错误, 请检查配置文件 [iaas] 是否配置"; + log.error(msg); + throw new NullPointerException(msg); + } + if (!IaasEnum.TENCENT.getType().equals(prop.getOsType())) { + String msg = String.format("初始化 [COS] 错误, 请检查配置文件 [iaas.osType] 配置项是否为 \"tencent\", 当前为 \"%s\"", prop.getOsType()); + log.error(msg); + throw new IllegalArgumentException(msg); + } + if (prop.getCos() == null) { + String msg = "初始化 [COS] 错误, 未配置 [COS] 参数, 请检查配置文件 [iaas.cos] 配置项"; + log.error(msg); + throw new IllegalArgumentException(msg); + } else { + regionName = prop.getCos().getRegionName(); + accessKey = prop.getCos().getAccessKey(); + secretKey = prop.getCos().getSecretKey(); + bucketName = prop.getCos().getBucketName(); + domain = prop.getCos().getDomain(); + defaultPath = prop.getCos().getDefaultPath(); + } + + COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey); + ClientConfig clientConfig = new ClientConfig(new Region(regionName)); + client = new COSClient(cred, clientConfig); + } + + @Override + public String getDefaultPath() { + return defaultPath; + } + + @Override + public String getDomain() { + return this.domain; + } + + @Override + + public String put(String filename, InputStream inputStream) { + ObjectMetadata objectMetadata = initObjectMetadata(filename); + final String finalFilename = defaultPath + (StrUtil.isBlank(filename) ? UUID.randomUUID().toString() + ".jpg" : filename); + PutObjectRequest putObjectRequest = new PutObjectRequest( + bucketName, + finalFilename, + inputStream, + objectMetadata); + client.putObject(putObjectRequest).getETag(); + return domain + finalFilename; + + } + + @Override + public String put(String filename, MultipartFile file) { + String name = null; + try (InputStream inputStream = file.getInputStream()) { + return put(filename, inputStream); + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500("COS 上传失败:" + e.getMessage()); + } + } + + @Override + public boolean delete(String fileName) { + String objName = StrUtil.removeAll(fileName, domain); + try { + client.deleteObject(bucketName, objName); + return true; + } finally { + client.shutdown(); + } + } + + private ObjectMetadata initObjectMetadata(String fileName) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(matchContentType(fileName)); + return objectMetadata; + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/kodo/KODOManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/kodo/KODOManager.java new file mode 100644 index 0000000..9617d58 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/kodo/KODOManager.java @@ -0,0 +1,135 @@ +package com.blossom.common.iaas.kodo; + +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.iaas.AbstractOSManager; +import com.blossom.common.iaas.IaasEnum; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import com.qiniu.common.QiniuException; +import com.qiniu.http.Response; +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.Region; +import com.qiniu.storage.UploadManager; +import com.qiniu.util.Auth; +import com.qiniu.util.Json; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 七牛云 + * + * @author xzzz + */ +@Slf4j +@Component +public class KODOManager extends AbstractOSManager implements OSManager { + + private final String endpoint; + private final String accessKey; + private final String secretKey; + private final String bucketName; + private final String domain; + + private final String defaultPath; + + private final Auth auth; + private final Configuration cfg; + private final UploadManager uploadManager; + + public static void main(String[] args) { + IaasProperties prop = new IaasProperties(); + IaasProperties.KODO kodo = new IaasProperties.KODO(); + kodo.setAccessKey(""); + kodo.setSecretKey(""); + kodo.setBucketName(""); + kodo.setDomain(""); + kodo.setDefaultPath(""); + prop.setOsType(""); + prop.setKodo(kodo); + KODOManager kodoManager = new KODOManager(prop); + } + + public KODOManager(IaasProperties prop) { + super(prop); + if (prop == null) { + String msg = "初始化 [KODO] 错误, 请检查配置文件 [iaas] 是否配置"; + log.error(msg); + throw new NullPointerException(msg); + } + if (!IaasEnum.QINIU.getType().equals(prop.getOsType())) { + String msg = String.format("初始化 [KODO] 错误, 请检查配置文件 [iaas.osType] 配置项是否为 \"qiniu\", 当前为 \"%s\"", prop.getOsType()); + log.error(msg); + throw new IllegalArgumentException(msg); + } + if (prop.getKodo() == null) { + String msg = "初始化 [KODO] 错误, 未配置 [KODO] 参数, 请检查配置文件 [iaas.kodo] 配置项"; + log.error(msg); + throw new IllegalArgumentException(msg); + } else { + endpoint = "Region.huadong()"; + accessKey = prop.getKodo().getAccessKey(); + secretKey = prop.getKodo().getSecretKey(); + bucketName = prop.getKodo().getBucketName(); + domain = prop.getKodo().getDomain(); + defaultPath = prop.getKodo().getDefaultPath(); + + cfg = new Configuration(Region.huadong()); + uploadManager = new UploadManager(cfg); + auth = Auth.create(accessKey, secretKey); + } + } + + @Override + public String getDefaultPath() { + return this.defaultPath; + } + + @Override + public String getDomain() { + return this.domain; + } + + @Override + public String put(String filename, InputStream inputStream) { + String upToken = auth.uploadToken(bucketName); + Response response; + try { + response = uploadManager.put(inputStream, subPrefixSeparator(filename), upToken, null, null); + // 解析上传成功的结果 + String responseKey = Json.decode(response.bodyString()).get("key").toString(); + System.out.println(responseKey); + return finalUrl(filename); + } catch (QiniuException ex) { + Response r = ex.response; + System.err.println(r.toString()); + throw new XzException400(ex.getMessage()); + } + } + + @Override + public String put(String filename, MultipartFile file) { + try { + return put(filename, file.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500(e.getMessage()); + } + } + + @Override + public boolean delete(String filename) { + BucketManager bucketManager = new BucketManager(auth, cfg); + try { + bucketManager.delete(bucketName, subPrefixSeparator(filename)); + } catch (QiniuException ex) { + throw new XzException400(ex.getMessage()); + } + return true; + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/obs/OBSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/obs/OBSManager.java new file mode 100644 index 0000000..c9fbddf --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/obs/OBSManager.java @@ -0,0 +1,121 @@ +package com.blossom.common.iaas.obs; + +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.iaas.AbstractOSManager; +import com.blossom.common.iaas.IaasEnum; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import com.obs.services.ObsClient; +import com.obs.services.exception.ObsException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 华为云 + * + * @author xzzz + */ +@Slf4j +@Component +public class OBSManager extends AbstractOSManager implements OSManager { + + private final String endpoint; + private final String accessKey; + private final String secretKey; + private final String bucketName; + private final String domain; + private final String defaultPath; + private final ObsClient client; + + public static void main(String[] args) { + IaasProperties prop = new IaasProperties(); + IaasProperties.OBS obs = new IaasProperties.OBS(); + obs.setEndpoint(""); + obs.setAccessKey(""); + obs.setSecretKey(""); + obs.setBucketName(""); + obs.setDomain(""); + obs.setDefaultPath(""); + prop.setOsType(""); + prop.setObs(obs); + OBSManager obsManager = new OBSManager(prop); + } + + public OBSManager(IaasProperties prop) { + super(prop); + if (prop == null) { + String msg = "初始化 [OBS] 错误, 请检查配置文件 [iaas] 是否配置"; + log.error(msg); + throw new NullPointerException(msg); + } + if (!IaasEnum.HUAWEI.getType().equals(prop.getOsType())) { + String msg = String.format("初始化 [OBS] 错误, 请检查配置文件 [iaas.osType] 配置项是否为 \"huawei\", 当前为 \"%s\"", prop.getOsType()); + log.error(msg); + throw new IllegalArgumentException(msg); + } + if (prop.getObs() == null) { + String msg = "初始化 [OBS] 错误, 未配置 [OBS] 参数, 请检查配置文件 [iaas.obs] 配置项"; + log.error(msg); + throw new IllegalArgumentException(msg); + } else { + endpoint = prop.getObs().getEndpoint(); + accessKey = prop.getObs().getAccessKey(); + secretKey = prop.getObs().getSecretKey(); + bucketName = prop.getObs().getBucketName(); + domain = prop.getObs().getDomain(); + defaultPath = prop.getObs().getDefaultPath(); + + client = new ObsClient(accessKey, secretKey, endpoint); + } + } + + @Override + public String getDefaultPath() { + return defaultPath; + } + + @Override + public String getDomain() { + return this.domain; + } + + /** + * 上传文件 + * + * @param fileName 文件名称 + * @return 文件路径 + */ + @Override + public String put(String filename, MultipartFile file) { + try { + return put(subPrefixSeparator(filename), file.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500(e.getMessage()); + } catch (ObsException e) { + log.error("[{}/{}]:{}", e.getResponseCode(), e.getErrorCode(), e.getErrorMessage()); + throw e; + } + } + + @Override + public String put(String filename, InputStream inputStream) { + client.putObject(bucketName, subPrefixSeparator(filename), inputStream); + return finalUrl(filename); + } + + @Override + public boolean delete(String filename) { + try { + client.deleteObject(bucketName, subPrefixSeparator(filename)); + return true; + } catch (ObsException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/OSSManager.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/OSSManager.java new file mode 100644 index 0000000..7947316 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/OSSManager.java @@ -0,0 +1,464 @@ +package com.blossom.common.iaas.oss; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.common.utils.BinaryUtil; +import com.aliyun.oss.model.*; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.iaas.AbstractOSManager; +import com.blossom.common.iaas.IaasEnum; +import com.blossom.common.iaas.IaasProperties; +import com.blossom.common.iaas.OSManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URL; +import java.net.URLEncoder; +import java.rmi.ServerException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Formatter; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * 阿里云 + * + * @author xzzz + */ +@Slf4j +@Component +public class OSSManager extends AbstractOSManager implements OSManager { + + private final String endpoint; + private final String accessKeyId; + private final String accessKeySecret; + private final String bucketName; + private final String domain; + private final String defaultPath; + private final OSS client; + + public static void main(String[] args) { + IaasProperties prop = new IaasProperties(); + IaasProperties.OSS oss = new IaasProperties.OSS(); + oss.setEndpoint(""); + oss.setAccessKeyId(""); + oss.setSecretAccessKey(""); + oss.setBucketName(""); + oss.setDomain(""); + oss.setDefaultPath(""); + prop.setOsType(""); + prop.setOss(oss); + OSSManager ossManager = new OSSManager(prop); + } + + /** + * constructor + * + * @param prop 配置 + */ + public OSSManager(IaasProperties prop) { + super(prop); + if (prop == null) { + String msg = "初始化 [OSS] 错误, 请检查配置文件 [iaas] 是否配置"; + log.error(msg); + throw new NullPointerException(msg); + } + if (!IaasEnum.ALIBABA.getType().equals(prop.getOsType())) { + String msg = String.format("初始化 [OSS] 错误, 请检查配置文件 [iaas.osType] 配置项是否为 \"alibaba\", 当前为 \"%s\"", prop.getOsType()); + log.error(msg); + throw new IllegalArgumentException(msg); + } + if (prop.getOss() == null) { + String msg = "初始化 [OSS] 错误, 未配置 [OSS] 参数, 请检查配置文件 [iaas.oss] 配置项"; + log.error(msg); + throw new IllegalArgumentException(msg); + } else { + endpoint = prop.getOss().getEndpoint(); + accessKeyId = prop.getOss().getAccessKeyId(); + accessKeySecret = prop.getOss().getSecretAccessKey(); + bucketName = prop.getOss().getBucketName(); + domain = prop.getOss().getDomain(); + defaultPath = prop.getOss().getDefaultPath(); + client = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + } + } + + @Override + public String getDefaultPath() { + return this.defaultPath; + } + + @Override + public String getDomain() { + return this.domain; + } + + /** + * 上传文件 + * + * @param filename 文件名, 文件名带有路径也会拼接在默认路径后 + * @param inputStream 输入流 + * @return 文件外网访问url + */ + @Override + public String put(String filename, InputStream inputStream) { + client.putObject(bucketName, pathAndFilename(filename), inputStream); + return finalUrl(filename); + } + + /** + * 上传文件流 + * + * @param filename 文件名 + * @param file springboot 文件 + * @return 文件外网访问url + */ + @Override + public String put(String filename, MultipartFile file) { + try (InputStream inputStream = file.getInputStream()) { + return put(filename, inputStream); + } catch (IOException e) { + e.printStackTrace(); + throw new XzException500("文件上传失败"); + } + } + + /** + * 上传本地文件 + * + * @param filePath 本地文件路径 + * @param filename 文件名, 如果需要指定 oss 目录, 则在文件名前增加目录, 且不要以 "/" 开头 + * @param expiration 访问路径的过期时间 + * @return 文件外网访问url + */ + public String put(String filePath, String filename, Date expiration) { + client.putObject(bucketName, pathAndFilename(filename), new File(filePath)); + String expireUrl = client.generatePresignedUrl(bucketName, filename, expiration).toString(); + return finalUrl(filename); + } + + /** + * 上传文件 + * + * @param fileBytes 文件字节 + * @param filename 文件名, 如果需要指定 oss 目录, 则在文件名前增加目录, 且不要以 "/" 开头 + * @param expiration 访问路径的过期时间 + * @return 文件外网访问url + */ + public String put(String filename, byte[] fileBytes) { + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes)) { + client.putObject(bucketName, pathAndFilename(filename), byteArrayInputStream); + } catch (IOException e) { + e.printStackTrace(); + } + return finalUrl(filename); + } + + /** + * 删除文件 + * + * @param filename 文件名 + */ + @Override + public boolean delete(String filename) { + client.deleteObject(bucketName, pathAndFilename(filename)); + return true; + } + + /** + * 判断指定的文件是否存在 + * + * @param fileName 文件名, 如果需要指定 oss 目录, 则在文件名前增加目录, 且不要以 "/" 开头 + * @return 是否存在 + */ + public boolean isExist(String filename) { + return client.doesObjectExist(bucketName, pathAndFilename(filename)); + } + + /** + * @param filename 合成图保存名字 + * @param oldFilename 底图地址 + * @param waterMark 水印图objectName(必须为OSS中的图片,例如:watermark.jpg) + * @param waterMarkParam 水印参数 + * @return + */ + public String waterMark(String filename, String oldFilename, String waterMark, WaterMarkParam waterMarkParam) { + StringBuilder styleType = new StringBuilder("image/watermark,image_"); + styleType.append(BinaryUtil.toBase64String(waterMark.getBytes())); + + String transparency = waterMarkParam.getTransparency(); + if (StrUtil.isNotBlank(transparency)) { + styleType.append(",t_").append(transparency); + } + String position = waterMarkParam.getPosition(); + if (StrUtil.isNotBlank(position)) { + styleType.append(",g_").append(position); + } + Integer x = waterMarkParam.getX(); + if (x != null) { + styleType.append(",x_").append(x); + } + Integer y = waterMarkParam.getY(); + if (y != null) { + styleType.append(",y_").append(y); + } + Integer voffset = waterMarkParam.getVoffset(); + if (voffset != null) { + styleType.append(",voffset_").append(voffset); + } + + StringBuilder sbStyle = new StringBuilder(); + Formatter styleFormatter = new Formatter(sbStyle); + + styleFormatter.format("%s|sys/saveas,o_%s,b_%s", styleType, + BinaryUtil.toBase64String(filename.getBytes()), + BinaryUtil.toBase64String(bucketName.getBytes())); + + ProcessObjectRequest request = new ProcessObjectRequest(bucketName, oldFilename, sbStyle.toString()); + OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + ossClient.processObject(request); + return domain + filename; + } + + /** + * 阿里云批量下载文件,以ZIP压缩包下载 + * + * @param fileNameList 文件名,作为key去获取文件 + * @param zipName 压缩包名称 + * @param response response + */ + public void uploadZipToResponse(List fileNameList, String zipName, HttpServletResponse response) { + if (CollUtil.isEmpty(fileNameList)) { + throw new XzException500("下载文件列表为空"); + } + try { + response.setCharacterEncoding("UTF-8"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + // ZIP压缩格式 + response.setContentType("application/zip;charset=UTF-8"); + // 设置在下载框默认显示的文件名 + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(zipName, "UTF-8")); + List ossArr = new ArrayList<>(fileNameList.size()); + for (String fileName : fileNameList) { + // 2.从阿里云OSS获取文件 + ossArr.add(client.getObject(bucketName, fileName.replace(domain, ""))); + } + // 3.写入流 + downloadToOutputStream(ossArr, response.getOutputStream()); + } catch (Exception e) { + throw new RuntimeException("下载文件异常: " + e.getMessage()); + } + } + + /** + * 写入流 + * + * @param ossObj oss对象集合 + * @param outputStream outputStream + * @throws ServerException 异常信息 + */ + private void downloadToOutputStream(List ossObj, OutputStream outputStream) throws ServerException { + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + // 将对象循环写入压缩包 + for (OSSObject ossObject : ossObj) { + try (InputStream ips = ossObject.getObjectContent()) { + zipOutputStream.putNextEntry(new ZipEntry(standardFileName(ossObject.getKey()))); + byte[] buffer = new byte[1024]; + int r = 0; + // 5.写入 + while ((r = ips.read(buffer)) != -1) { + zipOutputStream.write(buffer, 0, r); + } + // 异常不影响其余文件处理 + } catch (OSSException oos) { + log.error("查询文件异常: {}, 异常信息: {}", ossObject.getKey(), oos.getMessage()); + } catch (Exception e) { + log.error("文件写入压缩包异常: {}, 异常信息: {}", ossObject.getKey(), e.getMessage()); + } + } + zipOutputStream.flush(); + } catch (Exception e) { + throw new ServerException("下载文件异常: " + e.getMessage()); + } + } + + /** + * 阿里云批量下载文件,以ZIP流输出到 outputStream + * + * @param fileNameList 文件名,作为key去获取文件 + * @param outputStream outputStream + */ + public void uploadZipOutputStream(List fileNameList, OutputStream outputStream) { + if (CollUtil.isEmpty(fileNameList)) { + throw new XzException500("下载文件列表为空"); + } + try { + List ossArr = new ArrayList<>(fileNameList.size()); + for (String fileName : fileNameList) { + // 2.从阿里云OSS获取文件 + ossArr.add(client.getObject(bucketName, fileName.replace(domain, ""))); + } + // 3.写入流 + downloadToOutputStream(ossArr, outputStream); + } catch (Exception e) { + throw new RuntimeException("下载文件异常: " + e.getMessage()); + } + } + + /** + * 处理文件名,阿里云的key不包含HTTP路径,只有文件路径+文件名,需要对路径进行处理 + * 且在添加入ZIP中时会,若包含路径则会在压缩包中添加对应文件夹 + * + * @param fileName 文件名 + */ + private String standardFileName(String fileName) { + fileName = fileName.replace(domain, ""); + fileName = fileName.replace("/", ""); + return fileName; + } + + /** + * 获取oss文件url + * + * @param ossKey + * @return + */ + public String getUrl(String ossKey) { + try { + Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10); + URL url = client.generatePresignedUrl(bucketName, ossKey, expiration); + if (url != null) { + return url.toString(); + } + } catch (Exception e) { + log.error("获取oss文件url异常, ossKey: {}, 异常信息: {}", ossKey, e.getMessage()); + throw new RuntimeException("获取oss文件url 异常: " + e.getMessage()); + } + return null; + } + + /** + * 初始化分片 + * + * @param ossKey + * @return + */ + public String initMultiPartUpload(String ossKey) { + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + try { + + // 创建InitiateMultipartUploadRequest对象。 + InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, ossKey); + // 初始化分片。 + InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request); + // 返回uploadId,它是分片上传事件的唯一标识,您可以根据这个uploadId发起相关的操作,如取消分片上传、查询分片上传等。 + return upresult.getUploadId(); + } catch (Exception e) { + log.error("初始化分片异常, ossKey: {}, 异常信息: {}", ossKey, e.getMessage()); + throw new RuntimeException("初始化分片异常: " + e.getMessage()); + } + } + + /** + * 上传分片 + * + * @param ossKey + * @return + */ + public PartETag uploadPart(String ossKey, String uploadId, InputStream instream, Integer curPartSize, Integer partNumber) { + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + try { + UploadPartRequest uploadPartRequest = new UploadPartRequest(); + uploadPartRequest.setBucketName(bucketName); + uploadPartRequest.setKey(ossKey); + uploadPartRequest.setUploadId(uploadId); + uploadPartRequest.setInputStream(instream); + // 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。 + uploadPartRequest.setPartSize(curPartSize); + // 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出这个范围,OSS将返回InvalidArgument的错误码。 + uploadPartRequest.setPartNumber(partNumber); + // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。 + UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest); + // 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在redis中。 + return uploadPartResult.getPartETag(); + } catch (Exception e) { + log.error("上传分片异常, ossKey: {}, uploadId: {}, curPartSize: {}, partNumber: {}, 异常信息: {}", ossKey, uploadId, curPartSize, partNumber, e.getMessage()); + throw new RuntimeException("上传分片异常: " + e.getMessage()); + } + } + + /** + * 分片合并成完整的文件 + * + * @param ossKey + * @return + */ + public CompleteMultipartUploadResult completeMultipartUpload(String ossKey, String uploadId, List partETags) { + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + // 创建CompleteMultipartUploadRequest对象。 + // 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。 + CompleteMultipartUploadRequest completeMultipartUploadRequest = + new CompleteMultipartUploadRequest(bucketName, ossKey, uploadId, partETags); + try { + // 如果需要在完成文件上传的同时设置文件访问权限,请参考以下示例代码。 + // completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.PublicRead); + // 完成分片上传。 + return ossClient.completeMultipartUpload(completeMultipartUploadRequest); + } catch (Exception e) { + //分片合并异常清理原有分片数据 + AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(bucketName, ossKey, uploadId); + ossClient.abortMultipartUpload(abortMultipartUploadRequest); + throw new RuntimeException("分片合并异常: " + e.getMessage()); + } + } + + /** + * 取消分片上传 + * + * @param ossKey + * @return + */ + public void abortMultipartUpload(String ossKey, String uploadId) { + try { + // 创建abortMultipartUploadRequest对象。 + AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(bucketName, ossKey, uploadId); + client.abortMultipartUpload(abortMultipartUploadRequest); + } catch (Exception e) { + log.error("取消分片上传异常, ossKey: {}, 异常信息: {}", ossKey, e.getMessage()); + throw new RuntimeException("取消分片上传异常: " + e.getMessage()); + } + } + + /** + * 追加类型文件追加上传 + * + * @param ossKey 文件存储路径和文件名 + * @param inputStream 文件流 + * @return 文件路径 + */ + public String appendObjectUploadFile(String ossKey, InputStream inputStream) { + long position = 0L; + try { + ObjectMetadata objectMetadata = client.getObjectMetadata(bucketName, ossKey); + position = objectMetadata.getContentLength(); + } catch (Exception e) { + log.error(e.getMessage()); + } + AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucketName, ossKey, inputStream); + appendObjectRequest.setPosition(position); + AppendObjectResult appendObjectResult = client.appendObject(appendObjectRequest); + return domain + ossKey; + } + +} diff --git a/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/WaterMarkParam.java b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/WaterMarkParam.java new file mode 100644 index 0000000..c95c19c --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/java/com/blossom/common/iaas/oss/WaterMarkParam.java @@ -0,0 +1,47 @@ +package com.blossom.common.iaas.oss; + +import lombok.Data; + +/** + * 水印参数 + * + * @author xzzz + */ +@Data +public class WaterMarkParam { + + /** 透明度, 如果是图片水印,就是让图片变得透明,如果是文字水印,就是让水印变透明。 + 默认值:100, 表示 100%(不透明) + 取值范围: [0-100] */ + private String transparency; + + /** + * 位置,水印打在图的位置 + * 取值范围:[nw(左上),north(中上),ne(右上),west(左中),center(中间),east(右中),sw(左下),south(中下),se(右下)] + */ + private String position; + + /** + * 水平边距, 就是距离图片边缘的水平距离, 这个参数只有当水印位置是左上,左中,左下, 右上,右中,右下才有意义。 + * 默认值:10 + * 取值范围:[0 – 4096] + * 单位:像素(px) + */ + private Integer x; + + /** + * 垂直边距, 就是距离图片边缘的垂直距离, 这个参数只有当水印位置是左上,中上, 右上,左下,中下,右下才有意义 + * 默认值:10 + * 取值范围:[0 – 4096] + * 单位:像素(px) + */ + private Integer y; + + /** + * 中线垂直偏移,当水印位置在左中,中部,右中时,可以指定水印位置根据中线往上或者往下偏移 + * 默认值:0 + * 取值范围:[-1000, 1000] + * 单位:像素(px) + */ + private Integer voffset; +} diff --git a/blossom-backend/common/common-iaas/src/main/resources/META-INF/spring.factories b/blossom-backend/common/common-iaas/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ea8d375 --- /dev/null +++ b/blossom-backend/common/common-iaas/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.common.iaas.IaasProperties,\ + com.blossom.common.iaas.IaasConfiguration + diff --git a/blossom-backend/common/pom.xml b/blossom-backend/common/pom.xml new file mode 100644 index 0000000..d030531 --- /dev/null +++ b/blossom-backend/common/pom.xml @@ -0,0 +1,23 @@ + + + + com.blossom + blossom-backend + 1.0.0-SNAPSHOT + + 4.0.0 + pom + + common + 公共模块 + + + common-base + common-cache + common-db + common-iaas + + + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/pom.xml b/blossom-backend/expand-sentinel/expand-sentinel-metric/pom.xml new file mode 100644 index 0000000..53071e8 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/pom.xml @@ -0,0 +1,14 @@ + + + + com.blossom + expand-sentinel + 1.0.0-SNAPSHOT + + 4.0.0 + + expand-sentinel-metric + + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/config/SentinelMetricConfiguration.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/config/SentinelMetricConfiguration.java new file mode 100644 index 0000000..14a9ad4 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/config/SentinelMetricConfiguration.java @@ -0,0 +1,26 @@ +package com.blossom.expand.sentinel.metric.config; + +import com.blossom.expand.sentinel.metric.controller.SentinelMetricController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +/** + * @author xzzz + */ +@Slf4j +@Configuration +public class SentinelMetricConfiguration { + + @Bean + public SentinelMetricController sentinelMetricController() { + return new SentinelMetricController(); + } + + @PostConstruct + public void init() { + log.info("[SENTINEL] 指标接口初始化完成"); + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/AbstractSentinelController.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/AbstractSentinelController.java new file mode 100644 index 0000000..6ad8854 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/AbstractSentinelController.java @@ -0,0 +1,292 @@ +package com.blossom.expand.sentinel.metric.controller; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.util.StrUtil; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.SystemUtil; +import com.blossom.expand.sentinel.metric.pojo.OneLineMetric; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * 抽象 sentinel 接口 + * + * @author xzzz + */ +@Slf4j +public abstract class AbstractSentinelController { + + /** + * 检查并获取时间, 间隔的优先级更高 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 统计间隔 + * @return 返回String类型的日期数组,[开始时间,结束时间] + */ + public String[] checkTime(String startTime, String endTime, String interval) { + if (StrUtil.isBlank(interval)) { + if (StrUtil.isBlank(startTime)) { + throw new XzException400("未指定间隔(interval)时,开始日期为必填项"); + } + if (StrUtil.isBlank(endTime)) { + throw new XzException400("未指定间隔(interval)时,结束日期为必填项"); + } + } + return this.checkTime(startTime, endTime, interval, null); + } + + /** + * 检查并获取时间 + * + * @param interval 统计间隔 + * @return 返回String类型的日期数组,[开始时间,结束时间] + */ + public String[] checkTime(String interval) { + if (StrUtil.isBlank(interval)) { + throw new XzException400("统计间隔(interval)为必填项"); + } + return this.checkTime(null, null, interval, null); + } + + /** + * 检查并获取时间 + * + * @param offsetHour 日期偏移量,通常是一个负数,会获得一个[当前时间+偏移量,当前时间]格式的日期 + * @return 返回String类型的日期数组,[当前时间+offsetHour,当前时间] + */ + public String[] checkTime(Integer offsetHour) { + return this.checkTime(null, null, null, offsetHour); + } + + /** + * 检查并获取时间 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 统计间隔, 与 offsetHour 二选一,offsetHour 优先级更高 + * @param offsetHour 日期偏移量,通常是一个负数,会获得一个[当前时间+偏移量,当前时间]格式的日期, 与 interval 二选一,offsetHour 优先级更高 + * @return 返回String类型的日期数组,[开始时间,结束时间] + */ + public String[] checkTime(String startTime, String endTime, String interval, Integer offsetHour) { + if (offsetHour != null) { + Date now = DateUtils.date(); + endTime = DateUtils.toYMDHMS(now); + startTime = DateUtils.toYMDHMS(DateUtils.offsetHour(now, offsetHour)); + return new String[]{startTime, endTime}; + } + if (StrUtil.isNotBlank(interval)) { + Date now = DateUtils.date(); + endTime = DateUtils.toYMDHMS(now); + switch (interval) { + case "5m": + startTime = DateUtils.toYMDHMS(DateUtils.offsetMinute(now, -5)); + break; + case "10m": + startTime = DateUtils.toYMDHMS(DateUtils.offsetMinute(now, -10)); + break; + case "1h": + startTime = DateUtils.toYMDHMS(DateUtils.offsetHour(now, -1)); + break; + case "6h": + startTime = DateUtils.toYMDHMS(DateUtils.offsetHour(now, -6)); + break; + case "12h": + startTime = DateUtils.toYMDHMS(DateUtils.offsetHour(now, -12)); + break; + case "1d": + startTime = DateUtils.toYMDHMS(DateUtils.offsetHour(now, -24)); + break; + default: + throw new XzException400("无效的类型"); + } + } + return new String[]{startTime, endTime}; + } + + /** + * 根据日期计算日期间隔 + * + * @param startTime 开始日期 + * @param endTime 结束日期 + * @param quickInterval 快捷区间 + * @param repoType 类型, 用于生成指标解释 + * @return 返回 + */ + protected ResourceHistogram explain(String startTime, + String endTime, + String quickInterval, + Integer customInterval, + TimeUnit customIntervalUnit, + String repoType) { + String[] times = checkTime(startTime, endTime, quickInterval); + + ResourceHistogram rh = new ResourceHistogram(); + rh.setStartTime(times[0]); + rh.setStartTimestamp(DateUtils.toTimestamp(times[0])); + rh.setEndTime(times[1]); + rh.setEndTimestamp(DateUtils.toTimestamp(times[1])); + + long diff = (rh.getEndTimestamp() - rh.getStartTimestamp()) / 1000; + XzException400.throwBy(diff <= 0, "结束时间不能大于或等于开始时间"); + + if (customInterval != null && customIntervalUnit != null) { + XzException400.throwBy(customIntervalUnit == TimeUnit.MILLISECONDS, "时间单位最小为秒 [SECONDS]"); + if (customIntervalUnit == TimeUnit.SECONDS) { + rh.setIntervalMs(customInterval * 1000); + } else { + rh.setIntervalMs(customInterval * 1000 * 60); + } + + rh.setInterval(customInterval); + rh.setTimeUnit(customIntervalUnit); + rh.setTitle(String.format("本次统计按每【%s%s】聚合", customInterval, customIntervalUnit)); + } else { + // 自动判断时间 + // 相差的秒数 + // 小于1小时, 按1秒展示, 最多3600条 + if (diff < 3601) { + rh.setIntervalMs(1000); + rh.setInterval(1); + rh.setTimeUnit(TimeUnit.SECONDS); + rh.setTitle("本次统计按每【1】秒种聚合"); + } + // 小于2小时, 按5秒展示, 最多1440条 + else if (diff < 7201) { + rh.setIntervalMs(5000); + rh.setInterval(5); + rh.setTimeUnit(TimeUnit.SECONDS); + rh.setTitle("本次统计按每【1】秒种聚合"); + } + // 小于12小时, 按30秒展示, 最多1440条 + else if (diff < 43201) { + rh.setIntervalMs(30 * 1000); + rh.setInterval(30); + rh.setTimeUnit(TimeUnit.SECONDS); + rh.setTitle("本次统计按每【30】秒种聚合"); + } + // 小于二十四小时, 按1分钟展示, 最多1440条 + else if (diff < 86401) { + rh.setIntervalMs(60 * 1000); + rh.setInterval(1); + rh.setTimeUnit(TimeUnit.MINUTES); + rh.setTitle("本次统计按每【1】分种聚合"); + } + // 大于二十四小时, 按5分钟展示 + else { + rh.setIntervalMs(5 * 60 * 1000); + rh.setInterval(5); + rh.setTimeUnit(TimeUnit.MINUTES); + rh.setTitle("本次统计按每【5】分种聚合"); + } + } + + + // 小于1分钟, 用秒, 否则用分钟来格式化日期 + if (rh.getIntervalMs() < 60 * 1000) { + rh.setPattern(DatePattern.NORM_DATETIME_PATTERN); + } else { + rh.setPattern(DatePattern.NORM_DATETIME_MINUTE_PATTERN); + } + setTitle(rh, repoType); + return rh; + } + + private void setTitle(ResourceHistogram rh, String repoType) { + if ("local".equals(repoType)) { + rh.setSubTitle(String.format( + "1. 机器名【%s(%s)】\n" + + "2. %s\n" + + "3. 单机流量请在右上角选择集群\n" + + "4. 被查询的机器受负载均衡控制", + SystemUtil.getHostName(), SystemUtil.getIp(), rh.getTitle())); + rh.setTitle("单机流量统计"); + } else if ("elasticsearch".equals(repoType)) { + rh.setSubTitle(String.format( + "1. %s\n" + + "2. 当前统计为集群流量", + rh.getTitle())); + rh.setTitle("集群流量统计"); + } + } + + @Data + protected static class ResourceHistogram { + private String startTime; + private String endTime; + private Long startTimestamp; + private Long endTimestamp; + /** + * 日期格式化 + * 按秒格式化:DatePattern.NORM_DATETIME_PATTERN; + * 分钟格式化:DatePattern.NORM_DATETIME_MINUTE_PATTERN; + */ + private String pattern; + /** + * 直方图的解释 + */ + private String title; + private String subTitle; + /** + * 计算区间的范围, 单位毫秒 + */ + private Integer intervalMs; + /** + * 计算区间的范围, 单位是 TimeUnit + */ + private Integer interval; + /** + * 只有秒和分两种 + */ + private TimeUnit timeUnit; + } + + protected OneLineMetric lineToMetric(String line) { + String[] s = line.split("\\|"); + if (s.length != 10) { + log.warn("Sentinel 日志解析格式不正确:{}, 正常情况包含10个字段", line); + return null; + } + OneLineMetric metric = new OneLineMetric(); + metric.setTimestamp(Long.valueOf(s[0])); + metric.setResource(s[1]); + metric.setP(Integer.valueOf(s[2])); + metric.setB(Integer.valueOf(s[3])); + metric.setS(Integer.valueOf(s[4])); + metric.setE(Integer.valueOf(s[5])); + metric.setRt(Integer.valueOf(s[6])); + return metric; + } + + @Data + public static class MetricValue { + private Integer count; + private Integer sumRt; + private Integer minRt; + private Integer maxRt; + + private Integer success; + private Integer exception; + private Integer pass; + private Integer block; + + public static MetricValue init() { + MetricValue value = new MetricValue(); + value.setCount(0); + value.setCount(0); + value.setSumRt(0); + value.setMinRt(0); + value.setMaxRt(0); + value.setSuccess(0); + value.setException(0); + value.setPass(0); + value.setBlock(0); + return value; + } + } + +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/SentinelMetricController.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/SentinelMetricController.java new file mode 100644 index 0000000..a0a6596 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/controller/SentinelMetricController.java @@ -0,0 +1,329 @@ +package com.blossom.expand.sentinel.metric.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.handler.FetchClusterNodeByIdCommandHandler; +import com.alibaba.csp.sentinel.command.handler.FetchClusterNodeHumanCommandHandler; +import com.alibaba.csp.sentinel.command.handler.FetchSimpleClusterNodeCommandHandler; +import com.alibaba.csp.sentinel.command.handler.SendMetricCommandHandler; +import com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.exception.XzException404; +import com.blossom.common.base.exception.XzException500; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.util.DateUtils; +import com.blossom.common.base.util.SortUtil; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.expand.sentinel.metric.pojo.MetricLineRes; +import com.blossom.expand.sentinel.metric.pojo.MetricRes; +import com.blossom.expand.sentinel.metric.pojo.OneLineMetric; +import com.blossom.expand.sentinel.metric.pojo.ResourcesRes; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 流量监控【本地日志】 + * + * @author xzzz + * @apiNote Sentinel 功能逻辑与返回说明见文档: https://www.yuque.com/xiaozeizeizi/learning/rczi6d + */ +@Slf4j +@RestController +@RequestMapping("/sentinel") +public class SentinelMetricController extends AbstractSentinelController { + + /** + * 获取资源列表, Sentinel处理类 {@link FetchSimpleClusterNodeCommandHandler} + */ + private static final String CLUSTER_NODE = "clusterNode"; + + /** + * 获取某个资源信息, Sentinel处理类 {@link FetchClusterNodeByIdCommandHandler} + */ + private static final String CLUSTER_NODE_BY_ID = "clusterNodeById"; + + /** + * 实时指标监控接口, Sentinel处理类 {@link SendMetricCommandHandler} + */ + private static final String METRIC = "metric"; + + /** + * 资源的秒级, 分钟级指标信息, Sentinel处理类 {@link FetchClusterNodeHumanCommandHandler} + */ + private static final String CNODE = "cnode"; + + /** + * 资源列表 + * + * @param id 资源名称 + * @return 资源列表 + * @apiNote 详细信息见: https://www.yuque.com/xiaozeizeizi/learning/rczi6d + */ + @GetMapping("/clusterNode") + public R clusterNode(@RequestParam(value = "id", required = false) String id) { + CommandHandler commandHandler; + if (StrUtil.isBlank(id)) { + commandHandler = SimpleHttpCommandCenter.getHandler(CLUSTER_NODE); + } else { + commandHandler = SimpleHttpCommandCenter.getHandler(CLUSTER_NODE_BY_ID); + } + CommandRequest request = new CommandRequest(); + request.addParam("id", id); + CommandResponse response = commandHandler.handle(request); + return R.ok(JsonUtil.toJsonNode((String) response.getResult())); + } + + /** + * 一天内被请求的资源列表 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 快捷区间, 优先级比日期高 + * @return 资源列表 + * @apiNote 详细信息见: https://www.yuque.com/xiaozeizeizi/learning/rczi6d + */ + @GetMapping("/resources") + public R> resources( + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String interval) { + String[] times = checkTime(startTime, endTime, interval); + startTime = times[0]; + endTime = times[1]; + String all = this.metric("", startTime, endTime, ""); + String[] lines = all.split("\n"); + XzException404.throwBy(lines.length == 0, "无指标数据"); + + Set resources = new HashSet<>(); + Map> resourcesMetricMap = new HashMap<>(20); + + for (String line : lines) { + OneLineMetric metric; + if (StrUtil.isBlank(line) || (metric = lineToMetric(line)) == null) { + continue; + } + List metricList = resourcesMetricMap.getOrDefault(metric.getResource(), new ArrayList<>()); + metricList.add(metric); + resourcesMetricMap.put(metric.getResource(), metricList); + } + + resourcesMetricMap.forEach((resourceName, metrics) -> { + ResourcesRes resourceMetric = new ResourcesRes(); + resourceMetric.setResource(resourceName); + resourceMetric.setSuccess(metrics.stream().mapToInt(OneLineMetric::getS).sum()); + resourceMetric.setAvgRt((double) Math.round(metrics.stream().mapToDouble(OneLineMetric::getRt).average().orElse(0d) * 100) / 100); + resourceMetric.setMaxRt(metrics.stream().mapToInt(OneLineMetric::getRt).max().orElse(0)); + resourceMetric.setMinRt(metrics.stream().mapToInt(OneLineMetric::getRt).min().orElse(0)); + resources.add(resourceMetric); + }); + + + return R.ok( + CollUtil.reverse( + resources.stream() + .sorted((r1, r2) -> SortUtil.intSort.compare(r1.getSuccess(), r2.getSuccess())). + collect(Collectors.toList()) + ) + ); + } + + + /** + * 资源的监控信息 + * + * @param resource 资源名称, 如果不传入资源名称, 则搜索全部资源 + * @param startTime 开始时间 + * @param endTime 结束时间, 如果不传入结束时间, 则按日志最大行数搜索 + * @param maxLines 文件中查询最大行数, sentinel 本地查询最大 12000 行 + * @return 资源监控 + */ + @GetMapping("/metric") + public String metric(@RequestParam(required = false) final String resource, + String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String maxLines) { + CommandRequest request = new CommandRequest(); + if (StrUtil.isNotBlank(resource)) { + // sentinel 原生API字段为 identity + request.addParam("identity", resource); + } + if (StrUtil.isNotBlank(endTime)) { + request.addParam("endTime", String.valueOf(DateUtils.toTimestamp(endTime))); + } + XzException400.throwBy(StrUtil.isBlank(startTime), "开始日期为必填项"); + XzException400.throwBy(StrUtil.isBlank(endTime) && StrUtil.isBlank(maxLines), "未填结束日期时, 日志搜索行数不得为空"); + + request.addParam("startTime", String.valueOf(DateUtils.toTimestamp(startTime))); + request.addParam("maxLines", maxLines); + + CommandHandler commandHandler = SimpleHttpCommandCenter.getHandler(METRIC); + XzException500.throwBy(commandHandler == null, "系统当前无访问流量,Sentinel 尚未初始化,请访问后再查询此接口"); + + // 如果命令处理类存在, 则用该类处理该命令 + CommandResponse response = commandHandler.handle(request); + return (String) response.getResult(); + } + + /** + * 资源折线图 + * + * @param resource 资源名称 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 快捷区间, 用于约定时间范围, 会自定生成开始和结束时间 + * @param customInterval 自定义单位聚合区间, 如 1m, 10m + * @param customIntervalUnit 自定义单位聚合区间的时间单位, 如 SECONDS, MINUTES + * @return EChart 使用的数据结构 + */ + @GetMapping("/metric/line") + public R metricLine(final String resource, + String startTime, + String endTime, + String interval, + Integer customInterval, + TimeUnit customIntervalUnit) { + log.info("查询 Sentinel 单机流量折线图"); + ResourceHistogram rh = explain(startTime, endTime, interval, customInterval, customIntervalUnit, "local"); + // 读取日志 + String all = this.metric(resource, rh.getStartTime(), rh.getEndTime(), ""); + String[] lines = all.split("\n"); + XzException500.throwBy(lines.length == 0, "无流量指标数据, 请等待服务接收请求并产生日期"); + + // 初始化区间内所有的日期 + Map metricMap = new LinkedHashMap<>(); + for (long thisTimestamp = rh.getStartTimestamp(); thisTimestamp <= rh.getEndTimestamp(); thisTimestamp += rh.getIntervalMs()) { + long remainder = thisTimestamp % rh.getIntervalMs(); + // 一个区间的开始日期 + long thisTimeInterval = thisTimestamp - remainder; + metricMap.put(DateUtils.format(DateUtils.date(thisTimeInterval), rh.getPattern()), MetricValue.init()); + } + + // 单时间节点的监控信息, 按时间插入, [日志时间:日志统计结果] + for (String line : lines) { + if (StrUtil.isBlank(line)) { + continue; + } + OneLineMetric lineMetric; + if (StrUtil.isBlank(line) || (lineMetric = lineToMetric(line)) == null) { + continue; + } + + // 计算日期区间 + long logHistogramDt = lineMetric.getTimestamp() - (lineMetric.getTimestamp() % rh.getIntervalMs()); + String metricMapKey = DateUtils.format(DateUtils.date(logHistogramDt), rh.getPattern()); + + MetricValue value = metricMap.get(metricMapKey); + if (value == null) { + continue; + } + value = metricMap.get(metricMapKey); + value.setSuccess(value.getSuccess() + lineMetric.getS()); + value.setException(value.getException() + lineMetric.getE()); + value.setPass(value.getPass() + lineMetric.getP()); + value.setBlock(value.getBlock() + lineMetric.getB()); + value.setSumRt(value.getSumRt() + lineMetric.getRt()); + value.setCount(value.getCount() + 1); + Integer minRt = value.getMinRt(); + Integer maxRt = value.getMaxRt(); + if (lineMetric.getRt() < minRt) { + value.setMinRt(lineMetric.getRt()); + } + if (lineMetric.getRt() > maxRt) { + value.setMaxRt(lineMetric.getRt()); + } + metricMap.put(metricMapKey, value); + } + + final MetricLineRes metrics = new MetricLineRes(metricMap.size()); + metrics.setResource(resource); + metrics.setTitle(rh.getTitle()); + metrics.setSubTitle(rh.getSubTitle()); + int i = 0; + for (Map.Entry entry : metricMap.entrySet()) { + metrics.addMetric(entry.getKey(), entry.getValue(), i++); + } + return R.ok(metrics); + } + + /** + * 集群过去24小时的总体信息(__total_inbound_traffic__) + * + * @return 集群流量信息 + */ + @GetMapping("/metric/app") + public R metricTotalInboundTraffic(@RequestParam(required = false) String appName) { + log.info("查询应用24小时流量统计"); + String[] times = checkTime(-24); + String startTime = times[0]; + String endTime = times[1]; + + String all = this.metric("__total_inbound_traffic__", startTime, endTime, ""); + String[] lines = all.split("\n"); + XzException500.throwBy(lines.length == 0, "无指标数据"); + MetricRes res = MetricRes.empty(); + for (String line : lines) { + if (StrUtil.isBlank(line)) { + continue; + } + OneLineMetric metric; + if (StrUtil.isBlank(line) || (metric = lineToMetric(line)) == null) { + continue; + } + + res.setPass(res.getPass() + metric.getP()); + res.setBlock(res.getBlock() + metric.getB()); + res.setSuccess(res.getSuccess() + metric.getS()); + res.setException(res.getException() + metric.getE()); + res.setAvgRt(res.getAvgRt() + metric.getRt()); + + Integer maxQps = res.getMaxQps(); + if (metric.getS() > maxQps) { + res.setMaxQps(metric.getS()); + } + + Integer minRt = res.getMinRt(); + if (metric.getRt() < minRt) { + res.setMinRt(metric.getRt()); + } + + Integer maxRt = res.getMaxRt(); + if (metric.getRt() > maxRt) { + res.setMaxRt(metric.getRt()); + } + } + + res.setAvgRt((double) Math.round(res.getAvgRt() / lines.length)); + res.setAvgQps(res.getSuccess() / lines.length); + return R.ok(res); + } + + + /** + * 资源的秒级, 分钟级指标信息 + * + * @param id 资源名称 + * @return 秒级, 分钟级指标信息 + */ + @GetMapping("/cnode") + public String cnode(String id) { + CommandRequest request = new CommandRequest(); + request.addParam("id", id); + CommandHandler commandHandler = SimpleHttpCommandCenter.getHandler(CNODE); + // 如果命令处理类存在, 则用该类处理该命令 + CommandResponse response = commandHandler.handle(request); + return (String) response.getResult(); + } + +} + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricLineRes.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricLineRes.java new file mode 100644 index 0000000..48e7627 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricLineRes.java @@ -0,0 +1,70 @@ +package com.blossom.expand.sentinel.metric.pojo; + +import com.blossom.expand.sentinel.metric.controller.AbstractSentinelController; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * 资源直方图信息 + * + * @author xzzz + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetricLineRes { + + public MetricLineRes() { + } + + public MetricLineRes(Integer size) { + this.x = new String[size]; + this.s = new Integer[size]; + this.e = new Integer[size]; + this.p = new Integer[size]; + this.b = new Integer[size]; + this.minRt = new Integer[size]; + this.maxRt = new Integer[size]; + this.avgRt = new Double[size]; + } + + /** + * 资源名称 + */ + private String resource; + /** + * 标题 + */ + private String title; + /** + * 副标题 + */ + private String subTitle; + + private String[] x; + private Integer[] s; + private Integer[] e; + private Integer[] p; + private Integer[] b; + private Integer[] minRt; + private Integer[] maxRt; + private Double[] avgRt; + + public void addMetric(MetricRes metric) { + } + + + public void addMetric(String datetime, AbstractSentinelController.MetricValue v, Integer index) { + x[index] = datetime.substring(5).replaceAll(" ", "\n"); + s[index] = v.getSuccess(); + e[index] = v.getException(); + p[index] = v.getPass(); + b[index] = v.getBlock(); + minRt[index] = v.getMinRt(); + maxRt[index] = v.getMaxRt(); + if (v.getSumRt() == 0 || v.getCount() == 0) { + avgRt[index] = 0.0; + } else if (v.getSumRt() != 0 && v.getCount() != 0) { + avgRt[index] = (double) Math.round((double) v.getSumRt() / v.getCount()); + } + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricRes.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricRes.java new file mode 100644 index 0000000..ea0ba3c --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/MetricRes.java @@ -0,0 +1,150 @@ +package com.blossom.expand.sentinel.metric.pojo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * 应用流量统计 + * + * @author xzzz + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetricRes { + /** + * 指标时间 + */ + private String datetime; + /** + * 最大QPS + */ + private Integer maxQps; + /** + * 平均QPS + */ + private Integer avgQps; + /** + * 成功的请求数,success = pass + block + */ + private Integer success; + /** + * 异常的请求数 + */ + private Integer exception; + /** + * 通过的请求数 + */ + private Integer pass; + /** + * 阻塞的请求数 + */ + private Integer block; + /** + * 最小响应时间 ms + */ + private Integer minRt; + /** + * 最大响应时间 ms + */ + private Integer maxRt; + /** + * 平均响应时间 ms + */ + private Double avgRt; + + public void setMaxQps(Integer maxQps) { + if (invalid(maxQps)) { + this.maxQps = 0; + } else { + this.maxQps = maxQps; + } + } + + public void setAvgQps(Integer avgQps) { + if (invalid(avgQps)) { + this.avgQps = 0; + } else { + this.avgQps = avgQps; + } + } + + public void setSuccess(Integer success) { + if (invalid(success)) { + this.success = 0; + } else { + this.success = success; + } + } + + public void setException(Integer exception) { + if (invalid(exception)) { + this.exception = 0; + } else { + this.exception = exception; + } + } + + public void setPass(Integer pass) { + if (invalid(pass)) { + this.pass = 0; + } else { + this.pass = pass; + } + } + + public void setBlock(Integer block) { + if (invalid(block)) { + this.block = 0; + } else { + this.block = block; + } + } + + public void setMinRt(Integer minRt) { + if (invalid(minRt)) { + this.minRt = 0; + } else { + this.minRt = minRt; + } + } + + public void setMaxRt(Integer maxRt) { + if (invalid(maxRt)) { + this.maxRt = 0; + } else { + this.maxRt = maxRt; + } + } + + public void setAvgRt(Double avgRt) { +// if (invalid(avgRt)) { +// this.avgRt = 0; +// } else { + this.avgRt = avgRt; +// } + } + + /** + * 如果参数为 MAX_VALUE/MIN_VALUE, 则数据是无效的 + */ + private boolean invalid(Integer i) { + if (i == null) { + return true; + } + return (i == Integer.MAX_VALUE || i == Integer.MIN_VALUE); + } + + public static MetricRes empty() { + MetricRes metricRes = new MetricRes(); + metricRes.setSuccess(0); + metricRes.setException(0); + metricRes.setPass(0); + metricRes.setBlock(0); + metricRes.setMinRt(0); + metricRes.setMaxRt(0); + metricRes.setAvgRt(0.00); + metricRes.setMaxQps(0); + return metricRes; + } +} + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/OneLineMetric.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/OneLineMetric.java new file mode 100644 index 0000000..ebc08a8 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/OneLineMetric.java @@ -0,0 +1,24 @@ +package com.blossom.expand.sentinel.metric.pojo; + +import lombok.Data; + +/** + * Sentinel 原生日志字段 + *

详细信息见: https://www.yuque.com/xiaozeizeizi/learning/vdxicg + * + * @author xzzz + */ +@Data +public class OneLineMetric { + private Long timestamp; + private String datetime; + private String resource; + private Integer p; + private Integer b; + private Integer s; + private Integer e; + private Integer rt; + private Integer o; + private Integer c; + private Integer cf; +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourceAggsRes.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourceAggsRes.java new file mode 100644 index 0000000..06506e9 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourceAggsRes.java @@ -0,0 +1,49 @@ +package com.blossom.expand.sentinel.metric.pojo; + +import lombok.Data; +import lombok.Getter; + +/** + * 资源聚合 + * + * @author xzzz + */ +@Data +public class ResourceAggsRes { + + private String resourceName; + private Integer value; + private Integer rt; + private Integer count; + + public ResourceAggsRes(String resourceName, Integer value, Integer rt) { + this.resourceName = resourceName; + this.value = value; + this.rt = rt; + } + + @Getter + public static class StatisticDTO { + // 统计次数 + private Integer count; + // 请求数 + private Integer value; + // 响应时间总和 + private Integer rt; + + public StatisticDTO() { + this.count = 0; + this.rt = 0; + this.value = 0; + } + + public void increaseValue(Integer value) { + this.value += value; + } + + public void increaseRt(Integer rt) { + this.rt += rt; + this.count++; + } + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourcesRes.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourcesRes.java new file mode 100644 index 0000000..0bf806f --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/pojo/ResourcesRes.java @@ -0,0 +1,36 @@ +package com.blossom.expand.sentinel.metric.pojo; + +import lombok.Data; + +/** + * 资源信息 + * + * @author xzzz + */ +@Data +public class ResourcesRes { + /** + * 资源名 + */ + private String resource; + /** + * 统计该资源所用的文档数,相当于多少秒的指标信息 + */ + private Long docCount; + /** + * 最小响应时间 毫秒 + */ + private Integer minRt; + /** + * 最大响应时间 毫秒 + */ + private Integer maxRt; + /** + * 平均响应时间 毫秒 + */ + private Double avgRt; + /** + * 请求成功数 + */ + private Integer success; +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/util/SentinelUtil.java b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/util/SentinelUtil.java new file mode 100644 index 0000000..ae7ad85 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/java/com/blossom/expand/sentinel/metric/util/SentinelUtil.java @@ -0,0 +1,174 @@ +package com.blossom.expand.sentinel.metric.util; + +import cn.hutool.core.util.StrUtil; + +import java.util.HashSet; +import java.util.Set; + +/** + * 优化Sentinel使用的一些工具方法 + * + * @author xzzz + */ +public class SentinelUtil { + + private static final Set ignoreResource = new HashSet() {{ + this.add("__total_inbound_traffic__"); + this.add("__system_load__"); + this.add("__cpu_usage__"); + this.add("/webStatus"); + }}; + + /** + * 判断资源是否需要忽略, 一般是全局的资源在某些情况下是需要忽略的 + * @param resourceName 资源名称 + * @return 是否忽略 + */ + public static boolean ignoreResource(String resourceName) { + return ignoreResource.contains(resourceName); + } + + /** + * 简化的 resourceName + * + *

    + *
  1. http 资源会忽略路径的中间值
  2. + *
      + *
    • 完整资源名: /aaa/bbb/ccc
    • + *
    • 简化资源名: /aaa/.../ccc
    • + *
    + * + *
  3. dubbo 资源会简化只保留方法名和参数类名:
  4. + *
      + *
    • 简化资源名: login(DubboLoginDTO)
    • + *
    + *
+ * + * @param resourceName 资源名称 + * @return 简化的资源名称 + */ + public static String tinyResourceName(String resourceName) { + if (ignoreResource(resourceName)) { + return resourceName; + } + + try { + // 简化HTTP资源名 + if (isHttp(resourceName)) { + return tinyHttpResourceName(resourceName); + } + // 简化RPC资源名 + else if (isRpc(resourceName)) { + return tinyRpcResourceName(resourceName); + } + } catch (Exception e) { + // 简化失败 + } + + return resourceName; + } + + + // region 简化HTTP资源名 + + /** + * 检查是否是HTTP资源 + * @param resourceName 资源名称 + * @return 是否 + */ + public static boolean isHttp(String resourceName) { + if (StrUtil.isBlank(resourceName)) { + return false; + } + return resourceName.startsWith("/"); + } + + /** + * 简化 HTTP 资源名称 + * @param resourceName 资源名 + * @return 简化资源名 + */ + public static String tinyHttpResourceName(String resourceName) { + if (countStr(resourceName,"/") <= 2) { + return resourceName; + } + + // 字符串第二次出现的位置 + int second = index2of(resourceName, "/") + 1; + int last = StrUtil.lastIndexOfIgnoreCase(resourceName, "/"); + + return resourceName.substring(0, second) + "..." + resourceName.substring(last); + } + + // endregion + + + /** + * 是否RPC请求, 如果资源中包含 "." 和 "()" 则认为是RPC请求 + * @param resourceName 资源名 + * @return 是否RPC + */ + public static boolean isRpc(String resourceName) { + if (StrUtil.isBlank(resourceName)) { + return false; + } + if (StrUtil.indexOf(resourceName, '.') != -1 && + StrUtil.indexOf(resourceName, ':') != -1 && + StrUtil.indexOf(resourceName, '(') != -1 && + StrUtil.indexOf(resourceName, ')') != -1) { + return true; + } + return false; + } + + /** + * 简化 DUBBO 资源名 + * @param resourceName 资源名 + * @return 简化资源名 + */ + public static String tinyRpcResourceName(String resourceName) { + String method = resourceName.substring(resourceName.indexOf(":") + 1, resourceName.indexOf("(")); + + String argsStr = resourceName.substring(resourceName.indexOf("(") + 1, resourceName.indexOf(")")); + + String[] args = argsStr.split(","); + if (args.length <= 1) { + return method + "(" + argsStr.substring(StrUtil.lastIndexOfIgnoreCase(argsStr,".") + 1) + ")"; + } else { + StringBuilder argsBuilder = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + argsBuilder.append(","); + } + argsBuilder.append(args[i].substring(StrUtil.lastIndexOfIgnoreCase(args[i],".") + 1)); + } + return method + "(" + argsBuilder.toString() + ")"; + } + } + + + /** + * 查询字符第二次出现的位置 + * @param resourceName 字符串 + * @param searchStr 被查询的字符 + * @return 在字符串的位置 + */ + private static int index2of(String resourceName, String searchStr) { + int first = StrUtil.indexOfIgnoreCase(resourceName, searchStr) + 1; + return StrUtil.indexOfIgnoreCase(resourceName.substring(first), searchStr) + first; + } + + /** + * 计算字符串出现的次数 + * @param str 字符串 + * @param searchStr 被查询的字符 + * @return 次数 + */ + private static int countStr(String str, String searchStr) { + if (StrUtil.isBlank(str) || StrUtil.isBlank(searchStr)) { + return 0; + } + + return str.split(searchStr).length - 1; + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/resources/META-INF/spring.factories b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a2da08c --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-metric/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.expand.sentinel.metric.config.SentinelMetricConfiguration + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/pom.xml b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/pom.xml new file mode 100644 index 0000000..bab895b --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/pom.xml @@ -0,0 +1,14 @@ + + + + com.blossom + expand-sentinel + 1.0.0-SNAPSHOT + + 4.0.0 + + expand-sentinel-springmvc + + diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/AbstractSentinelInterceptor.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/AbstractSentinelInterceptor.java new file mode 100644 index 0000000..f5a2699 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/AbstractSentinelInterceptor.java @@ -0,0 +1,200 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc; + +import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.blossom.expand.sentinel.mvc.config.BaseWebMvcConfig; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Since request may be reprocessed in flow if any forwarding or including or other action + * happened (see {@link javax.servlet.ServletRequest#getDispatcherType()}) we will only + * deal with the initial request. So we use reference count to track in + * dispathing "onion" though which we could figure out whether we are in initial type "REQUEST". + * That means the sub-requests which we rarely meet in practice will NOT be recorded in Sentinel. + *

+ * How to implement a forward sub-request in your action: + *

+ * initalRequest() {
+ *     ModelAndView mav = new ModelAndView();
+ *     mav.setViewName("another");
+ *     return mav;
+ * }
+ * 
+ * + * @author kaizi2009 + * @since 1.7.1 + */ +public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { + + public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context"; + private static final String EMPTY_ORIGIN = ""; + + private final BaseWebMvcConfig baseWebMvcConfig; + + public AbstractSentinelInterceptor(BaseWebMvcConfig config) { + AssertUtil.notNull(config, "BaseWebMvcConfig should not be null"); + AssertUtil.assertNotBlank(config.getRequestAttributeName(), "requestAttributeName should not be blank"); + this.baseWebMvcConfig = config; + } + + /** + * @param request + * @param rcKey + * @param step + * @return reference count after increasing (initial value as zero to be increased) + */ + private Integer increaseReferece(HttpServletRequest request, String rcKey, int step) { + Object obj = request.getAttribute(rcKey); + + if (obj == null) { + // initial + obj = Integer.valueOf(0); + } + + Integer newRc = (Integer) obj + step; + request.setAttribute(rcKey, newRc); + return newRc; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String resourceName = getResourceName(request); + + if (StringUtil.isEmpty(resourceName)) { + return true; + } + + if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) { + return true; + } + + // Parse the request origin using registered origin parser. + String origin = parseOrigin(request); + String contextName = getContextName(request); + ContextUtil.enter(contextName, origin); + Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry); + return true; + } catch (BlockException e) { + try { + handleBlockException(request, response, e); + } finally { + ContextUtil.exit(); + } + return false; +// throw e; + } + } + + /** + * Return the resource name of the target web resource. + * + * @param request web request + * @return the resource name of the target web resource. + */ + protected abstract String getResourceName(HttpServletRequest request); + + /** + * Return the context name of the target web resource. + * + * @param request web request + * @return the context name of the target web resource. + */ + protected String getContextName(HttpServletRequest request) { + return SENTINEL_SPRING_WEB_CONTEXT_NAME; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) { + return; + } + + Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName()); + if (entry == null) { + // should not happen + RecordLog.warn("[{}] No entry found in request, key: {}", + getClass().getSimpleName(), baseWebMvcConfig.getRequestAttributeName()); + return; + } + + if (ex == null) { + ex = SpringInterceptorUtil.getException(request); + } + + traceExceptionAndExit(entry, ex); + removeEntryInRequest(request); + ContextUtil.exit(); + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + } + + protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) { + Object entryObject = request.getAttribute(attrKey); + return entryObject == null ? null : (Entry) entryObject; + } + + protected void removeEntryInRequest(HttpServletRequest request) { + request.removeAttribute(baseWebMvcConfig.getRequestAttributeName()); + request.removeAttribute("request_attribute_sentinel_exception"); + } + + protected void traceExceptionAndExit(Entry entry, Exception ex) { + if (entry != null) { + if (ex != null) { + Tracer.traceEntry(ex, entry); + } + entry.exit(); + } + } + + protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e) + throws Exception { + if (baseWebMvcConfig.getBlockExceptionHandler() != null) { + baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e); + } else { + // Throw BlockException directly. Users need to handle it in Spring global exception handler. + throw e; + } + } + + protected String parseOrigin(HttpServletRequest request) { + String origin = EMPTY_ORIGIN; + if (baseWebMvcConfig.getOriginParser() != null) { + origin = baseWebMvcConfig.getOriginParser().parseOrigin(request); + if (StringUtil.isEmpty(origin)) { + return EMPTY_ORIGIN; + } + } + return origin; + } + +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebInterceptor.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebInterceptor.java new file mode 100644 index 0000000..889ecf9 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebInterceptor.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.blossom.expand.sentinel.mvc.callback.DefaultBlockExceptionHandler; +import com.blossom.expand.sentinel.mvc.callback.UrlCleaner; +import com.blossom.expand.sentinel.mvc.config.SentinelWebMvcConfig; +import org.springframework.web.servlet.HandlerMapping; + +import javax.servlet.http.HttpServletRequest; + +/** + * Spring Web MVC interceptor that integrates with Sentinel. + * + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebInterceptor extends AbstractSentinelInterceptor { + + private final SentinelWebMvcConfig config; + + public SentinelWebInterceptor() { + this(new SentinelWebMvcConfig()); + } + + public SentinelWebInterceptor(SentinelWebMvcConfig config) { + super(config); + if (config == null) { + // Use the default config by default. + this.config = new SentinelWebMvcConfig(); + // 设置阻塞异常处理器 + } else { + this.config = config; + } + if (this.config.getBlockExceptionHandler() == null) { + this.config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + } + } + + @Override + protected String getResourceName(HttpServletRequest request) { + // Resolve the Spring Web URL pattern from the request attribute. + Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (resourceNameObject == null || !(resourceNameObject instanceof String)) { + return null; + } + String resourceName = (String) resourceNameObject; + UrlCleaner urlCleaner = config.getUrlCleaner(); + if (urlCleaner != null) { + resourceName = urlCleaner.clean(resourceName); + } + // Add method specification if necessary + if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) { + resourceName = request.getMethod().toUpperCase() + ":" + resourceName; + } + return resourceName; + } + + @Override + protected String getContextName(HttpServletRequest request) { + if (config.isWebContextUnify()) { + return super.getContextName(request); + } + + return getResourceName(request); + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebTotalInterceptor.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebTotalInterceptor.java new file mode 100644 index 0000000..9f819cb --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SentinelWebTotalInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc; + + + +import com.blossom.expand.sentinel.mvc.config.SentinelWebMvcTotalConfig; + +import javax.servlet.http.HttpServletRequest; + +/** + * The web interceptor for all requests, which will unify all URL as + * a single resource name (configured in {@link SentinelWebMvcTotalConfig}). + * + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebTotalInterceptor extends AbstractSentinelInterceptor { + + private final SentinelWebMvcTotalConfig config; + + public SentinelWebTotalInterceptor(SentinelWebMvcTotalConfig config) { + super(config); + if (config == null) { + this.config = new SentinelWebMvcTotalConfig(); + } else { + this.config = config; + } + } + + public SentinelWebTotalInterceptor() { + this(new SentinelWebMvcTotalConfig()); + } + + @Override + protected String getResourceName(HttpServletRequest request) { + return config.getTotalResourceName(); + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SpringInterceptorUtil.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SpringInterceptorUtil.java new file mode 100644 index 0000000..4f8b79b --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/SpringInterceptorUtil.java @@ -0,0 +1,20 @@ +package com.blossom.expand.sentinel.mvc; + +import javax.servlet.http.HttpServletRequest; + +public class SpringInterceptorUtil { + + private static final String REQUEST_ATTRIBUTE_SENTINEL_EXCEPTION = "request_attribute_sentinel_exception"; + + public static void fillException(HttpServletRequest request, Exception ex) { + request.setAttribute(REQUEST_ATTRIBUTE_SENTINEL_EXCEPTION, ex); + } + + public static Exception getException(HttpServletRequest request) { + Object ex = request.getAttribute(REQUEST_ATTRIBUTE_SENTINEL_EXCEPTION); + if (ex != null) { + return (Exception) ex; + } + return null; + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/BlockExceptionHandler.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/BlockExceptionHandler.java new file mode 100644 index 0000000..4a0fed6 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/BlockExceptionHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.callback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handler for the blocked request. + * + * @author kaizi2009 + */ +public interface BlockExceptionHandler { + + /** + * Handle the request when blocked. + * + * @param request Servlet request + * @param response Servlet response + * @param e the block exception + * @throws Exception users may throw out the BlockException or other error occurs + */ + void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception; + +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/DefaultBlockExceptionHandler.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/DefaultBlockExceptionHandler.java new file mode 100644 index 0000000..b09f391 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/DefaultBlockExceptionHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.callback; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.blossom.common.base.pojo.R; +import com.blossom.common.base.pojo.RCode; +import com.blossom.common.base.util.json.JsonUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Default handler for the blocked request. + * + * @author kaizi2009 + */ +public class DefaultBlockExceptionHandler implements BlockExceptionHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + // Return 429 (Too Many Requests) by default. + response.setStatus(200); + + StringBuffer url = request.getRequestURL(); +// +// if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { +// url.append("?").append(request.getQueryString()); +// } + + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + + Map flowRule = new HashMap<>(); + flowRule.put("flowRule", JsonUtil.toJsonNode(e.getRule())); + + R> r = R.fault(RCode.TOO_MANY_REQUESTS.getCode(), RCode.TOO_MANY_REQUESTS.getMsg(), e.getMessage()); + r.setApi(url.toString()); + r.setData(flowRule); + r.setEx(StrUtil.isBlank(e.getMessage()) ? e.getClass().getName() : e.getMessage()); + + PrintWriter out = response.getWriter(); + out.print(JsonUtil.toJson(r)); + out.flush(); + out.close(); + } + +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/RequestOriginParser.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/RequestOriginParser.java new file mode 100644 index 0000000..450cef0 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/RequestOriginParser.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.callback; + +import javax.servlet.http.HttpServletRequest; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author kaizi2009 + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(HttpServletRequest request); +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/UrlCleaner.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/UrlCleaner.java new file mode 100644 index 0000000..57ea3a3 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/callback/UrlCleaner.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.callback; + +/** + * Unify the resource target. + * + * @author kaizi2009 + */ +public interface UrlCleaner { + + /** + * Unify the resource target. + * + * @param originUrl the original URL + * @return the unified resource name + */ + String clean(String originUrl); +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/BaseWebMvcConfig.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/BaseWebMvcConfig.java new file mode 100644 index 0000000..782da18 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/BaseWebMvcConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.config; + + +import com.blossom.expand.sentinel.mvc.callback.BlockExceptionHandler; +import com.blossom.expand.sentinel.mvc.callback.RequestOriginParser; + +/** + * Common base configuration for Spring Web MVC adapter. + * + * @author kaizi2009 + * @since 1.7.1 + */ +public abstract class BaseWebMvcConfig { + + protected String requestAttributeName; + protected String requestRefName; + protected BlockExceptionHandler blockExceptionHandler; + protected RequestOriginParser originParser; + + public String getRequestAttributeName() { + return requestAttributeName; + } + + public void setRequestAttributeName(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + this.requestRefName = this.requestAttributeName + "-rc"; + } + + /** + * Paired with attr name used to track reference count. + * + * @return + */ + public String getRequestRefName() { + return requestRefName; + } + + public BlockExceptionHandler getBlockExceptionHandler() { + return blockExceptionHandler; + } + + public void setBlockExceptionHandler(BlockExceptionHandler blockExceptionHandler) { + this.blockExceptionHandler = blockExceptionHandler; + } + + public RequestOriginParser getOriginParser() { + return originParser; + } + + public void setOriginParser(RequestOriginParser originParser) { + this.originParser = originParser; + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelInterceptorConfig.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelInterceptorConfig.java new file mode 100644 index 0000000..7ffda50 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelInterceptorConfig.java @@ -0,0 +1,34 @@ +package com.blossom.expand.sentinel.mvc.config; + +import com.blossom.expand.sentinel.mvc.SentinelWebInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.PostConstruct; + +/** + * @author : xzzz + */ +@Slf4j +@Configuration +public class SentinelInterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(sentinelWebInterceptor()).addPathPatterns("/**"); + } + + @Bean + public SentinelWebInterceptor sentinelWebInterceptor() { + return new SentinelWebInterceptor(); + } + + @PostConstruct + public void init() { + log.info("[SENTINEL] 已适配 SPRING_MVC BY INTERCEPTOR"); + } + +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcConfig.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcConfig.java new file mode 100644 index 0000000..c094ac9 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcConfig.java @@ -0,0 +1,91 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.config; + + +import com.blossom.expand.sentinel.mvc.callback.UrlCleaner; +import lombok.extern.slf4j.Slf4j; + +/** + * @author kaizi2009 + * @since 1.7.1 + */ +@Slf4j +public class SentinelWebMvcConfig extends BaseWebMvcConfig { + + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_entry_attr"; + + /** + * Specify the URL cleaner that unifies the URL resources. + */ + private UrlCleaner urlCleaner; + + /** + * Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}). + */ + private boolean httpMethodSpecify; + + /** + * Specify whether unify web context(i.e. use the default context name), and is true by default. + * + * @since 1.7.2 + */ + private boolean webContextUnify = true; + + public SentinelWebMvcConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public SentinelWebMvcConfig setUrlCleaner(UrlCleaner urlCleaner) { + this.urlCleaner = urlCleaner; + return this; + } + + public boolean isHttpMethodSpecify() { + return httpMethodSpecify; + } + + public SentinelWebMvcConfig setHttpMethodSpecify(boolean httpMethodSpecify) { + this.httpMethodSpecify = httpMethodSpecify; + return this; + } + + public boolean isWebContextUnify() { + return webContextUnify; + } + + public SentinelWebMvcConfig setWebContextUnify(boolean webContextUnify) { + this.webContextUnify = webContextUnify; + return this; + } + + @Override + public String toString() { + return "SentinelWebMvcConfig{" + + "urlCleaner=" + urlCleaner + + ", httpMethodSpecify=" + httpMethodSpecify + + ", webContextUnify=" + webContextUnify + + ", requestAttributeName='" + requestAttributeName + '\'' + + ", blockExceptionHandler=" + blockExceptionHandler + + ", originParser=" + originParser + + '}'; + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcTotalConfig.java b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcTotalConfig.java new file mode 100644 index 0000000..5bc4ac3 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/java/com/blossom/expand/sentinel/mvc/config/SentinelWebMvcTotalConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blossom.expand.sentinel.mvc.config; + + +/** + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebMvcTotalConfig extends BaseWebMvcConfig { + + public static final String DEFAULT_TOTAL_RESOURCE_NAME = "spring-mvc-total-url-request"; + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_total_entry_attr"; + + private String totalResourceName = DEFAULT_TOTAL_RESOURCE_NAME; + + public SentinelWebMvcTotalConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public String getTotalResourceName() { + return totalResourceName; + } + + public SentinelWebMvcTotalConfig setTotalResourceName(String totalResourceName) { + this.totalResourceName = totalResourceName; + return this; + } + + @Override + public String toString() { + return "SentinelWebMvcTotalConfig{" + + "totalResourceName='" + totalResourceName + '\'' + + ", requestAttributeName='" + requestAttributeName + '\'' + + ", blockExceptionHandler=" + blockExceptionHandler + + ", originParser=" + originParser + + '}'; + } +} diff --git a/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/resources/META-INF/spring.factories b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..368d9f2 --- /dev/null +++ b/blossom-backend/expand-sentinel/expand-sentinel-springmvc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.expand.sentinel.mvc.config.SentinelInterceptorConfig + diff --git a/blossom-backend/expand-sentinel/pom.xml b/blossom-backend/expand-sentinel/pom.xml new file mode 100644 index 0000000..1335ac6 --- /dev/null +++ b/blossom-backend/expand-sentinel/pom.xml @@ -0,0 +1,48 @@ + + + + blossom-backend + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + pom + + expand-sentinel + sentinel的增加功能,拓展了ControllerAdvice的异常流控归属 + + + expand-sentinel-metric + expand-sentinel-springmvc + + + + 1.8.3 + + + + + + com.blossom + common-base + + + + + com.alibaba.csp + sentinel-core + ${sentinel.version} + + + + + com.alibaba.csp + sentinel-transport-simple-http + ${sentinel.version} + + + + + \ No newline at end of file diff --git a/blossom-backend/expand-tracker/expand-tracker-core/pom.xml b/blossom-backend/expand-tracker/expand-tracker-core/pom.xml new file mode 100644 index 0000000..a3a0dd4 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/pom.xml @@ -0,0 +1,44 @@ + + + + expand-tracker + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + expand-tracker-core + 单机版的链路追踪,删除了很多分布式中间件链路和RPC拦截 + + + + + com.blossom + common-base + + + + + com.blossom + common-cache + true + + + + com.blossom + common-db + true + + + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + \ No newline at end of file diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanContext.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanContext.java new file mode 100644 index 0000000..b5995d7 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanContext.java @@ -0,0 +1,57 @@ +package com.blossom.expand.tracker.core; + +import com.blossom.common.base.util.TimeUtil; + +/** + * span 的上下文对象,主要负责记录相同线程上下文中的多个 span, 并构造成一个链表 + * + * @author xzzz + */ +public class SpanContext { + + private SpanNode curSpan; + + public SpanNode getCurSpan() { + return this.curSpan; + } + + /** + *

将新的 span 添加到当前上下文中, 并作为当前 span 记录. 如果在此之前添加过 span, 则多个 span 之间构造成一个双向链表, + * 新的 span 会添加进链表尾部. + *

同时, 新添加的的 span 会使用旧 span 的 traceId, 即新 span 不会成为一个新的独立的追踪, 而是并入到当前追踪中. + * @param currentSpan 返回 + */ + protected void add(final SpanNode currentSpan) { + if (curSpan == null) { + curSpan = currentSpan; + return; + } + + final SpanNode lastSpan = curSpan; + lastSpan.setNext(currentSpan); + + currentSpan.setTraceId(lastSpan.getTraceId()); + currentSpan.setSpanParentId(lastSpan.getSpanId()); + currentSpan.setLast(lastSpan); + + this.curSpan = currentSpan; + } + + /** + *

删除当前 span, 如果当前 span 存在上一个 span, 删除后将上一个 span 设置为当前 span 并返回 + * + * @return + */ + protected SpanNode remove() { + final SpanNode currentSpan = curSpan; + currentSpan.setSpanEnd(TimeUtil.currentTimeMillis()); + SpanNode last = currentSpan.getLast(); + if (last != null) { + last.setNext(null); + curSpan = last; + } else { + curSpan = null; + } + return last; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanNode.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanNode.java new file mode 100644 index 0000000..4dc90af --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/SpanNode.java @@ -0,0 +1,154 @@ +package com.blossom.expand.tracker.core; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.id.TraceIdGeneratorUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.blossom.common.base.exception.XzException400; +import com.blossom.common.base.util.TimeUtil; +import com.blossom.common.base.util.json.JsonUtil; +import lombok.Data; + +import java.io.Serializable; +import java.util.TreeMap; + + +/** + * span 对象 + *

span 记录了一次追踪中的多个节点, + * + * @author xzzz + * @since 0.0.1 + */ +@Data +public final class SpanNode implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * 上级 SpanNode + */ + @JsonIgnore + private transient SpanNode last; + /** + * 下级 SpanNode + */ + @JsonIgnore + private transient SpanNode next; + /** + * traceId + */ + private String traceId; + /** + * span 名称 + */ + private String spanName; + /** + * span 类型 + */ + private String spanType; + /** + * spanId + */ + private String spanId; + /** + * spanParentId + */ + private String spanParentId; + /** + * span开始时间 + */ + private long spanStart; + /** + * span结束时间 + */ + private long spanEnd; + /** + * 用时 + */ + private long spanInterval; + /** + * 应用名称 + */ + private String appName; + /** + * 自定义说明 + */ + private TreeMap records = new TreeMap<>(); + + public SpanNode() { + } + + /** + * 新建根 span + * @param spanName + * @param spanType + * @return + */ + public static SpanNode rootSpan(String spanName, String spanType) { + if (StrUtil.isBlank(spanName)) { + throw new XzException400("spanName 不能为空"); + } + SpanNode span = new SpanNode(); + span.setSpanParentId("ROOT_SPAN"); + span.setSpanName(spanName); + span.setSpanType(spanType); + span.setTraceId(TraceIdGeneratorUtil.traceId()); + span.setSpanId(TraceIdGeneratorUtil.spanId()); + span.setSpanStart(TimeUtil.currentTimeMillis()); + return span; + } + + /** + * 新建子 span + * @param spanName span 名称 + * @param spanType + * @param traceId + * @param spanParentId + * @return + */ + public static SpanNode forkSpan(String spanName, String spanType, final String traceId, final String spanParentId) { + if (StrUtil.isBlank(spanName)) { + throw new XzException400("spanName 不能为空"); + } + if (StrUtil.isBlank(traceId)) { + throw new XzException400("TraceId 不能为空"); + } + if (StrUtil.isBlank(spanParentId)) { + throw new XzException400("spanParentId 不能为空"); + } + SpanNode span = new SpanNode(); + span.setTraceId(traceId); + span.setSpanName(spanName); + span.setSpanType(spanType); + span.setSpanId(TraceIdGeneratorUtil.spanId()); + span.setSpanParentId(spanParentId); + span.setSpanStart(TimeUtil.currentTimeMillis()); + return span; + } + + public boolean hasLast() { + return null != last; + } + + public boolean hasNext() { + return null != next; + } + + public void setSpanStart(long spanStart) { + this.spanStart = spanStart; + } + + public void setSpanEnd(long ignored) { + this.spanEnd = ignored; + this.spanInterval = Math.max(this.spanEnd - this.spanStart, 0); + } + + public void setRecord(String key, String value) { + this.records.put(key, value); + } + + public String toJson() { + return JsonUtil.toJson(this); + } + + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/Tracker.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/Tracker.java new file mode 100644 index 0000000..2e54249 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/Tracker.java @@ -0,0 +1,195 @@ +package com.blossom.expand.tracker.core; + +import com.blossom.expand.tracker.core.collector.TrackerCollector; +import com.blossom.common.base.util.spring.SpringUtil; +import com.blossom.common.base.util.threadlocal.InternalThreadLocal; +import lombok.extern.slf4j.Slf4j; + + +/** + * 链路追踪管理工具,只有此类会对外部暴露 + * + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +public class Tracker implements AutoCloseable { + + /** + *

复制来自 Dubbo {@see org.apache.dubbo.common.threadlocal.InternalThreadLocal} + *

代码源自 Netty {@see io.netty.util.concurrent.FastThreadLocal} + */ + private static final InternalThreadLocal TRACE_CONTEXT = new InternalThreadLocal<>(); + + private static TrackerCollector trackerCollector; + + private static boolean needCollectSpan = true; + + /** + * 开始追踪 + * + *

如果当前线程已存在 span 信息,则本次 start 会成为一个 child span + * + * @param name 本次 span 名称 + * @param type 本次 span 类型 + * @return TraceNode 对象 + */ + public static SpanContext start(String name, String type) { + try { + SpanContext context = TRACE_CONTEXT.get(); + // 如果上下文不存在则新建,存在则保存到当前上下文链表 + if (context == null) { + context = new SpanContext(); + SpanNode currentSpan = SpanNode.rootSpan(name, type); + // 添加到上下文链表中 + context.add(currentSpan); + TRACE_CONTEXT.set(context); + TrackerMDCLog.traceId(currentSpan.getTraceId()); + TrackerMDCLog.spanId(currentSpan.getSpanId()); + return context; + } else { + String traceId = Tracker.getTraceId(); + String spanParentId = Tracker.getSpanId(); + return fork(name, type, traceId, spanParentId); + } + + } catch (Exception e) { + log.error("[TRACKERS] 追踪开始时发生错误: {}", e.getMessage()); + return null; + } + } + + /** + * fork 与 start + * + * @param name + * @param type + * @param traceId + * @param spanParentId + * @return + */ + public static SpanContext fork(String name, String type, String traceId, String spanParentId) { + try { + SpanContext context = TRACE_CONTEXT.get(); + if (context == null) { + context = new SpanContext(); + } + SpanNode currentSpan = SpanNode.forkSpan(name, type, traceId, spanParentId); + context.add(currentSpan); + TRACE_CONTEXT.set(context); + TrackerMDCLog.traceId(currentSpan.getTraceId()); + TrackerMDCLog.spanId(currentSpan.getSpanId()); + return context; + } catch (Exception e) { + log.error("[TRACKERS] 追踪开始时发生错误: {}", e.getMessage()); + return null; + } + } + + /** + * 结束追踪 + */ + public static void end() { + // 当前不存在山下文,则直接返回 + SpanContext context = TRACE_CONTEXT.get(); + if (context == null) { + return; + } + + // 获取当前 span + SpanNode currentSpan = context.getCurSpan(); + + SpanNode lastSpan = context.remove(); + if (null != lastSpan) { + TrackerMDCLog.traceId(lastSpan.getTraceId()); + TrackerMDCLog.spanId(lastSpan.getSpanId()); + } else { + TrackerMDCLog.removeAll(); + TRACE_CONTEXT.remove(); + } + // 当一个 span 结束后, 发送到 span 收集器中 + sendSpanCollector(currentSpan); + } + + public static void record(String key, String value) { + SpanContext context = TRACE_CONTEXT.get(); + if (context != null && context.getCurSpan() != null) { + context.getCurSpan().setRecord(key, value); + } + } + + /** + * 获取到当前链路的 traceId + * + * @return traceId + */ + public static String getTraceId() { + SpanContext context = TRACE_CONTEXT.get(); + if (context != null && context.getCurSpan() != null) { + return context.getCurSpan().getTraceId(); + } + return ""; + } + + /** + * 获取到当前链路的 spanId + * + * @return spanId + */ + public static String getSpanId() { + SpanContext context = TRACE_CONTEXT.get(); + if (context != null && context.getCurSpan() != null) { + return context.getCurSpan().getSpanId(); + } + return ""; + } + + /** + * 获取到当前链路的 spanParentId + * + * @return spanId + */ + public static String getSpanParentId() { + SpanContext context = TRACE_CONTEXT.get(); + if (context != null && context.getCurSpan() != null) { + return context.getCurSpan().getSpanParentId(); + } + return ""; + } + + /** + * 初始化 span 收集器 + */ + private synchronized static void initSpanQueue() { + try { + // 初始化收集器失败时, 则不需要收集 + trackerCollector = SpringUtil.getBean(TrackerCollector.class); + } catch (Exception e) { + log.info("[TRACKERS] 无法发现任何 TrackerCollector 收集器, TRACE 将只在日志 MDC 中记录, 不会持久化..."); + needCollectSpan = false; + } + } + + /** + * span 收集器, 当 span 结束时发送到 span 收集器 + * + * @param spanNode span节点信息 + */ + private static void sendSpanCollector(SpanNode spanNode) { + // 如果需要收集 + if (needCollectSpan) { + // 如果为空则需要 + if (trackerCollector == null) { + initSpanQueue(); + } + if (trackerCollector != null) { + trackerCollector.collect(spanNode); + } + } + } + + @Override + public void close() { + Tracker.end(); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerConfiguration.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerConfiguration.java new file mode 100644 index 0000000..6c20470 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerConfiguration.java @@ -0,0 +1,51 @@ +package com.blossom.expand.tracker.core; + +import com.blossom.expand.tracker.core.repository.TrackerRepository; +import com.blossom.expand.tracker.core.adapter.aspect.TrackerStartAspect; +import com.blossom.expand.tracker.core.collector.TrackerCollector; +import com.blossom.expand.tracker.core.collector.TrackerLocalCacheCollector; +import com.blossom.expand.tracker.core.repository.LocalDiskFileRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 链路追踪配置 + * + * @author xzzz + */ +@Slf4j +@Configuration +public class TrackerConfiguration { + + @Bean + public TrackerStartAspect trackerBeginAspect() { + return new TrackerStartAspect(); + } + + /** + * havingValue 为一个不存在的值, 永不开启trace收集 + */ + @Bean(name = "trackerRepository") + @ConditionalOnProperty(value = "project.tracker.repository.type", havingValue = "disk") + public TrackerRepository trackerRepository(TrackerProperties trackerProperties) { + return new LocalDiskFileRepository(trackerProperties); + } + + /** + * Tracker span 收集器, 开启收集器才会生效 + * + * @param trackerProperties 配置 + * @return Tracker span 收集器 + */ + @Bean + @ConditionalOnBean(name = "trackerRepository") + @ConditionalOnProperty(value = "project.tracker.collector.enabled", havingValue = "true") + public TrackerCollector traceCollector(TrackerProperties trackerProperties) { + return new TrackerLocalCacheCollector(trackerProperties); + } + + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerMDCLog.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerMDCLog.java new file mode 100644 index 0000000..809aae7 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerMDCLog.java @@ -0,0 +1,54 @@ +package com.blossom.expand.tracker.core; + +import com.blossom.expand.tracker.core.common.TrackerConstants; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; + +/** + * 日志MDC拓展 + * + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +public class TrackerMDCLog { + + /** + * 设置 traceId 到日志 MDC + * @param traceId traceId + */ + protected static void traceId(String traceId) { + try { + MDC.put(TrackerConstants.MDC_TRACE_ID_KEY,traceId); + } catch (IllegalArgumentException e) { + log.error("[TRACKERS] 设置日志MDC拓展错误:{}",e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 设置 spanId 到日志 MDC + * @param spanId spanId + */ + protected static void spanId(String spanId) { + try { + MDC.put(TrackerConstants.MDC_SPAN_ID_KEY,spanId); + } catch (IllegalArgumentException e) { + log.error("[TRACKERS] 设置日志MDC拓展错误:{}",e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 删除日志MDC + */ + protected static void removeAll() { + try { + MDC.remove(TrackerConstants.MDC_TRACE_ID_KEY); + MDC.remove(TrackerConstants.MDC_SPAN_ID_KEY); + } catch (IllegalArgumentException e) { + log.error("[TRACKERS] 删除日志MDC拓展错误:{}",e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerProperties.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerProperties.java new file mode 100644 index 0000000..ec88340 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerProperties.java @@ -0,0 +1,137 @@ +package com.blossom.expand.tracker.core; + +import com.blossom.expand.tracker.core.common.TrackerConstants; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * 链路追踪配置 + * + * @author xzzz + */ +@Data +@Slf4j +@Configuration +@ConfigurationProperties(prefix = "project.tracker") +public class TrackerProperties { + + /** + * 链路追踪收集器配置, 收集器会将 Trace 信息发送发指定的存储方式中, 存储方式需要另外 + * 配置 {@link TrackerProperties#repository} + */ + private Collector collector; + + /** + * Trace 信息的存储方式配置 + */ + private Repository repository; + + @PostConstruct + public void init() { + if (collector != null) { + if (collector.getRate() / Double.valueOf(TrackerConstants.COLLECTOR_MAX_RATE) > 0.5) { + log.warn("[TRACKERS] 当前采样率[{}/{}], 生成环境建议调小该值", + collector.getRate(), TrackerConstants.COLLECTOR_MAX_RATE); + } + } + } + + @Data + public static class Collector { + + /** + * 是否开启本地收集, 需要开启收集 + */ + private boolean enabled = false; + + /** + * 收集器本地缓存最大大小, 默认5000 + *

本地缓存超过该配置后, 将舍弃新的收集信息, 这是用于在远程服务无法连接时, 或本地磁盘无法写入时, 防止本地内存过大 + */ + private Integer maxCache = TrackerConstants.DEFAULT_COLLECTOR_LOCAL_CACHE; + + /** + * 采样率, 0 - 1000 之间的整数, 为 1000 则全部收集, 为 0 则不收集. 默认采样率为1/1000 + *

每个 span 会取 traceId 进行 hash, hash % 1000 后与该值比较, 小于该值则进行采集。 + *

服务集群需要有相同的采样率, 否则数据将出现不完整的情况, + *

注意:

+ *
    + *
  1. 该配置仅在大量数据情况下保证整体采样率近似于该值, 并不能保证每 1000 个请求就有 rate 数的请求被采集.
  2. + *
  3. 较高的采样率并不会对吞吐量直接造成太大影响, 但较高的采样率会使用更多内存, 这在较低配置的机器时会造成更频繁的GC, 从而影响响应rt指标
  4. + *
  5. 出于性能考虑, 采样率过高时 tracker 选择丢弃 span 信息来保证系统稳定运行.
  6. + *
+ */ + private Integer rate = TrackerConstants.DEFAULT_COLLECTOR_RATE; + + /** + * 忽略的追踪 + */ + private List ignoreTrackers; + } + + @Data + public static class IgnoreTracker { + /** + * 追踪名称 + */ + private String spanName; + /** + * 忽略的追踪类型 + */ + private String spanType; + /** + * 是否只有该 span 为 rootSpan 时才忽略 + *

例如未被监控的定时任务调用了SQL, 此时会出现一条单独的SQL追踪信息, 在整个链路中仅此一条, + * 如果此时希望忽略这种追踪, 可以将 rootSpan 设置为 true + */ + private Boolean rootSpan; + } + + @Data + public static class Repository { + + /** + * 收集方式, 在对象注入时会进行判断 {@link TrackerConfiguration#trackerRepository(TrackerProperties)} + *

1. disk: 将 span 信息保存在磁盘中, 目前仅支持该种方式, 但不允许开启. + */ + private String type; + + /** + * trace 信息持久化在本地磁盘时的配置 + */ + private Disk disk; + + } + + @Data + public static class Disk { + /** + * 追踪文件名增加 pid, 为了在本地启动多个项目时进行区分 + */ + Boolean usePid = false; + /** + * 记录的路径 + */ + String logDir = "/tracker/"; + /** + * 单个文件大小, byte + *

10MB: 83886080 + *

20MB: 167772160 + *

30MB: 251658240 + */ + Long singleFileSize = 251658240L; + /** + * 保留的文件总数 + */ + Integer totalFileCount = 30; + + } + + + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerUtil.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerUtil.java new file mode 100644 index 0000000..6474457 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/TrackerUtil.java @@ -0,0 +1,42 @@ +package com.blossom.expand.tracker.core; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.common.TrackerConstants; + +/** + * Trace 对 HTTP 请求处理的相关工具类 + * + * @author xzzz + */ +public class TrackerUtil { + + /** + * 构造一个 Trace 请求头 + */ + public static String buildHeader(String type) { + String traceId = Tracker.getTraceId(); + String spanParentId = Tracker.getSpanId(); + if (StrUtil.isNotBlank(traceId) && StrUtil.isNotBlank(spanParentId)) { + return String.format("%s|%s|%s", traceId, spanParentId, type); + } + return ""; + } + + public static boolean checkIgnoreApi(String spanName) { + for (String api : TrackerConstants.ignoreInnerApi) { + if (spanName.contains(api)) { + return true; + } + } + return false; + } + + public static boolean checkIgnoreSql(String spanName) { + for (String api : TrackerConstants.ignoreInnerSql) { + if (spanName.contains(api)) { + return true; + } + } + return false; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStart.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStart.java new file mode 100644 index 0000000..aa858de --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStart.java @@ -0,0 +1,29 @@ +package com.blossom.expand.tracker.core.adapter.aspect; + + +import com.blossom.expand.tracker.core.common.TrackerConstants; + +import java.lang.annotation.*; + +/** + * + * @author xzzz + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TrackerStart { + + /** + * 是否创建新的Trace + * + * false: 如果当前存在跟踪链路, 则以当前为准 + * true : 忽略当前已存在的路径, 会替换后续全部链路 + * + * @return 默认false + */ + String spanName() default ""; + + String spanType() default TrackerConstants.SPAN_TYPE_ANNOTATION; + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStartAspect.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStartAspect.java new file mode 100644 index 0000000..65bca88 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/aspect/TrackerStartAspect.java @@ -0,0 +1,46 @@ +package com.blossom.expand.tracker.core.adapter.aspect; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.SpanContext; +import com.blossom.expand.tracker.core.Tracker; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +/** + * 创建追踪切面 + * + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +@Aspect +@Component +public class TrackerStartAspect { + + /** + * 通过注解 start 或 fork 到追踪中 + * @param point 方法 + * @param trackerStart 注解 + * @return 返回值 + */ + @Around("@annotation(trackerStart)") + public Object around(ProceedingJoinPoint point, TrackerStart trackerStart) throws Throwable { + SpanContext span = null; + try { + String spanName = trackerStart.spanName(); + if (StrUtil.isBlank(spanName)) { + // 取方法名称 + spanName = point.getSignature().getDeclaringTypeName(); + } + span = Tracker.start(spanName, trackerStart.spanType()); + return point.proceed(point.getArgs()); + } finally { + if (span != null) { + Tracker.end(); + } + } + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlConfiguration.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlConfiguration.java new file mode 100644 index 0000000..2f81dd3 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlConfiguration.java @@ -0,0 +1,21 @@ +package com.blossom.expand.tracker.core.adapter.mysql; + +import org.apache.ibatis.session.SqlSession; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 链路追踪 mysql 配置 + * + * @author xzzz + */ +@Configuration +@ConditionalOnClass(value = SqlSession.class) +public class TrackerMysqlConfiguration { + + @Bean + public TrackerMysqlInterceptor trackerMysqlInterceptor() { + return new TrackerMysqlInterceptor(); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlInterceptor.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlInterceptor.java new file mode 100644 index 0000000..8e96c82 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/mysql/TrackerMysqlInterceptor.java @@ -0,0 +1,167 @@ +package com.blossom.expand.tracker.core.adapter.mysql; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.SpanContext; +import com.blossom.expand.tracker.core.TrackerUtil; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import com.github.pagehelper.util.StringUtil; +import com.blossom.expand.tracker.core.Tracker; +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 链路追踪 mysql 拦截 + * + * @author xzzz + */ +@Component +@Intercepts({ +@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), +@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), +@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), +}) +public class TrackerMysqlInterceptor implements Interceptor { + + private static final ThreadLocal DATETIME_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + + String sql = null; + try { + MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; + if (TrackerUtil.checkIgnoreSql(mappedStatement.getId())) { + return invocation.proceed(); + } + Object parameter = null; + if (invocation.getArgs().length > 1) { + parameter = invocation.getArgs()[1]; + } + BoundSql boundSql = mappedStatement.getBoundSql(parameter); + Configuration configuration = mappedStatement.getConfiguration(); + + sql = this.getSql(configuration, boundSql); + } catch (Exception ignored) { + } + + SpanContext spanContext = null; + try { + spanContext = Tracker.start(TrackerConstants.SPAN_TYPE_MYSQL, TrackerConstants.SPAN_TYPE_MYSQL); + if (StrUtil.isNotBlank(sql)) { + Tracker.record("SQL", sql); + } + return invocation.proceed(); + } finally { + if (spanContext != null) { + Tracker.end(); + } + } + } + + /** + * 获取完整的sql语句 + * + * @param configuration + * @param boundSql + * @return + */ + private String getSql(Configuration configuration, BoundSql boundSql) { + // 输入sql字符串空判断 + String sql = boundSql.getSql(); + if (StringUtil.isEmpty(sql)) { + return ""; + } + return formatSql(sql, configuration, boundSql); + } + + /** + * 将占位符替换成参数值 + * + * @param sql + * @param configuration + * @param boundSql + * @return + */ + private String formatSql(String sql, Configuration configuration, BoundSql boundSql) { + //美化sql + sql = clearSql(sql); + //填充占位符, 目前基本不用mybatis存储过程调用,故此处不做考虑 + Object parameterObject = boundSql.getParameterObject(); + List parameterMappings = boundSql.getParameterMappings(); + TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + List parameters = new ArrayList<>(); + if (parameterMappings != null) { + MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); + for (ParameterMapping parameterMapping : parameterMappings) { + if (parameterMapping.getMode() != ParameterMode.OUT) { + // 参数值 + Object value; + String propertyName = parameterMapping.getProperty(); + // 获取参数名称 + if (boundSql.hasAdditionalParameter(propertyName)) { + // 获取参数值 + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + // 如果是单个值则直接赋值 + value = parameterObject; + } else { + value = metaObject == null ? null : metaObject.getValue(propertyName); + } + + if (value instanceof Number) { + parameters.add(String.valueOf(value)); + } else { + StringBuilder builder = new StringBuilder(); + builder.append("'"); + if (value instanceof Date) { + builder.append(DATETIME_FORMATTER.get().format((Date) value)); + } else if (value instanceof String) { + builder.append(value); + } + builder.append("'"); + parameters.add(builder.toString()); + } + } + } + } + + for (String value : parameters) { + sql = sql.replaceFirst("\\?", value); + } + return sql; + } + + public static String clearSql(String sql) { + sql = sql.replaceAll("[\\s\n ]+", " "); + sql = sql.replace(" = ", "="); + return sql; + } + + + + @Override + public Object plugin(Object target) { + if (target instanceof Executor) { + return Plugin.wrap(target, this); + } + return target; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/okhttp/TrackerInterceptor.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/okhttp/TrackerInterceptor.java new file mode 100644 index 0000000..37182d8 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/okhttp/TrackerInterceptor.java @@ -0,0 +1,50 @@ +package com.blossom.expand.tracker.core.adapter.okhttp; + +import com.blossom.expand.tracker.core.TrackerUtil; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import com.blossom.common.base.util.okhttp.OkHttpInterceptor; +import com.blossom.expand.tracker.core.SpanContext; +import com.blossom.expand.tracker.core.Tracker; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * 链路追踪对 okhttp 的拦截 + * + * @author xzzz + */ +@Slf4j +public class TrackerInterceptor implements OkHttpInterceptor { + + /** + * 在请求头中增加 TRACE 请求头 + * @param chain 拦截器链 + * @return response + */ + @Override + public Response intercept(Chain chain) throws IOException { + SpanContext spanContext = null; + try { + spanContext = Tracker.start(chain.request().method() + ":" + chain.request().url().toString(), + TrackerConstants.SPAN_TYPE_HTTP_OKHTTP); + + // 创建请求头 + Request request = chain.request().newBuilder().addHeader( + TrackerConstants.HTTP_HEADERS, + TrackerUtil.buildHeader(TrackerConstants.SPAN_TYPE_HTTP_OKHTTP_INVOKE)).build(); + return chain.proceed(request); + } finally { + if (spanContext != null) { + Tracker.end(); + } + } + } + + @Override + public void instructions() { + log.info("[TRACKERS] 已经适配框架 : OKHTTP BY OKHTTP_INTERCEPTOR"); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisConnectionFactoryProxy.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisConnectionFactoryProxy.java new file mode 100644 index 0000000..7919895 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisConnectionFactoryProxy.java @@ -0,0 +1,115 @@ + +package com.blossom.expand.tracker.core.adapter.redis; + +import cn.hutool.core.util.ArrayUtil; +import com.blossom.expand.tracker.core.SpanContext; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import com.blossom.expand.tracker.core.Tracker; +import com.blossom.common.base.util.ProxyUtils; +import com.blossom.common.base.util.json.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInvocation; + +import java.lang.reflect.Method; + +/** + * 使用自定义代理完成日志输出 + */ +@Slf4j +class RedisConnectionFactoryProxy { + + /** + * 追踪时忽略的方法 + */ + private static final String IGNORE_IS_PIPELINED = "isPipelined"; + private static final String IGNORE_IS_QUEUEING = "isQueueing"; + private static final String IGNORE_CLOSE = "close"; + + /** + * 被代理方法 + */ + private static final String PROXY_METHOD = "getConnection"; + + private TrackerRedisProperties trackerRedisProperties; + + public RedisConnectionFactoryProxy(TrackerRedisProperties trackerRedisProperties) { + this.trackerRedisProperties = trackerRedisProperties; + } + + /** + * 如果是 getConnection 方法,把返回结果进行代理包装 + * + * @param invocation 方法 + * @return + */ + public Object interceptorRedisFactory(MethodInvocation invocation) throws Throwable { + Object ret = invocation.proceed(); + String methodName = invocation.getMethod().getName(); + if (PROXY_METHOD.equals(methodName)) { + return ProxyUtils.getProxy(ret, this::getConnectionMethodProxy); + } + return ret; + } + + /** + * 拦截 redisConnectionFactory.getConnection 的方法, 来进行追踪 span 中的 redis 节点 + * + * @param invocation 调用方法 + * @return 方法调用结果 + * @throws Throwable 异常 + */ + private Object getConnectionMethodProxy(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + String methodName = method.getName(); + + // 忽略的REDIS命令 + if (IGNORE_IS_PIPELINED.equals(methodName) || IGNORE_IS_QUEUEING.equals(methodName) || IGNORE_CLOSE.equals(methodName)) { + return invocation.proceed(); + } + + SpanContext spanContext = Tracker.start(methodName, TrackerConstants.SPAN_TYPE_REDIS); + + if (trackerRedisProperties.getDetailToRecord()) { + paramToRecord(invocation.getArguments()); + } + + try { + return invocation.proceed(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + throw throwable; + } finally { + if (spanContext != null) { + Tracker.end(); + } + } + } + + + private void paramToRecord(Object[] params) { + if (ArrayUtil.isEmpty(params)) { + return; + } + for (int i = 0; i < params.length; i++) { + Tracker.record("key_" + (i + 1), deserialization(params[i])); + } + } + + private String deserialization(Object obj) { + if (obj == null) { + return ""; + } + try { + if (obj instanceof byte[]) { + obj = new String((byte[]) obj); + } + if (obj instanceof String) { + return obj.toString(); + } + return JsonUtil.toJson(obj); + } catch (Exception ex) { + log.error(String.format("%s serialize error: %s", obj.getClass().getName(), ex.toString())); + return obj.toString(); + } + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisFactoryBeanPostProcessor.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisFactoryBeanPostProcessor.java new file mode 100644 index 0000000..40202f2 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/RedisFactoryBeanPostProcessor.java @@ -0,0 +1,87 @@ +package com.blossom.expand.tracker.core.adapter.redis; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.blossom.common.base.util.ProxyUtils; +import com.blossom.common.base.util.json.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +@Slf4j +@SuppressWarnings("all") +public class RedisFactoryBeanPostProcessor implements BeanPostProcessor { + + private static final String REDIS_CONNECTION_FACTORY_BEAN_NAME = "redisConnectionFactory"; + + private TrackerRedisProperties trackerRedisProperties; + + public RedisFactoryBeanPostProcessor(TrackerRedisProperties trackerRedisProperties) { + this.trackerRedisProperties = trackerRedisProperties; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (REDIS_CONNECTION_FACTORY_BEAN_NAME.equals(beanName)) { + log.info("[TRACKERS] 已经适配框架 : Redis"); + // 使用日志代理,覆盖原 Redis 工厂类 + return ProxyUtils.getProxy(bean, invocation -> { + return new RedisConnectionFactoryProxy(trackerRedisProperties).interceptorRedisFactory(invocation); + }); + } else if (bean instanceof RedisTemplate) { + // 避免默认的序列化方式,导致进入Redis的字符串无法直接辨识 + initSerializer((RedisTemplate) bean); + } + return bean; + } + + private static void initSerializer(RedisTemplate redisTemplate) { + // Key用StringRedisSerializer,避免写入Redis的Key和Value,前缀都会出现 \xAC\xED\x00\x05t\x00\x03 +// RedisSerializer keySerializer = new StringRedisSerializer(); +// redisTemplate.setKeySerializer(keySerializer); +// redisTemplate.setHashKeySerializer(keySerializer); +// RedisJsonValueSerializer valSerializer = new RedisJsonValueSerializer(Object.class); +// redisTemplate.setValueSerializer(valSerializer); +// redisTemplate.setHashValueSerializer(valSerializer); + } + + @Slf4j + private static class RedisJsonValueSerializer implements RedisSerializer { + + protected static ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, true); + + private Class clazz; + + public RedisJsonValueSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) { + return JsonUtil.toByte(t); +// try { +// return mapper.writeValueAsBytes(t); +// } catch (JsonProcessingException e) { +// log.error(e.toString()); +// return null; +// } + } + + @Override + public T deserialize(byte[] bytes) { + if (bytes == null) { + return null; + } + return JsonUtil.toObj(bytes, clazz); +// return mapper.readValue(bytes, clazz); + } + + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisConfiguration.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisConfiguration.java new file mode 100644 index 0000000..f85ad96 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisConfiguration.java @@ -0,0 +1,28 @@ +package com.blossom.expand.tracker.core.adapter.redis; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * 链路追踪对 redis 的拓展 + * + * @author xzzz + */ +@Configuration +@ConditionalOnClass(value = RedisTemplate.class) +public class TrackerRedisConfiguration { + + @Bean + public TrackerRedisProperties trackerRedisProperties() { + return new TrackerRedisProperties(); + } + + @Bean + @ConditionalOnProperty(value = "tracker.redis.enabled", havingValue = "true", matchIfMissing = true) + public RedisFactoryBeanPostProcessor redisFactoryBeanPostProcessor(TrackerRedisProperties trackerRedisProperties) { + return new RedisFactoryBeanPostProcessor(trackerRedisProperties); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisProperties.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisProperties.java new file mode 100644 index 0000000..b5bf477 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/redis/TrackerRedisProperties.java @@ -0,0 +1,26 @@ +package com.blossom.expand.tracker.core.adapter.redis; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Redis 链路追踪配置 + * + * @author xzzz + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "tracker.redis") +public class TrackerRedisProperties { + + /** + * 开启 redis 追踪 + */ + private Boolean enabled = true; + + /** + * redis 请求详情保存至 Tracker.Record 中 + */ + private Boolean detailToRecord = false; +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerFilter.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerFilter.java new file mode 100644 index 0000000..ea26d8b --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerFilter.java @@ -0,0 +1,63 @@ +package com.blossom.expand.tracker.core.adapter.spring; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import com.blossom.expand.tracker.core.Tracker; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.web.filter.GenericFilterBean; + +import javax.annotation.PostConstruct; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Rest 端点的过滤器配置 + * + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +@Order(Integer.MIN_VALUE) +public class TrackerFilter extends GenericFilterBean { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + try { + try { + HttpServletRequest request = (HttpServletRequest) servletRequest; + String header = request.getHeader(TrackerConstants.HTTP_HEADERS); + + boolean isFork = false; + + String[] params = null; + if (StrUtil.isNotBlank(header)) { + params = header.split("\\|"); + if (params.length == 3) { + isFork = true; + } + } + if (isFork) { + Tracker.fork(request.getMethod() + ":" + request.getServletPath(), params[2], params[0], params[1]); + } else { + Tracker.start(request.getMethod() + ":" + request.getServletPath(), TrackerConstants.SPAN_TYPE_HTTP_MVC); + } + + } catch (Exception ignored) { + } + filterChain.doFilter(servletRequest,response); + } finally { + Tracker.end(); + } + } + + @PostConstruct + public void init () { + log.info("[TRACKERS] 已经适配框架 : Spring Filter"); + } + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerSpringConfiguration.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerSpringConfiguration.java new file mode 100644 index 0000000..aa945b3 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerSpringConfiguration.java @@ -0,0 +1,19 @@ +package com.blossom.expand.tracker.core.adapter.spring; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +@Configuration +public class TrackerSpringConfiguration { + + @Bean + public TrackerFilter trackerFilter() { + return new TrackerFilter(); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerTaskDecorator.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerTaskDecorator.java new file mode 100644 index 0000000..d4729bc --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/adapter/spring/TrackerTaskDecorator.java @@ -0,0 +1,56 @@ +package com.blossom.expand.tracker.core.adapter.spring; + +import cn.hutool.core.util.StrUtil; +import com.blossom.expand.tracker.core.Tracker; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.task.TaskDecorator; + +/** + * Spring task 线程池装饰器 + * + * @author xzzz + * @since 1.2.0 + */ +@Slf4j +public class TrackerTaskDecorator implements TaskDecorator { + + /** + * 装饰器名称 + */ + private String decoratorName; + + private String spanType; + + /** + * 装饰器名称, 将作为异步任务的 spanName + * + * @param decoratorName 装饰器名称 + */ + public TrackerTaskDecorator(String decoratorName, String spanType) { + this.decoratorName = decoratorName; + this.spanType = spanType; + } + + /** + * 嵌套异步线程,只需要设 + * @param runnable 异步线程 + * @return runnable + */ + @Override + public Runnable decorate(Runnable runnable) { + String traceId = Tracker.getTraceId(); + String spanParentId = Tracker.getSpanId(); + return () -> { + try { + if (StrUtil.isBlank(traceId) || StrUtil.isBlank(spanParentId)) { + Tracker.start(decoratorName, spanType); + } else { + Tracker.fork(decoratorName, spanType, traceId, spanParentId); + } + runnable.run(); + } finally { + Tracker.end(); + } + }; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerCollector.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerCollector.java new file mode 100644 index 0000000..2c12ad7 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerCollector.java @@ -0,0 +1,27 @@ +package com.blossom.expand.tracker.core.collector; + +import com.blossom.expand.tracker.core.SpanNode; + +import java.util.List; + +/** + * 追踪收集器 + * + * @author xzzz + */ +public interface TrackerCollector { + + /** + * 收集 span 信息 + * + * @param spanNode span 信息 + */ + void collect(SpanNode spanNode); + + /** + * 获取 span 列表 + * + * @return span 列表 + */ + List get(); +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerLocalCacheCollector.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerLocalCacheCollector.java new file mode 100644 index 0000000..98f20b8 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/collector/TrackerLocalCacheCollector.java @@ -0,0 +1,199 @@ +package com.blossom.expand.tracker.core.collector; + +import cn.hutool.core.collection.CollUtil; +import com.blossom.expand.tracker.core.SpanNode; +import com.blossom.expand.tracker.core.TrackerProperties; +import com.blossom.expand.tracker.core.common.TrackerConstants; +import com.blossom.expand.tracker.core.repository.TrackerRepository; +import com.blossom.common.base.util.spring.SpringUtil; +import com.blossom.common.base.util.thread.NamedThreadFactory; +import com.blossom.expand.tracker.core.Tracker; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.locks.ReentrantLock; + +/** + * tracker 开启 trace 信息收集, 该方法只用来收集本应用的 trace 信息, 具体信息保存在何处, 由 + * {@link TrackerRepository} 的实现类来实现 + * + * @author xzzz + */ +@Slf4j +public class TrackerLocalCacheCollector implements TrackerCollector { + + /** + * Tracker Span 收集器配置文件 + */ + private final TrackerProperties properties; + + /** + * 本地缓存, 每次发送将一次性将缓存内全部发送到服务端, 并将缓存清空, 内存大小会受到 + * {@link TrackerProperties#getCollector()#localSpanCache} 配置项的控制 + */ + private final List localSpanCache; + + /** + * 发送 Span 集合追踪的线程池 + */ + private final ScheduledExecutorService spanSendExecutor = new ScheduledThreadPoolExecutor( + 1, new NamedThreadFactory("span-send-task", true)); + + /** + * 数据收集线程池 + */ + private final ExecutorService spanCollectWorker; + + /** + * 在发送时将禁止数据写入缓存 + */ + private final ReentrantLock LOCK = new ReentrantLock(); + + /** + * 应用名称 + */ + private String APP_NAME; + + /** + * 写入缓存的超时时间 + */ + private final long LOCAL_CACHE_WRITE_OVERTIME = 2000; + + /** + * tracker span 持久化方法 + */ + @Autowired(required = false) + private TrackerRepository trackerRepository; + + public TrackerLocalCacheCollector(TrackerProperties properties) { + log.info("[TRACKERS] 开启 Span 收集器, 采样率[{}/1000], 本地缓存数[{}]", + properties.getCollector().getRate(), properties.getCollector().getMaxCache()); + this.properties = properties; + this.localSpanCache = new ArrayList<>(properties.getCollector().getMaxCache()); + + int cores = Runtime.getRuntime().availableProcessors(); + // 数据收集线程池,线程池满时,会直接丢弃新任务 + spanCollectWorker = new ThreadPoolExecutor( + cores, cores, + 10, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(2048), + new NamedThreadFactory("tracker-collect-worker"), + new ThreadPoolExecutor.DiscardPolicy()); + } + + /** + * 异步收集数据到本地缓存, 如果此时正在存储中, 则异步线程阻塞, 超时后 (默认5s) 放弃该条追踪数据。异步线程保存并不会阻塞业务线程, + * 所以 span 信息的收集对性能影响很小。 + *

+ * 一次 span 结束时 {@link Tracker#end()} 会收集该 span 信息, 例如一次 trace 有 10 个 span 节点, 则会进入 + * 该方法 10 次, span 信息会交由 {@link TrackerLocalCacheCollector#spanCollectWorker} 线程池发送到本地缓存 + * {@link TrackerLocalCacheCollector#localSpanCache} 中, 并由 {@link TrackerLocalCacheCollector#spanSendExecutor} + * 定时任务将缓存中的数据发送至 {@link TrackerRepository} trace 存储对象中。发送时本次缓存是被锁定的且无法写入的。 + * 发送完成后会清空本地缓存。此时收集器才能再次写入。 + * + * @param spanNode span 信息 + */ + @Override + public void collect(SpanNode spanNode) { + if (Math.abs(spanNode.getTraceId().hashCode() % TrackerConstants.COLLECTOR_MAX_RATE) >= properties.getCollector().getRate()) { + return; + } + + if (ignore(spanNode)) { + return; + } + + spanNode.setAppName(APP_NAME); + + spanCollectWorker.submit(() -> { + try { + if (localSpanCache.size() > properties.getCollector().getMaxCache()) { + return; + } + if (LOCK.tryLock(LOCAL_CACHE_WRITE_OVERTIME, TimeUnit.MILLISECONDS)) { + try { + localSpanCache.add(spanNode); + } finally { + LOCK.unlock(); + } + } else { + log.warn("收集本地 span 失败, 本地缓存写入超时"); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + } + + @PostConstruct + public void init() { + this.APP_NAME = SpringUtil.getAppName(); + // 每个任务会等上个任务完成后 delay 秒后执行 + spanSendExecutor.scheduleWithFixedDelay(() -> { + if (localSpanCache.size() == 0) { + return; + } + log.debug("即将发送本地SPAN日志({}条)", localSpanCache.size()); + try { + if (LOCK.tryLock(5000, TimeUnit.MILLISECONDS)) { + try { + if (trackerRepository.save(localSpanCache)) { + localSpanCache.clear(); + } + } catch (Throwable t) { + log.error("[TRACKERS] 发送本地SPAN日志({}条)失败, 异常信息: {}", localSpanCache.size(), t.getMessage()); + } finally { + LOCK.unlock(); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 10000, 5000, TimeUnit.MILLISECONDS); + } + + @Override + public List get() { + return localSpanCache; + } + + /** + * 不收集的 span 信息 + * + * @param spanNode + * @return + */ + private boolean ignore(SpanNode spanNode) { + if (CollUtil.isEmpty(properties.getCollector().getIgnoreTrackers())) { + return false; + } + + for (TrackerProperties.IgnoreTracker ignoreTracker : properties.getCollector().getIgnoreTrackers()) { + + if (spanNode.getSpanName().equals(ignoreTracker.getSpanName())) { + // 忽略全部该名称的 span + if (!ignoreTracker.getRootSpan()) { + return true; + } + // 忽略全部该名称的 root_span + else if (ignoreTracker.getRootSpan() & spanNode.getSpanParentId().equals(TrackerConstants.ROOT_SPAN)) { + return true; + } + } + + if (spanNode.getSpanType().equals(ignoreTracker.getSpanType())) { + if (!ignoreTracker.getRootSpan()) { + return true; + } else if (ignoreTracker.getRootSpan() & spanNode.getSpanParentId().equals(TrackerConstants.ROOT_SPAN)) { + return true; + } + } + } + return false; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/common/TrackerConstants.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/common/TrackerConstants.java new file mode 100644 index 0000000..cfa938b --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/common/TrackerConstants.java @@ -0,0 +1,127 @@ +package com.blossom.expand.tracker.core.common; + +import com.blossom.expand.tracker.core.adapter.aspect.TrackerStart; +import com.blossom.expand.tracker.core.adapter.spring.TrackerTaskDecorator; + +import java.util.HashSet; +import java.util.Set; + +/** + * 静态参数 + * + * @author xzzz + * @since 1.2.0 + */ +public class TrackerConstants { + + public static final String ROOT_SPAN = "ROOT_SPAN"; + + /** + * 应用启动 + */ + public static final String SPAN_TYPE_APPLICATION_RUN = "APPLICATION_RUN"; + + /** + * 通过注解的形式 {@link TrackerStart} + */ + public static final String SPAN_TYPE_ANNOTATION = "ANNOTATION"; + + /** + * Spring 异步线程池装饰器类型 {@link TrackerTaskDecorator} + */ + public static final String SPAN_TYPE_SPRING_ASYNC = "SPRING_ASYNC"; + + /** + * Spring 定时线程池装饰器类型 {@link TrackerTaskDecorator} + */ + public static final String SPAN_TYPE_SPRING_SCHEDULED = "SPRING_SCHEDULED"; + + /** + * 调用 ES + */ + public static final String SPAN_TYPE_ELASTICSEARCH = "ELASTIC_SEARCH"; + + /** + * 服务被外部直接调用网关接口 + */ + public static final String SPAN_TYPE_HTTP_GATEWAY = "HTTP_GATEWAY"; + + /** + * 内部服务被网关调用 + */ + public static final String SPAN_TYPE_HTTP_GATEWAY_INVOKE = "HTTP_GATEWAY_INVOKE"; + + /** + * 服务被外部直接调用 HTTP 接口 + */ + public static final String SPAN_TYPE_HTTP_MVC = "HTTP_MVC"; + + /** + * 内部服务被 FEIGN 调用 + */ + public static final String SPAN_TYPE_HTTP_FEIGN_INVOKE = "HTTP_FEIGN_INVOKE"; + + /** + * OKHTTP 调用他方 + */ + public static final String SPAN_TYPE_HTTP_OKHTTP = "HTTP_OKHTTP"; + + /** + * 被内部 OKHTTP 工具调用 + */ + public static final String SPAN_TYPE_HTTP_OKHTTP_INVOKE = "HTTP_OKHTTP_INVOKE"; + + /** + * 追踪到请求 REDIS + */ + public static final String SPAN_TYPE_REDIS = "REDIS"; + + /** + * 追踪到请求 MySQL + */ + public static final String SPAN_TYPE_MYSQL = "MYSQL"; + + /** + * 通过HTTP传递参数时使用的请求头 + *

格式: traceId|spanId|spanType + */ + public static final String HTTP_HEADERS = "XZ_TRACE"; + + /** + * MDC 中 Trace 的 key + */ + public static final String MDC_TRACE_ID_KEY = "traceId"; + + /** + * MDC 中 Span 的 key + */ + public static final String MDC_SPAN_ID_KEY = "spanId"; + + public static final Integer DEFAULT_COLLECTOR_LOCAL_CACHE = 5000; + public static final Integer DEFAULT_COLLECTOR_RATE = 1; + public static final Integer COLLECTOR_MAX_RATE = 1000; + + /** + * 忽略的追踪信息 + */ + public static final Set ignoreSpanName = new HashSet(){{ + this.add("/tracker/metric"); + }}; + + public static final Set ignoreInnerApi = new HashSet(){{ + this.add("/tracker/metric"); + }}; + + public static final Set ignoreInnerSql = new HashSet(){{ + this.add("com.blossomexpand.tracker.server.mappers.TrackerMetricMySqlMapper.save"); + }}; + + /** + * Trace 的API + */ + interface API { + String SERVER_SAVE_SPAN = "/tracker/metric"; + } + +} + diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGenerator.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGenerator.java new file mode 100644 index 0000000..bf16ff1 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGenerator.java @@ -0,0 +1,25 @@ +package com.blossom.expand.tracker.core.id; + +/** + * ID生成接口 + * + * @author xzzz + * @since 1.2.0 + */ +public interface TraceIdGenerator { + + + /** + * 生成 traceId + * @return traceId + */ + String traceId(); + + + /** + * 生成 spanId + * @return spanId + */ + String spanId(); + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUUID.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUUID.java new file mode 100644 index 0000000..5b91af0 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUUID.java @@ -0,0 +1,40 @@ +package com.blossom.expand.tracker.core.id; + + +import cn.hutool.core.lang.UUID; + +/** + * trace 生成类 + * + * @author xzzz + * @since 1.2.0 + */ +public class TraceIdGeneratorUUID implements TraceIdGenerator { + + /** + * 生成 traceId + * @return traceId + */ + @Override + public String traceId () { + return generator(); + } + + /** + * 生成 spanId + * @return spanId + */ + @Override + public String spanId () { + return generator(); + } + + /** + * 生成ID + * @return ID + */ + private String generator() { + return UUID.fastUUID().toString(true).toUpperCase(); + } + +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUtil.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUtil.java new file mode 100644 index 0000000..6e3c2fe --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/id/TraceIdGeneratorUtil.java @@ -0,0 +1,21 @@ +package com.blossom.expand.tracker.core.id; + +/** + * 追踪ID生成 + * + * @author xzzz + */ +public class TraceIdGeneratorUtil { + /** + * TraceGenerator traceId 生成器 + */ + private static final TraceIdGenerator traceGenerator = new TraceIdGeneratorUUID(); + + public static String traceId() { + return traceGenerator.traceId(); + } + + public static String spanId() { + return traceGenerator.spanId(); + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/LocalDiskFileRepository.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/LocalDiskFileRepository.java new file mode 100644 index 0000000..3f9e173 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/LocalDiskFileRepository.java @@ -0,0 +1,107 @@ +package com.blossom.expand.tracker.core.repository; + +import com.blossom.expand.tracker.core.SpanNode; +import com.blossom.expand.tracker.core.TrackerProperties; +import com.blossom.common.base.util.json.JsonUtil; +import com.blossom.common.base.util.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.util.List; + +/** + * 本地存储追踪信息 + * + * @author xzzz + */ +@Slf4j +public class LocalDiskFileRepository implements TrackerRepository, EnvironmentAware { + + private static final String USER_HOME = "user.dir"; + + private static String logBaseDir; + + private final TrackerProperties.Disk diskProperties; + + private TrackerFileWriter trackerFileWriter; + + private Environment env; + + public LocalDiskFileRepository(TrackerProperties properties) { + log.info("[TRACKERS] 开启追踪信息本地磁盘持久化"); + if (properties.getRepository().getDisk() == null) { + diskProperties = new TrackerProperties.Disk(); + } else { + diskProperties = properties.getRepository().getDisk(); + } + } + + @PostConstruct + public void init() { + // 获取配置的日志路径 + logBaseDir = diskProperties.getLogDir(); + // 路径判断增加前缀 + logBaseDir = addSeparator(logBaseDir); + // 判断文件夹是否存在, 不存在则创建 + File dir = new File(logBaseDir); + if (!dir.exists()) { + if (!dir.mkdirs()) { + System.err.println("ERROR: create Sentinel log base directory error: " + logBaseDir); + } + } + String appName = env.getProperty(SpringUtil.APP_NAME); + + trackerFileWriter = new TrackerFileWriter( + diskProperties.getSingleFileSize(), + diskProperties.getTotalFileCount(), + logBaseDir, + appName, + diskProperties.getUsePid()); + } + + @Override + public boolean save(List spanNodes) { + StringBuilder sb = new StringBuilder(); + for (SpanNode spanNode : spanNodes) { + sb.append(spanNode.getAppName()).append("|"); + sb.append(spanNode.getSpanStart()).append("|"); + sb.append(spanNode.getSpanEnd()).append("|"); + sb.append(spanNode.getTraceId()).append("|"); + sb.append(spanNode.getSpanId()).append("|"); + sb.append((spanNode.getSpanParentId() == null ? "" : spanNode.getSpanParentId())).append("|"); + sb.append(spanNode.getSpanInterval()).append("|"); + sb.append(spanNode.getSpanType()).append("|"); + sb.append(spanNode.getSpanName()).append("|"); + + // 最终以 record + /r/n结尾 + sb.append(JsonUtil.toJson(spanNode.getRecords())).append("\r\n"); + } + try { + trackerFileWriter.write(System.currentTimeMillis(), sb.toString()); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public long expire() { + return 0; + } + + public static String addSeparator(String dir) { + if (!dir.endsWith(File.separator)) { + dir += File.separator; + } + return System.getProperty(USER_HOME) + dir; + } + + @Override + public void setEnvironment(Environment environment) { + this.env = environment; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerFileWriter.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerFileWriter.java new file mode 100644 index 0000000..d7e193b --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerFileWriter.java @@ -0,0 +1,445 @@ +package com.blossom.expand.tracker.core.repository; + +import com.blossom.common.base.util.SystemUtil; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 指标文件内容写入, 文件将在JVM停止后终止写入, 并且不会继续在文件后添加内容, 即使文件大小不满足 {@link TrackerFileWriter#singleFileSize} + * 也会重新创建全新的文件, 并在文件添加标识 + * + *

二. Demo

+ *
    + *
  1. 单个文件大小为 [ 20MB ]
  2. + *
  3. 最多保留 [ 100 ] 个文件
  4. + *
  5. 路径为 [ D:\\logs\\ ]
  6. + *
  7. 文件名为 [ testApp-metrics.log.yyyy-MM-dd.num ]
  8. + *
+ *
{@code
+ * public static void main(String[] args) {
+ *     MetricFileWriter writer = new MetricFileWriter(
+ *      167772160,100,"D:\\logs\\","testApp");
+ *     for (int i = 1; i <= 100; i++) {
+ *         try {
+ *             writer.write(System.currentTimeMillis(), "line" + i + "\r\n");
+ *         } catch (Ex e) {
+ *             e.printStackTrace();
+ *         }
+ *     }
+ * }
+ * }
+ */ +@Slf4j +@SuppressWarnings("all") +public class TrackerFileWriter { + + private static final String CHARSET = "UTF-8"; + + /** + * 指标度量文件名后缀 + * 文件名格式如下: + *
{@code
+     * testApp-metrics.log.2022-10-30
+     * testApp-metrics.log.2022-10-30.1
+     * testApp-metrics.log.2022-10-30.2
+     * testApp-metrics.log.2022-10-31
+     * testApp-metrics.log.2022-10-31.1
+     * }
+ * {@link MetricFileNameComparator} 的实现依赖于指标度量文件名, 因此在更改度量文件名时应该小心. + */ + public static final String METRIC_FILE = "tracker.log"; + + /** + * 指标度量文件的索引后缀 + * 文件名格式如下: + *
{@code
+     * testApp-metrics.log.2022-10-30.idx
+     * testApp-metrics.log.2022-10-30.1.idx
+     * testApp-metrics.log.2022-10-30.2.idx
+     * testApp-metrics.log.2022-10-31.idx
+     * testApp-metrics.log.2022-10-31.1.idx
+     * }
+ */ + public static final String METRIC_FILE_INDEX_SUFFIX = ".idx"; + + /** + * 文件名比较器 + */ + public static final Comparator METRIC_FILE_NAME_CMP = new MetricFileNameComparator(); + + /** + * 是否允许在文件后追加内容, 为保证写入效率, 所以每次启动代码都会新建文件, 即不允许在文件后追加内容 + */ + private static final boolean append = false; + + /** + * 排除时差干扰 + */ + private long timeSecondBase; + + /** + * 文件所在目录 + */ + private final String baseDir; + + /** + * 文件名 + */ + private String baseFileName; + + /** + * 文件前缀 + */ + private final String appName; + + /** + * file must exist when writing + */ + private File curMetricFile; + private File curMetricIndexFile; + + private FileOutputStream outMetric; + private DataOutputStream outIndex; + private BufferedOutputStream outMetricBuf; + /** + * 单个文件大小, byte + *

10MB: 83886080 + *

20MB: 167772160 + *

30MB: 251658240 + */ + private final long singleFileSize; + + /** + * 保留的文件总数 + */ + private final int totalFileCount; + + /** + * 应用 pid + */ + private final int pid = SystemUtil.getPid(); + + private final boolean usePid; + + /** + * 秒级统计,忽略毫秒数。 + */ + private long lastSecond = -1; + + /** + * @param singleFileSize 单个文件大小 + * @param totalFileCount 总文件大小 + * @param baseDir 文件路径 + * @param appName 文件名称 + */ + public TrackerFileWriter(long singleFileSize, int totalFileCount, String baseDir, String appName, boolean usePid) { + if (singleFileSize <= 0 || totalFileCount <= 0) { + throw new IllegalArgumentException(); + } + log.info("[TRACKERS] 创建 FileWriter, 单文件大小[{}MB], 最大文件数[{}]", (singleFileSize / 8 / 1024 / 1024), totalFileCount); + + this.usePid = usePid; + this.baseDir = baseDir; + this.appName = appName; + File dir = new File(baseDir); + if (!dir.exists()) { + dir.mkdirs(); + } + + long time = System.currentTimeMillis(); + this.lastSecond = time / 1000; + this.singleFileSize = singleFileSize; + this.totalFileCount = totalFileCount; + try { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + this.timeSecondBase = df.parse("1970-01-01 00:00:00").getTime() / 1000; + } catch (Exception e) { + log.warn("[TRACKERS] 创建 FileWriter 失败", e); + } + } + + /** + * 如果传入了time,就认为nodes中所有的时间时间戳都是time. + */ + public synchronized void write(long time, String oneLineMsg) throws Exception { + if (oneLineMsg == null) { + return; + } + // first write, should create file + if (curMetricFile == null) { + baseFileName = formMetricFileName(appName, pid); + closeAndNewFile(nextFileNameOfDay(time)); + } + if (!(curMetricFile.exists() && curMetricIndexFile.exists())) { + closeAndNewFile(nextFileNameOfDay(time)); + } + + long second = time / 1000; + if (second < lastSecond) { + // 时间靠前的直接忽略,不应该发生。 + } else if (second == lastSecond) { + outMetricBuf.write(oneLineMsg.getBytes(CHARSET)); + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } else { + writeIndex(second, outMetric.getChannel().position()); + if (isNewDay(lastSecond, second)) { + closeAndNewFile(nextFileNameOfDay(time)); + outMetricBuf.write(oneLineMsg.getBytes(CHARSET)); + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } else { + outMetricBuf.write(oneLineMsg.getBytes(CHARSET)); + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } + lastSecond = second; + } + } + + public synchronized void close() throws Exception { + if (outMetricBuf != null) { + outMetricBuf.close(); + } + if (outIndex != null) { + outIndex.close(); + } + } + + private void writeIndex(long time, long offset) throws Exception { + outIndex.writeLong(time); + outIndex.writeLong(offset); + outIndex.flush(); + } + + private String nextFileNameOfDay(long time) { + List list = new ArrayList<>(); + File baseFile = new File(baseDir); + DateFormat fileNameDf = new SimpleDateFormat("yyyy-MM-dd"); + String dateStr = fileNameDf.format(new Date(time)); + String fileNameModel = baseFileName + "." + dateStr; + for (File file : Objects.requireNonNull(baseFile.listFiles())) { + String fileName = file.getName(); + if (fileName.contains(fileNameModel) + && !fileName.endsWith(METRIC_FILE_INDEX_SUFFIX) + && !fileName.endsWith(".lck")) { + list.add(file.getAbsolutePath()); + } + } + Collections.sort(list, METRIC_FILE_NAME_CMP); + if (list.isEmpty()) { + return baseDir + fileNameModel; + } + String last = list.get(list.size() - 1); + int n = 0; + String[] strs = last.split("\\."); + if (strs.length > 0 && strs[strs.length - 1].matches("[0-9]{1,10}")) { + n = Integer.parseInt(strs[strs.length - 1]); + } + return baseDir + fileNameModel + "." + (n + 1); + } + + /** + * A comparator for metric file name. Metric file name is like:
+ *

+     * metrics.log.2018-03-06
+     * metrics.log.2018-03-07
+     * metrics.log.2018-03-07.10
+     * metrics.log.2018-03-06.100
+     * 
+ *

+ * File name with the early date is smaller, if date is same, the one with the small file number is smaller. + * Note that if the name is an absolute path, only the fileName({@link File#getName()}) part will be considered. + * So the above file names should be sorted as:
+ *

+     * metrics.log.2018-03-06
+     * metrics.log.2018-03-06.100
+     * metrics.log.2018-03-07
+     * metrics.log.2018-03-07.10
+     *
+     * 
+ *

+ */ + private static final class MetricFileNameComparator implements Comparator { + + private final String pid = "pid"; + + @Override + public int compare(String o1, String o2) { + String name1 = new File(o1).getName(); + String name2 = new File(o2).getName(); + String dateStr1 = name1.split("\\.")[2]; + String dateStr2 = name2.split("\\.")[2]; + // in case of file name contains pid, skip it, like Sentinel-Admin-metrics.log.pid22568.2018-12-24 + if (dateStr1.startsWith(pid)) { + dateStr1 = name1.split("\\.")[3]; + dateStr2 = name2.split("\\.")[3]; + } + + // compare date first + int t = dateStr1.compareTo(dateStr2); + if (t != 0) { + return t; + } + + // same date, compare file number + t = name1.length() - name2.length(); + if (t != 0) { + return t; + } + return name1.compareTo(name2); + } + } + + /** + * Get all metric files' name in {@code baseDir}. The file name must like + *
+     * baseFileName + ".yyyy-MM-dd.number"
+     * 
+ * and not endsWith {@link #METRIC_FILE_INDEX_SUFFIX} or ".lck". + * + * @param baseDir the directory to search. + * @param baseFileName the file name pattern. + * @return the metric files' absolute path({@link File#getAbsolutePath()}) + * @throws Exception + */ + static List listMetricFiles(String baseDir, String baseFileName) throws Exception { + List list = new ArrayList(); + File baseFile = new File(baseDir); + File[] files = baseFile.listFiles(); + if (files == null) { + return list; + } + for (File file : files) { + String fileName = file.getName(); + if (file.isFile() + && fileNameMatches(fileName, baseFileName) + && !fileName.endsWith(TrackerFileWriter.METRIC_FILE_INDEX_SUFFIX) + && !fileName.endsWith(".lck")) { + list.add(file.getAbsolutePath()); + } + } + Collections.sort(list, TrackerFileWriter.METRIC_FILE_NAME_CMP); + return list; + } + + /** + * Test whether fileName matches baseFileName. fileName matches baseFileName when + *
+     * fileName = baseFileName + ".yyyy-MM-dd.number"
+     * 
+ * + * @param fileName file name + * @param baseFileName base file name. + * @return if fileName matches baseFileName return true, else return false. + */ + public static boolean fileNameMatches(String fileName, String baseFileName) { + if (fileName.startsWith(baseFileName)) { + String part = fileName.substring(baseFileName.length()); + // part is like: ".yyyy-MM-dd.number", eg. ".2018-12-24.11" + return part.matches("\\.[0-9]{4}-[0-9]{2}-[0-9]{2}(\\.[0-9]*)?"); + } else { + return false; + } + } + + private void removeMoreFiles() throws Exception { + List list = listMetricFiles(baseDir, baseFileName); + if (list == null || list.isEmpty()) { + return; + } + for (int i = 0; i < list.size() - totalFileCount + 1; i++) { + String fileName = list.get(i); + String indexFile = formIndexFileName(fileName); + new File(fileName).delete(); + log.info("[TRACKERS] Removing metric file: " + fileName); + new File(indexFile).delete(); + log.info("[TRACKERS] Removing metric index file: " + indexFile); + } + } + + /** + * @param fileName + * @throws Exception + */ + private void closeAndNewFile(String fileName) throws Exception { + removeMoreFiles(); + if (outMetricBuf != null) { + outMetricBuf.close(); + } + if (outIndex != null) { + outIndex.close(); + } + // 新的文件将是一个全新的文件, 无法在最后追加, 若流关闭再次打开相同文件将会清空, 但实际不会打开相同的文件. + outMetric = new FileOutputStream(fileName, append); + outMetricBuf = new BufferedOutputStream(outMetric); + curMetricFile = new File(fileName); + String idxFile = formIndexFileName(fileName); + curMetricIndexFile = new File(idxFile); + outIndex = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(idxFile, append))); + log.info("[TRACKERS] New metric file created: " + fileName); + log.info("[TRACKERS] New metric index file created: " + idxFile); + } + + private boolean validSize() throws Exception { + long size = outMetric.getChannel().size(); + return size < singleFileSize; + } + + private boolean isNewDay(long lastSecond, long second) { + long lastDay = (lastSecond - timeSecondBase) / 86400; + long newDay = (second - timeSecondBase) / 86400; + return newDay > lastDay; + } + + /** + * Form metric file name use the specific appName and pid. Note that only + * form the file name, not include path. + *

+ * Note: {@link MetricFileNameComparator}'s implementation relays on the metric file name, + * we should be careful when changing the metric file name. + * + * @param appName + * @param pid + * @return metric file name. + */ + public String formMetricFileName(String appName, int pid) { + if (appName == null) { + appName = ""; + } + // dot is special char that should be replaced. + final String dot = "."; + final String separator = "-"; + if (appName.contains(dot)) { + appName = appName.replace(dot, separator); + } + String name = appName + separator + METRIC_FILE; + if (usePid) { + name += ".pid" + pid; + } + return name; + } + + /** + * Form index file name of the {@code metricFileName} + * + * @param metricFileName + * @return the index file name of the metricFileName + */ + public static String formIndexFileName(String metricFileName) { + return metricFileName + METRIC_FILE_INDEX_SUFFIX; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerRepository.java b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerRepository.java new file mode 100644 index 0000000..ae3ae9c --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/java/com/blossom/expand/tracker/core/repository/TrackerRepository.java @@ -0,0 +1,38 @@ +package com.blossom.expand.tracker.core.repository; + +import com.blossom.expand.tracker.core.SpanNode; + +import java.util.List; + +/** + * tracker span 信息持久化接口 + *

tracker 客户端和服务端的实现不同 + *

客户端的实现目前有

+ *
    + *
  1. disk: 保存在本地日志中 + *
+ * + *

服务端接收到应用自身,或其他应用的 span 信息,通过如下方式保存

+ * 本项目是 xz.irda.tracker 框架的简单版本, 不提供服务方 + * + * @author xzzz + */ +public interface TrackerRepository { + + /** + * 保存 span 集合, + * + * @param spanNodes span 集合 + * @return true: 保存成功; false:保存失败 + */ + boolean save(List spanNodes); + + /** + * span 信息到期 + * + * @return 删除的条数 + */ + default long expire() { + return 0; + } +} diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring-configuration-metadata.json b/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..a0a283f --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,108 @@ +{ + "groups": [ + { + "name": "project.tracker", + "type": "com.blossom.expand.tracker.core.TrackerProperties", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties" + }, + { + "name": "project.tracker.collector", + "type": "com.blossom.expand.tracker.core.TrackerProperties$Collector", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties" + }, + { + "name": "project.tracker.repository", + "type": "com.blossom.expand.tracker.core.TrackerProperties$Repository", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties" + }, + { + "name": "project.tracker.repository.disk", + "type": "com.blossom.expand.tracker.core.TrackerProperties$Disk", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Repository" + }, + { + "name": "tracker.redis", + "type": "com.blossom.expand.tracker.core.adapter.redis.TrackerRedisProperties", + "sourceType": "com.blossom.expand.tracker.core.adapter.redis.TrackerRedisProperties" + } + ], + "properties": [ + { + "name": "project.tracker.collector.enabled", + "type": "java.lang.Boolean", + "description": "是否开启本地收集, 需要开启收集", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Collector", + "defaultValue": false + }, + { + "name": "project.tracker.collector.ignore-trackers", + "type": "java.util.List", + "description": "忽略的追踪", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Collector" + }, + { + "name": "project.tracker.collector.max-cache", + "type": "java.lang.Integer", + "description": "收集器本地缓存最大大小, 默认5000

本地缓存超过该配置后, 将舍弃新的收集信息, 这是用于在远程服务无法连接时, 或本地磁盘无法写入时, 防止本地内存过大", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Collector", + "defaultValue": 5000 + }, + { + "name": "project.tracker.collector.rate", + "type": "java.lang.Integer", + "description": "采样率, 0 - 1000 之间的整数, 为 1000 则全部收集, 为 0 则不收集. 默认采样率为1\/1000

每个 span 会取 traceId 进行 hash, hash % 1000 后与该值比较, 小于该值则进行采集。

服务集群需要有相同的采样率, 否则数据将出现不完整的情况,

注意:<\/h3>
  1. 该配置仅在大量数据情况下保证整体采样率近似于该值, 并不能保证每 1000 个请求就有 rate 数的请求被采集<\/li>
  2. 出于性能考虑, 采样率过高时 tracker 选择丢弃 span 信息来保证系统稳定运行.<\/li> <\/ol>", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Collector", + "defaultValue": 1 + }, + { + "name": "project.tracker.repository.disk.log-dir", + "type": "java.lang.String", + "description": "记录的路径", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Disk", + "defaultValue": "\/tracker\/" + }, + { + "name": "project.tracker.repository.disk.single-file-size", + "type": "java.lang.Long", + "description": "单个文件大小, byte

    10MB: 83886080

    20MB: 167772160

    30MB: 251658240", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Disk", + "defaultValue": 251658240 + }, + { + "name": "project.tracker.repository.disk.total-file-count", + "type": "java.lang.Integer", + "description": "保留的文件总数", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Disk", + "defaultValue": 30 + }, + { + "name": "project.tracker.repository.disk.use-pid", + "type": "java.lang.Boolean", + "description": "追踪文件名增加 pid, 为了在本地启动多个项目时进行区分", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Disk", + "defaultValue": false + }, + { + "name": "project.tracker.repository.type", + "type": "java.lang.String", + "description": "收集方式, 在对象注入时会进行判断 {@link TrackerConfiguration#trackerRepository(TrackerProperties)}

    1. disk: 将 span 信息保存在磁盘中, 目前仅支持该种方式, 但不允许开启.", + "sourceType": "com.blossom.expand.tracker.core.TrackerProperties$Repository", + "defaultValue": "disk" + }, + { + "name": "tracker.redis.detail-to-record", + "type": "java.lang.Boolean", + "description": "redis 请求详情保存至 Tracker.Record 中", + "sourceType": "com.blossom.expand.tracker.core.adapter.redis.TrackerRedisProperties", + "defaultValue": false + }, + { + "name": "tracker.redis.enabled", + "type": "java.lang.Boolean", + "description": "开启 redis 追踪", + "sourceType": "com.blossom.expand.tracker.core.adapter.redis.TrackerRedisProperties", + "defaultValue": true + } + ], + "hints": [] +} \ No newline at end of file diff --git a/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring.factories b/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..32b1a75 --- /dev/null +++ b/blossom-backend/expand-tracker/expand-tracker-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.blossom.expand.tracker.core.TrackerProperties,\ + com.blossom.expand.tracker.core.TrackerConfiguration,\ + com.blossom.expand.tracker.core.adapter.spring.TrackerSpringConfiguration,\ + com.blossom.expand.tracker.core.adapter.redis.TrackerRedisConfiguration,\ + com.blossom.expand.tracker.core.adapter.mysql.TrackerMysqlConfiguration + diff --git a/blossom-backend/expand-tracker/pom.xml b/blossom-backend/expand-tracker/pom.xml new file mode 100644 index 0000000..d7532a7 --- /dev/null +++ b/blossom-backend/expand-tracker/pom.xml @@ -0,0 +1,20 @@ + + + + blossom-backend + com.blossom + 1.0.0-SNAPSHOT + + 4.0.0 + + expand-tracker + pom + + + expand-tracker-core + + + + \ No newline at end of file diff --git a/blossom-backend/pom.xml b/blossom-backend/pom.xml new file mode 100644 index 0000000..c78050a --- /dev/null +++ b/blossom-backend/pom.xml @@ -0,0 +1,217 @@ + + + 4.0.0 + pom + + + org.springframework.boot + spring-boot-starter-parent + 2.7.11 + + + + com.blossom + blossom-backend + 1.0.0-SNAPSHOT + + + + 1.0.0-SNAPSHOT + + + 1.8 + UTF-8 + 1.8 + 1.8 + + + 2.7.11 + + + 1.18.8 + + 7.10.2 + 2.9.3 + + + + + backend + common + expand-tracker + expand-sentinel + + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + aliyun + https://maven.aliyun.com/repository/public + + true + + + false + + + + + + + aliyun-plugin + https://maven.aliyun.com/repository/public + + true + + + false + + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + provided + + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + + + + org.elasticsearch.client + elasticsearch-rest-client + ${elasticsearch.version} + + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + ${elasticsearch.version} + + + + + org.elasticsearch.client + transport + ${elasticsearch.version} + + + + org.elasticsearch.plugin + transport-netty4-client + ${elasticsearch.version} + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + + + + com.blossom + common-base + ${revision} + + + + com.blossom + common-cache + ${revision} + + + + com.blossom + common-db + ${revision} + + + + com.blossom + common-elasticsearch + ${revision} + + + + com.blossom + common-iaas + ${revision} + + + + com.blossom + common-wechat + ${revision} + + + + com.blossom + expand-sentinel-metric + ${revision} + + + + com.blossom + expand-sentinel-springmvc + ${revision} + + + + + com.blossom + expand-tracker-core + ${revision} + + + + com.blossom + service-base + ${revision} + + + + com.blossom + service-biz + ${revision} + + + + com.blossom + service-flow + ${revision} + + + + + + diff --git a/blossom-editor/.editorconfig b/blossom-editor/.editorconfig new file mode 100644 index 0000000..3dce414 --- /dev/null +++ b/blossom-editor/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/blossom-editor/.eslintignore b/blossom-editor/.eslintignore new file mode 100644 index 0000000..a6f34fe --- /dev/null +++ b/blossom-editor/.eslintignore @@ -0,0 +1,4 @@ +node_modules +dist +out +.gitignore diff --git a/blossom-editor/.eslintrc.cjs b/blossom-editor/.eslintrc.cjs new file mode 100644 index 0000000..75e8f4c --- /dev/null +++ b/blossom-editor/.eslintrc.cjs @@ -0,0 +1,38 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + env: { + browser: true, + commonjs: true, + es6: true, + node: true, + 'vue/setup-compiler-macros': true + }, + extends: [ + 'plugin:vue/vue3-recommended', + 'eslint:recommended', + '@vue/eslint-config-typescript/recommended', + '@vue/eslint-config-prettier' + ], + rules: { + '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-var-requires': 'off', + 'vue/require-default-prop': 'off', + 'vue/multi-word-component-names': 'off' + }, + overrides: [ + { + files: ['*.js'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off' + } + } + ] +} diff --git a/blossom-editor/.gitignore b/blossom-editor/.gitignore new file mode 100644 index 0000000..e7c3088 --- /dev/null +++ b/blossom-editor/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +out +*.log* diff --git a/blossom-editor/.npmrc b/blossom-editor/.npmrc new file mode 100644 index 0000000..fdc1a98 --- /dev/null +++ b/blossom-editor/.npmrc @@ -0,0 +1 @@ +ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ diff --git a/blossom-editor/.prettierignore b/blossom-editor/.prettierignore new file mode 100644 index 0000000..9c6b791 --- /dev/null +++ b/blossom-editor/.prettierignore @@ -0,0 +1,6 @@ +out +dist +pnpm-lock.yaml +LICENSE.md +tsconfig.json +tsconfig.*.json diff --git a/blossom-editor/.prettierrc.yaml b/blossom-editor/.prettierrc.yaml new file mode 100644 index 0000000..35893b3 --- /dev/null +++ b/blossom-editor/.prettierrc.yaml @@ -0,0 +1,4 @@ +singleQuote: true +semi: false +printWidth: 100 +trailingComma: none diff --git a/blossom-editor/README.md b/blossom-editor/README.md new file mode 100644 index 0000000..978cd73 --- /dev/null +++ b/blossom-editor/README.md @@ -0,0 +1,94 @@ +# blossom + +`Blossom` MD 文档管理与编辑 + + +## 启动安装 +```bash +# Install +$ npm install + +# Development +$ npm run dev + +# Build +# For windows +$ npm run build:win +# For macOS +$ npm run build:mac +# For Linux +$ npm run build:linux + +``` + +# 文档 + +## 相关文档 + +1. vue: https://cn.vuejs.org/guide/introduction.html +2. electron-vite: https://cn-evite.netlify.app/guide/ +3. element-plus: https://element-plus.gitee.io/zh-CN/component/button.html +4. iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=1823192 +5. tui-editor: https://nhn.github.io/tui.editor/latest/ToastUIEditorCore +6. electron: https://www.electronjs.org/zh/docs/latest/api/app + +```java +public String flowCheck () throws InterruptedException { + FlowRule rule = new FlowRule(); + rule.setLimitApp("default"); + // 资源名 + rule.setResource("getTest"); + // 系统预热限流方式 + rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); + // 阈值类型,QPS,系统预热其实只能针对QPS进行判断 + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + // 阈值个数,系统进入稳定期时最大的放行速度,QPS + rule.setCount(50); + // 系统进入稳定期需要的时长,单位秒,实际并不是精准的在这个时间进入稳定期的 + rule.setWarmUpPeriodSec(60); + try (Entry ignored = SphU.entry("getTest")) { + System.out.println("SUCC"); + } catch (BlockException e) { + System.out.println("> FAIL"); + } +} +``` + + +# 文档结构 + +文档分为两种: +1. 文件夹 Folder +2. 文章 Article + +认为文件夹也是一种文档,只不过不具有正文内容`content`. + +| Tree | field | Folder | Article | 说明 | +| ---- | ---------------- | ---------------- | ---------- | ---------------------------------------------- | +| | type | folder | article | 类型 | +| i | id | id | id | 唯一ID | +| p | pid | pid | pid | 所属的父级内容,Article 的父级通常是个 Folder | +| n | name | name | name | 名称 | +| icon | icon | icon | icon | 图标 | +| t | tags | tags | tags | 标签集合 | +| s | sort | sort | sort | 排序 | +| | cover | cover | cover | 封面图 | +| | describes | describes | describes | 描述 | +| o | openStatus | openStatus | openStatus | 公开状态 0:不公开;1:公开 | +| | openTime | | openTime | 发布时间 | +| star | starStatus | | starStatus | 收藏状态 0:不收藏;1:收藏 | +| | pv | | pv | 页面的查看数 | +| | uv | | uv | 独立的访问次数,每日IP重置 | +| | likes | | likes | 点赞数 | +| | words | | words | 文章字数 | +| | version | | version | 文章字数 | +| | sp | storePath | | 图片的存储路径,图片会存储在对应路径下,方便整理 | +| | subject_words | subject_words | | 专题字数 | +| | subject_upd_time | subject_upd_time | | 专题最后修改时间 | +| | creTime | creTime | creTime | 创建时间 | +| | updTime | updTime | updTime | 修改时间 | + +# 功能列表 + +全局 WebSocket 实现文章的保存 + diff --git a/blossom-editor/build/entitlements.mac.plist b/blossom-editor/build/entitlements.mac.plist new file mode 100644 index 0000000..38c887b --- /dev/null +++ b/blossom-editor/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/blossom-editor/build/icon.icns b/blossom-editor/build/icon.icns new file mode 100644 index 0000000..28644aa Binary files /dev/null and b/blossom-editor/build/icon.icns differ diff --git a/blossom-editor/build/icon.ico b/blossom-editor/build/icon.ico new file mode 100644 index 0000000..3000a6c Binary files /dev/null and b/blossom-editor/build/icon.ico differ diff --git a/blossom-editor/build/icon.png b/blossom-editor/build/icon.png new file mode 100644 index 0000000..c9e665f Binary files /dev/null and b/blossom-editor/build/icon.png differ diff --git a/blossom-editor/build/notarize.js b/blossom-editor/build/notarize.js new file mode 100644 index 0000000..f3a44b3 --- /dev/null +++ b/blossom-editor/build/notarize.js @@ -0,0 +1,36 @@ +const { notarize } = require('@electron/notarize') + +module.exports = async (context) => { + if (process.platform !== 'darwin') return + + console.log('aftersign hook triggered, start to notarize app.') + + if (!process.env.CI) { + console.log(`skipping notarizing, not in CI.`) + return + } + + if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { + console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') + return + } + + const appId = 'com.electron.app' + + const { appOutDir } = context + + const appName = context.packager.appInfo.productFilename + + try { + await notarize({ + appBundleId: appId, + appPath: `${appOutDir}/${appName}.app`, + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLEIDPASS + }) + } catch (error) { + console.error(error) + } + + console.log(`done notarizing ${appId}.`) +} diff --git a/blossom-editor/dev-app-update.yml b/blossom-editor/dev-app-update.yml new file mode 100644 index 0000000..d4e897e --- /dev/null +++ b/blossom-editor/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: blossom-updater diff --git a/blossom-editor/electron-builder.yml b/blossom-editor/electron-builder.yml new file mode 100644 index 0000000..be30f67 --- /dev/null +++ b/blossom-editor/electron-builder.yml @@ -0,0 +1,50 @@ +appId: com.xzzz.blossom +productName: blossom +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' +asarUnpack: + - resources/** +afterSign: build/notarize.js +win: + executableName: blossom +# https://www.electron.build/configuration/nsis +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always + # 安装退保 + # installerIcon: 'icon.ico' + # 不允许一键安装 + oneClick: false + # 允许用户指定安装目录, oneClick 必须为 false + allowToChangeInstallationDirectory: true +mac: + entitlementsInherit: build/entitlements.mac.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates diff --git a/blossom-editor/electron.vite.config.ts b/blossom-editor/electron.vite.config.ts new file mode 100644 index 0000000..9125ed0 --- /dev/null +++ b/blossom-editor/electron.vite.config.ts @@ -0,0 +1,45 @@ +import { resolve } from 'path' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + plugins: [vue()], + resolve: { + alias: { + '@renderer': resolve('src/renderer/src') + } + }, + css: { + preprocessorOptions: { + scss: { + /** + * 如果引入多个文件,可以使用 + * '@import "@/assets/scss/globalVariable1.scss";@import"@/assets/scss/globalVariable2.scss";' + */ + additionalData: '@import "@renderer/assets/styles/config.scss";', + } + } + }, + build: { + // 警告大小, 单位kb + // chunkSizeWarningLimit: 1000, + rollupOptions: { + output: { + //@ts-ignore + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + } + } + } + } + } +}) diff --git a/blossom-editor/package-lock.json b/blossom-editor/package-lock.json new file mode 100644 index 0000000..97883c8 --- /dev/null +++ b/blossom-editor/package-lock.json @@ -0,0 +1,11893 @@ +{ + "name": "blossom", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "blossom", + "version": "1.0.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@codemirror/lang-markdown": "^6.2.0", + "@codemirror/language-data": "^6.3.1", + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^1.0.2", + "@types/marked": "^5.0.0", + "axios": "^1.4.0", + "codemirror": "^6.0.1", + "echarts": "^5.4.2", + "electron-updater": "^5.3.0", + "element-plus": "^2.3.7", + "highlight.js": "^11.8.0", + "marked": "^5.1.0", + "marked-highlight": "^2.0.1", + "pinia": "^2.1.3", + "sass": "^1.63.3", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@electron/notarize": "^1.2.3", + "@rushstack/eslint-patch": "^1.3.0", + "@types/node": "^18.16.16", + "@vitejs/plugin-vue": "^4.2.3", + "@vue/eslint-config-prettier": "^7.1.0", + "@vue/eslint-config-typescript": "^11.0.3", + "electron": "^24.4.1", + "electron-builder": "^23.6.0", + "electron-vite": "^1.0.23", + "eslint": "^8.42.0", + "eslint-plugin-vue": "^9.14.1", + "prettier": "^2.8.8", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vue": "^3.3.4", + "vue-tsc": "^1.6.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.8.1.tgz", + "integrity": "sha512-HpphvDcTdOx+9R3eUw9hZK9JA77jlaBF0kOt2McbyfvY0rX9pnMoO8rkkZc0GzSbzhIY4m5xJ0uHHgjfqHNmXQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", + "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.2.tgz", + "integrity": "sha512-Nq7lmx9SU+JyoaRcs6SaJs7uAmW2W06HpgJVQYeZptVGNWDzDvzhjwVb/ZuG1rwTlOocY4Y9GwNOBuKCeJbKtw==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.0.tgz", + "integrity": "sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.5.tgz", + "integrity": "sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.2.2", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz", + "integrity": "sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.1.tgz", + "integrity": "sha512-ABcsKBjLbyPZwPR5gePpc8jEKCQrFF4pby2WlMVdmJOOr7OWwwyz8DZonPx/cKDE00hfoSLc8F7yAcn/d6+rTQ==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz", + "integrity": "sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.5.2.tgz", + "integrity": "sha512-VYiCbApDDRUVx3k0jtZ+b5h2hMkMKZpPR9tx+VgVA3Euuf2MVjVsjx/S2+4AlJjKjS5J6z9+4bEvtponK96e4A==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.2.tgz", + "integrity": "sha512-D4YrefiRBAr+CfEIM4S3yvGSbYW+N69mttIfGMEf7diHpRbmygDxS+R/5xSqjgtkY6VO6qmUrre1GkRcWeZa9A==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz", + "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz", + "integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.8.0.tgz", + "integrity": "sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.3.1.tgz", + "integrity": "sha512-p6jhJmvhGe1TG1EGNhwH7nFWWFSTJ8NDKnB2fVx5g3t+PpO0+63R7GJNxjS0TmmH3cdMxZbzejsik+rlEh1EyQ==", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.2.tgz", + "integrity": "sha512-ki5sqNKWzKi5AKvpVE6Cna4Q+SgxYuYVLAZFSsMjGBWx5qSVa+D+xipix65GS3f2syTfAD9pXKMX4i4p49eneQ==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.3.0.tgz", + "integrity": "sha512-tzxOVQNoDhhwFNfcTO2IB74wQoWarARcH6gv3YufPpiJ9yhcb7zD6JCkO5+FWARskqRFc8GFa6E+wUyOvADl5A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.0.tgz", + "integrity": "sha512-64/M40YeJPToKvGO6p3fijo2vwUEj4nACEAXElCaYQ50HrXSvRaK+NHEhSh73WFBGdvIdhrV+lL9PdJy2RfCYA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.1.tgz", + "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" + }, + "node_modules/@codemirror/view": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.14.0.tgz", + "integrity": "sha512-I263FPs4In42MNmrdwN2DfmYPFMVMXgT7o/mxdGp4jv5LPs8i0FOxzmxF5yeeQdYSTztb2ZhmPIu0ahveInVTg==", + "dependencies": { + "@codemirror/state": "^6.1.4", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz", + "integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==", + "engines": { + "node": ">=10" + } + }, + "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, + "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-toolkit/preload": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-2.0.0.tgz", + "integrity": "sha512-zpZDzbqJTZQC5d4LRs2EKruKWnqah+T75s+niBYFemYLtiW5TTZcWi3Q8UxHqnwTudDMuWJb233aaS2yjx3Xiw==", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron-toolkit/tsconfig": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", + "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", + "dev": true, + "peerDependencies": { + "@types/node": "*" + } + }, + "node_modules/@electron-toolkit/utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-1.0.2.tgz", + "integrity": "sha512-EHqVijVPdjBJBivxJH5E/Gu6Dzd5GfoUH0Ilbtx+EQarwaeJLPLxRM5DzD54uuhs1b8TGDI7uMYmcJW2CKrijg==", + "dependencies": { + "jszip": "^3.7.1" + }, + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "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==", + "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/get/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==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/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==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz", + "integrity": "sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", + "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", + "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.9.tgz", + "integrity": "sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==", + "dependencies": { + "@floating-ui/core": "^1.2.6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "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/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@lezer/common": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.3.tgz", + "integrity": "sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA==" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.0.tgz", + "integrity": "sha512-zUHrjNFuY/DOZCkOBJ6qItQIkcopHM/Zv/QOE0a4XNG3HDNahxTNu5fQYl8dIuKCpxCqRdMl5cEwl5zekFc7BA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.2.tgz", + "integrity": "sha512-5TKMAReXukfEmIiZprDlGfZVfOOCyEStFi1YLzxclm9H3G/HHI49/2wzlRT6bQw5r7PoZVEtjTItEkb/UuZQyg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", + "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.4.tgz", + "integrity": "sha512-HdJYMVZcT4YsMo7lW3ipL4NoyS2T67kMPuSVS5TgLGqmaCjEU/D6xv7zsa1ktvTK5lwk7zzF1e3eU6gBZIPm5g==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.3.tgz", + "integrity": "sha512-kKN17wmgP1cgHb8juR4pwVSPMKkDMzY/lAPbBsZ1fpXwbk2sg3N1kIrf0q+LefxgrANaQb/eNO7+m2QPruTFng==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.3.tgz", + "integrity": "sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==", + "dependencies": { + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.7.tgz", + "integrity": "sha512-ssHKb3p0MxhJXT2i7UBmgAY1BIM3Uq/D772Qutu3EVmxWIyNMU12nQ0rL3Fhu+MiFtiTzyTmd3xGwEf3ON5PSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.4.tgz", + "integrity": "sha512-Qgqrau7dc91S9L5EGWuNEawgqQe4rVaD4ebkyTAJYKlNN3lUGjvsX4Osv0a/06m6beU23jg4A+rZ0x2oB2c97w==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.1.tgz", + "integrity": "sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.7.tgz", + "integrity": "sha512-RbhKQ9+Y/r/Xv6OcJmETEM5tBFdpdAJRqrgi3akJkWBLCuiAaLP/jKdYzu+ICljaSXPCQeznrv+r9HUEnjq3HQ==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.1.tgz", + "integrity": "sha512-S/aYAzABzMqWLfKKqV89pCWME4yjZYC6xzD02l44wbmb0sHxmN9/8aE4GULrKFzFaGazHdXcGEbPZ4zzB6yqwQ==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "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" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "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, + "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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz", + "integrity": "sha512-RkmuBcqiNioeeBKbgzMlOdreUkJfYaSjwgx9XDgGGpjvWgyaxWvDmZVSN9CS6LjEASadhgPv2BcFp+SeouWXXA==", + "dev": true + }, + "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==", + "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==", + "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, + "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==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "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, + "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, + "optional": true, + "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==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "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==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz", + "integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/marked": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz", + "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==" + }, + "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, + "optional": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.16.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.17.tgz", + "integrity": "sha512-QAkjjRA1N7gPJeAP4WLXZtYv6+eMXFNviqktCDt4GLcmCugMr5BcRHfkOjCQzvCsnMp+L79a54zBkbw356xv9Q==" + }, + "node_modules/@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "dev": true, + "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==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + }, + "node_modules/@types/verror": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "dev": true, + "optional": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": 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==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz", + "integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/type-utils": "5.59.9", + "@typescript-eslint/utils": "5.59.9", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz", + "integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/typescript-estree": "5.59.9", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz", + "integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/visitor-keys": "5.59.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz", + "integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.9", + "@typescript-eslint/utils": "5.59.9", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz", + "integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz", + "integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/visitor-keys": "5.59.9", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz", + "integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/typescript-estree": "5.59.9", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz", + "integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.9", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", + "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.4.1.tgz", + "integrity": "sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.4.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.4.1.tgz", + "integrity": "sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==", + "dev": true, + "dependencies": { + "muggle-string": "^0.2.2" + } + }, + "node_modules/@volar/typescript": { + "version": "1.4.1-patch.2", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.4.1-patch.2.tgz", + "integrity": "sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.4.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/vue-language-core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.6.5.tgz", + "integrity": "sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.4.1", + "@volar/source-map": "1.4.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/compiler-sfc": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.2.2", + "vue-template-compiler": "^2.7.14" + } + }, + "node_modules/@volar/vue-language-core/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@volar/vue-language-core/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@volar/vue-typescript": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.6.5.tgz", + "integrity": "sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==", + "dev": true, + "dependencies": { + "@volar/typescript": "1.4.1-patch.2", + "@volar/vue-language-core": "1.6.5" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", + "integrity": "sha512-Pv/lVr0bAzSIHLd9iz0KnvAr4GKyCEl+h52bc4e5yWuDVtLgFwycF7nrbWTAQAS+FU6q1geVd07lc6EWfJiWKQ==", + "dev": true, + "dependencies": { + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0" + }, + "peerDependencies": { + "eslint": ">= 7.28.0", + "prettier": ">= 2.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.3.tgz", + "integrity": "sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", + "vue-eslint-parser": "^9.1.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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, + "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, + "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, + "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==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/universal": "1.2.1", + "@malept/flatpak-bundler": "^0.4.0", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^4.0.10", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^3.1.2", + "read-config-file": "6.2.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.7", + "tar": "^6.1.11", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + }, + "optionalDependencies": { + "@types/glob": "^7.1.1" + } + }, + "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, + "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, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "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, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.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 + }, + "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" + } + ] + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "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 + }, + "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, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "optional": true + }, + "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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", + "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001489", + "electron-to-chromium": "^1.4.411", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "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==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, + "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 + }, + "node_modules/builder-util": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "@types/fs-extra": "^9.0.11", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.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.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "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, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001498", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz", + "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "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==", + "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/chokidar/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==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "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 + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "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/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "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==", + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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 + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "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, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "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, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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==", + "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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "optional": true + }, + "node_modules/dir-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", + "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "dev": true, + "dependencies": { + "buffer-equal": "1.0.0", + "colors": "1.0.3", + "commander": "2.9.0", + "minimatch": "3.0.4" + }, + "bin": { + "dircompare": "src/cli/dircompare.js" + } + }, + "node_modules/dir-compare/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "dev": true, + "dependencies": { + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.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, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.4.3" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-24.5.0.tgz", + "integrity": "sha512-9Xo2EFZHWeuw1otm9mcJYKCNC64fPRpgp+ZJWMJ9RtvsnSgcuitkM4esZv4gIsqhWk5yiKApYHqinIUyu82O0Q==", + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "dev": true, + "dependencies": { + "@types/yargs": "^17.0.1", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "dmg-builder": "23.6.0", + "fs-extra": "^10.0.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.2.0", + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.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-osx-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", + "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", + "dev": true, + "dependencies": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^3.0.1" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "dependencies": { + "buffer-alloc": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/electron-osx-sign/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/electron-publish": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "fs-extra": "^10.0.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, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz", + "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==", + "dev": true + }, + "node_modules/electron-updater": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-5.3.0.tgz", + "integrity": "sha512-iKEr7yQBcvnQUPnSDYGSWC9t0eF2YbZWeYYYZzYxdl+HiRejXFENjYMnYjoOm2zxyD6Cr2JTHZhp9pqxiXuCOw==", + "dependencies": { + "@types/semver": "^7.3.6", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5", + "typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/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==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-1.0.23.tgz", + "integrity": "sha512-L4df40Lqq16ltsXXeGtD/Xa3PkK8UJG6eY0WtMiNdSNmsFNV7gaE4WzjbMCqFeg9bv09Pkhh9o3amUbvHAAmdg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.1", + "@babel/plugin-transform-arrow-functions": "^7.21.5", + "cac": "^6.7.14", + "esbuild": "^0.17.19", + "magic-string": "^0.30.0", + "picocolors": "^1.0.0" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/element-plus": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.7.tgz", + "integrity": "sha512-h6TxclbaLUJxg/Bv5j/ZKsK+K5yadQliw5+R30HWyE69pXlqXTX24oYx+yw3pA4Dy+lqEDi5501FQ0CORk3OSA==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.0.6", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.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 + }, + "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==", + "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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "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==", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", + "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.42.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.14.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz", + "integrity": "sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.3.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^9.3.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.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==", + "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" + ], + "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 + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/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 + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.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, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "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/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, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/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/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 + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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==", + "optional": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.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, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "optional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "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==", + "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==", + "dev": true, + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "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/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "optional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "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==" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "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==", + "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==", + "optional": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "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==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", + "engines": { + "node": ">=12.0.0" + } + }, + "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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/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/hosted-git-info/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/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==" + }, + "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, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "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, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "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, + "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" + } + ], + "optional": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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==", + "dev": true, + "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==" + }, + "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==", + "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, + "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==", + "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, + "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==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "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, + "engines": { + "node": ">= 8.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 + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "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==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "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 + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "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==", + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "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==" + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/marked": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.0.tgz", + "integrity": "sha512-z3/nBe7aTI8JDszlYLk7dDVNpngjw0o1ZJtrA9kIfkkHcIF+xH7mO23aISl4WxP83elU+MFROgahqdpd05lMEQ==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-highlight": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.0.1.tgz", + "integrity": "sha512-LDUfR/zDvD+dJ+lQOWHkxvBLNxiXcaN8pBtwJ/i4pI0bkDC/Ef6Mz1qUrAuHXfnpdr2rabdMpVFhqFuU+5Mskg==", + "peerDependencies": { + "marked": "^4 || ^5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.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==", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "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, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/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/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "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==" + }, + "node_modules/muggle-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz", + "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "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, + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "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==", + "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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "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==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pinia": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.3.tgz", + "integrity": "sha512-XNA/z/ye4P5rU1pieVmh0g/hSuDO98/a5UC8oSP0DNdvt6YtetJNHTrXwpwsQuflkGT34qKxAEcp7lSxXNjf/A==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/plist": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "dev": true, + "dependencies": { + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "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/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true, + "peer": true + }, + "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==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-config-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", + "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "dev": true, + "dependencies": { + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "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==", + "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.24.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.1.tgz", + "integrity": "sha512-REHe5dx30ERBRFS0iENPHy+t6wtSEYkjrhwNsLyh3qpRaZ1+aylvMUdMBUHWUD/RjjLmLzEvY8Z9XRlpcdIkHA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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==" + }, + "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 + }, + "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, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sass": { + "version": "1.63.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.3.tgz", + "integrity": "sha512-ySdXN+DVpfwq49jG1+hmtDslYqpS7SkOR5GpF6o2bmb1RL/xS+wvPmegMvMywyfsmAV6p7TgwXYGrCZIFFbAHg==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "optional": true + }, + "node_modules/semver/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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "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==", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.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, + "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, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "optional": true + }, + "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, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dependencies": { + "safe-buffer": "~5.1.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, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", + "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "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.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "dev": true, + "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/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/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, + "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, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "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, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "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, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "optional": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "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==" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "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/verror/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, + "optional": true + }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz", + "integrity": "sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-router": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz", + "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.6.5.tgz", + "integrity": "sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==", + "dev": true, + "dependencies": { + "@volar/vue-language-core": "1.6.5", + "@volar/vue-typescript": "1.6.5", + "semver": "^7.3.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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==" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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, + "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, + "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==", + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true + }, + "@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==" + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, + "@codemirror/autocomplete": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.8.1.tgz", + "integrity": "sha512-HpphvDcTdOx+9R3eUw9hZK9JA77jlaBF0kOt2McbyfvY0rX9pnMoO8rkkZc0GzSbzhIY4m5xJ0uHHgjfqHNmXQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", + "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/lang-angular": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.2.tgz", + "integrity": "sha512-Nq7lmx9SU+JyoaRcs6SaJs7uAmW2W06HpgJVQYeZptVGNWDzDvzhjwVb/ZuG1rwTlOocY4Y9GwNOBuKCeJbKtw==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "@codemirror/lang-css": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.0.tgz", + "integrity": "sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "@codemirror/lang-html": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.5.tgz", + "integrity": "sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.2.2", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz", + "integrity": "sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "@codemirror/lang-less": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.1.tgz", + "integrity": "sha512-ABcsKBjLbyPZwPR5gePpc8jEKCQrFF4pby2WlMVdmJOOr7OWwwyz8DZonPx/cKDE00hfoSLc8F7yAcn/d6+rTQ==", + "requires": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-markdown": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz", + "integrity": "sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==", + "requires": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "requires": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, + "@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "requires": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "@codemirror/lang-sql": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.5.2.tgz", + "integrity": "sha512-VYiCbApDDRUVx3k0jtZ+b5h2hMkMKZpPR9tx+VgVA3Euuf2MVjVsjx/S2+4AlJjKjS5J6z9+4bEvtponK96e4A==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-vue": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.2.tgz", + "integrity": "sha512-D4YrefiRBAr+CfEIM4S3yvGSbYW+N69mttIfGMEf7diHpRbmygDxS+R/5xSqjgtkY6VO6qmUrre1GkRcWeZa9A==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "@codemirror/lang-wast": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz", + "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-xml": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz", + "integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.8.0.tgz", + "integrity": "sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/language-data": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.3.1.tgz", + "integrity": "sha512-p6jhJmvhGe1TG1EGNhwH7nFWWFSTJ8NDKnB2fVx5g3t+PpO0+63R7GJNxjS0TmmH3cdMxZbzejsik+rlEh1EyQ==", + "requires": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0" + } + }, + "@codemirror/legacy-modes": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.2.tgz", + "integrity": "sha512-ki5sqNKWzKi5AKvpVE6Cna4Q+SgxYuYVLAZFSsMjGBWx5qSVa+D+xipix65GS3f2syTfAD9pXKMX4i4p49eneQ==", + "requires": { + "@codemirror/language": "^6.0.0" + } + }, + "@codemirror/lint": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.3.0.tgz", + "integrity": "sha512-tzxOVQNoDhhwFNfcTO2IB74wQoWarARcH6gv3YufPpiJ9yhcb7zD6JCkO5+FWARskqRFc8GFa6E+wUyOvADl5A==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.0.tgz", + "integrity": "sha512-64/M40YeJPToKvGO6p3fijo2vwUEj4nACEAXElCaYQ50HrXSvRaK+NHEhSh73WFBGdvIdhrV+lL9PdJy2RfCYA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.1.tgz", + "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" + }, + "@codemirror/view": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.14.0.tgz", + "integrity": "sha512-I263FPs4In42MNmrdwN2DfmYPFMVMXgT7o/mxdGp4jv5LPs8i0FOxzmxF5yeeQdYSTztb2ZhmPIu0ahveInVTg==", + "requires": { + "@codemirror/state": "^6.1.4", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@ctrl/tinycolor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz", + "integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==" + }, + "@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, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "@electron-toolkit/preload": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-2.0.0.tgz", + "integrity": "sha512-zpZDzbqJTZQC5d4LRs2EKruKWnqah+T75s+niBYFemYLtiW5TTZcWi3Q8UxHqnwTudDMuWJb233aaS2yjx3Xiw==", + "requires": {} + }, + "@electron-toolkit/tsconfig": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", + "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", + "dev": true, + "requires": {} + }, + "@electron-toolkit/utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-1.0.2.tgz", + "integrity": "sha512-EHqVijVPdjBJBivxJH5E/Gu6Dzd5GfoUH0Ilbtx+EQarwaeJLPLxRM5DzD54uuhs1b8TGDI7uMYmcJW2CKrijg==", + "requires": { + "jszip": "^3.7.1" + } + }, + "@electron/get": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", + "requires": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "dependencies": { + "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==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + } + }, + "@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "dev": true, + "requires": { + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + } + }, + "@element-plus/icons-vue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz", + "integrity": "sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==", + "requires": {} + }, + "@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", + "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", + "dev": true + }, + "@floating-ui/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", + "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==" + }, + "@floating-ui/dom": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.9.tgz", + "integrity": "sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==", + "requires": { + "@floating-ui/core": "^1.2.6" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@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==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@lezer/common": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.3.tgz", + "integrity": "sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA==" + }, + "@lezer/cpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.0.tgz", + "integrity": "sha512-zUHrjNFuY/DOZCkOBJ6qItQIkcopHM/Zv/QOE0a4XNG3HDNahxTNu5fQYl8dIuKCpxCqRdMl5cEwl5zekFc7BA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/css": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.2.tgz", + "integrity": "sha512-5TKMAReXukfEmIiZprDlGfZVfOOCyEStFi1YLzxclm9H3G/HHI49/2wzlRT6bQw5r7PoZVEtjTItEkb/UuZQyg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/highlight": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", + "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/html": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.4.tgz", + "integrity": "sha512-HdJYMVZcT4YsMo7lW3ipL4NoyS2T67kMPuSVS5TgLGqmaCjEU/D6xv7zsa1ktvTK5lwk7zzF1e3eU6gBZIPm5g==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/java": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.3.tgz", + "integrity": "sha512-kKN17wmgP1cgHb8juR4pwVSPMKkDMzY/lAPbBsZ1fpXwbk2sg3N1kIrf0q+LefxgrANaQb/eNO7+m2QPruTFng==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.3.tgz", + "integrity": "sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==", + "requires": { + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.7.tgz", + "integrity": "sha512-ssHKb3p0MxhJXT2i7UBmgAY1BIM3Uq/D772Qutu3EVmxWIyNMU12nQ0rL3Fhu+MiFtiTzyTmd3xGwEf3ON5PSA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/markdown": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.4.tgz", + "integrity": "sha512-Qgqrau7dc91S9L5EGWuNEawgqQe4rVaD4ebkyTAJYKlNN3lUGjvsX4Osv0a/06m6beU23jg4A+rZ0x2oB2c97w==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@lezer/php": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.1.tgz", + "integrity": "sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "@lezer/python": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.7.tgz", + "integrity": "sha512-RbhKQ9+Y/r/Xv6OcJmETEM5tBFdpdAJRqrgi3akJkWBLCuiAaLP/jKdYzu+ICljaSXPCQeznrv+r9HUEnjq3HQ==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/sass": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.1.tgz", + "integrity": "sha512-S/aYAzABzMqWLfKKqV89pCWME4yjZYC6xzD02l44wbmb0sHxmN9/8aE4GULrKFzFaGazHdXcGEbPZ4zzB6yqwQ==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "@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, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@popperjs/core": { + "version": "npm:@sxzz/popperjs-es@2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" + }, + "@rushstack/eslint-patch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz", + "integrity": "sha512-RkmuBcqiNioeeBKbgzMlOdreUkJfYaSjwgx9XDgGGpjvWgyaxWvDmZVSN9CS6LjEASadhgPv2BcFp+SeouWXXA==", + "dev": true + }, + "@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==" + }, + "@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==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@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 + }, + "@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==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@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, + "requires": { + "@types/node": "*" + } + }, + "@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, + "optional": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@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==" + }, + "@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" + }, + "@types/lodash-es": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz", + "integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/marked": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz", + "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==" + }, + "@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, + "optional": true + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "@types/node": { + "version": "18.16.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.17.tgz", + "integrity": "sha512-QAkjjRA1N7gPJeAP4WLXZtYv6+eMXFNviqktCDt4GLcmCugMr5BcRHfkOjCQzvCsnMp+L79a54zBkbw356xv9Q==" + }, + "@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + }, + "@types/verror": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "dev": true, + "optional": true + }, + "@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz", + "integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/type-utils": "5.59.9", + "@typescript-eslint/utils": "5.59.9", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz", + "integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/typescript-estree": "5.59.9", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz", + "integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/visitor-keys": "5.59.9" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz", + "integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.59.9", + "@typescript-eslint/utils": "5.59.9", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz", + "integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz", + "integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/visitor-keys": "5.59.9", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz", + "integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.9", + "@typescript-eslint/types": "5.59.9", + "@typescript-eslint/typescript-estree": "5.59.9", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz", + "integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.9", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@vitejs/plugin-vue": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", + "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", + "dev": true, + "requires": {} + }, + "@volar/language-core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.4.1.tgz", + "integrity": "sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==", + "dev": true, + "requires": { + "@volar/source-map": "1.4.1" + } + }, + "@volar/source-map": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.4.1.tgz", + "integrity": "sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==", + "dev": true, + "requires": { + "muggle-string": "^0.2.2" + } + }, + "@volar/typescript": { + "version": "1.4.1-patch.2", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.4.1-patch.2.tgz", + "integrity": "sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==", + "dev": true, + "requires": { + "@volar/language-core": "1.4.1" + } + }, + "@volar/vue-language-core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.6.5.tgz", + "integrity": "sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==", + "dev": true, + "requires": { + "@volar/language-core": "1.4.1", + "@volar/source-map": "1.4.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/compiler-sfc": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.2.2", + "vue-template-compiler": "^2.7.14" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@volar/vue-typescript": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.6.5.tgz", + "integrity": "sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==", + "dev": true, + "requires": { + "@volar/typescript": "1.4.1-patch.2", + "@volar/vue-language-core": "1.6.5" + } + }, + "@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "requires": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "requires": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "requires": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "requires": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "@vue/eslint-config-prettier": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", + "integrity": "sha512-Pv/lVr0bAzSIHLd9iz0KnvAr4GKyCEl+h52bc4e5yWuDVtLgFwycF7nrbWTAQAS+FU6q1geVd07lc6EWfJiWKQ==", + "dev": true, + "requires": { + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0" + } + }, + "@vue/eslint-config-typescript": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.3.tgz", + "integrity": "sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", + "vue-eslint-parser": "^9.1.1" + } + }, + "@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "requires": { + "@vue/shared": "3.3.4" + } + }, + "@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "requires": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "requires": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "requires": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "requires": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + }, + "@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "requires": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "requires": {} + } + } + }, + "@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" + }, + "@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "requires": { + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "requires": {} + } + } + }, + "7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "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, + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "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, + "requires": {} + }, + "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 + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "app-builder-lib": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "dev": true, + "requires": { + "@develar/schema-utils": "~2.6.5", + "@electron/universal": "1.2.1", + "@malept/flatpak-bundler": "^0.4.0", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^4.0.10", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^3.1.2", + "read-config-file": "6.2.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.7", + "tar": "^6.1.11", + "temp-file": "^3.4.0" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + } + }, + "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, + "optional": true + }, + "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, + "optional": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "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 + }, + "async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "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 + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "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 + }, + "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 + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "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, + "requires": { + "bluebird": "^3.5.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "optional": true + }, + "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, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", + "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001489", + "electron-to-chromium": "^1.4.411", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "optional": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, + "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 + }, + "builder-util": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "dev": true, + "requires": { + "@types/debug": "^4.1.6", + "@types/fs-extra": "^9.0.11", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.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" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "builder-util-runtime": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "requires": { + "debug": "^4.3.4", + "sax": "^1.2.4" + } + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "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==" + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "requires": { + "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" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001498", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz", + "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.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" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "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 + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "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, + "optional": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "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, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "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 + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "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 + }, + "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 + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "is-what": "^3.14.1" + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "requires": { + "buffer": "^5.1.0" + } + }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "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, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "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==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "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==" + } + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "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==" + }, + "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==", + "optional": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "optional": true + }, + "dir-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", + "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "dev": true, + "requires": { + "buffer-equal": "1.0.0", + "colors": "1.0.3", + "commander": "2.9.0", + "minimatch": "3.0.4" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dmg-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "dev": true, + "requires": { + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "dmg-license": "^1.0.11", + "fs-extra": "^10.0.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "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, + "optional": true, + "requires": { + "@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" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "requires": { + "tslib": "2.3.0", + "zrender": "5.4.3" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-24.5.0.tgz", + "integrity": "sha512-9Xo2EFZHWeuw1otm9mcJYKCNC64fPRpgp+ZJWMJ9RtvsnSgcuitkM4esZv4gIsqhWk5yiKApYHqinIUyu82O0Q==", + "requires": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + } + }, + "electron-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "dev": true, + "requires": { + "@types/yargs": "^17.0.1", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "dmg-builder": "23.6.0", + "fs-extra": "^10.0.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.2.0", + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "electron-osx-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "electron-publish": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "dev": true, + "requires": { + "@types/fs-extra": "^9.0.11", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "fs-extra": "^10.0.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "electron-to-chromium": { + "version": "1.4.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz", + "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==", + "dev": true + }, + "electron-updater": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-5.3.0.tgz", + "integrity": "sha512-iKEr7yQBcvnQUPnSDYGSWC9t0eF2YbZWeYYYZzYxdl+HiRejXFENjYMnYjoOm2zxyD6Cr2JTHZhp9pqxiXuCOw==", + "requires": { + "@types/semver": "^7.3.6", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5", + "typed-emitter": "^2.1.0" + }, + "dependencies": { + "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==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "electron-vite": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-1.0.23.tgz", + "integrity": "sha512-L4df40Lqq16ltsXXeGtD/Xa3PkK8UJG6eY0WtMiNdSNmsFNV7gaE4WzjbMCqFeg9bv09Pkhh9o3amUbvHAAmdg==", + "dev": true, + "requires": { + "@babel/core": "^7.22.1", + "@babel/plugin-transform-arrow-functions": "^7.21.5", + "cac": "^6.7.14", + "esbuild": "^0.17.19", + "magic-string": "^0.30.0", + "picocolors": "^1.0.0" + } + }, + "element-plus": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.7.tgz", + "integrity": "sha512-h6TxclbaLUJxg/Bv5j/ZKsK+K5yadQliw5+R30HWyE69pXlqXTX24oYx+yw3pA4Dy+lqEDi5501FQ0CORk3OSA==", + "requires": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.0.6", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + } + }, + "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 + }, + "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==", + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "prr": "~1.0.1" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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==", + "devOptional": true + }, + "eslint": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", + "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.42.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "requires": {} + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-vue": { + "version": "9.14.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz", + "integrity": "sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.3.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^9.3.0", + "xml-name-validator": "^4.0.0" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + }, + "espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "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==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "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==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "optional": true + }, + "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 + }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "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, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "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 + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "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==", + "requires": { + "pend": "~1.2.0" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "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, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "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==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "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, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "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, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "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 + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "optional": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "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 + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "optional": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "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==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "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==", + "optional": true, + "requires": { + "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" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "optional": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@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" + } + }, + "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==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "optional": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "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 + }, + "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==", + "optional": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "optional": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "optional": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==" + }, + "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, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "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, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "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==" + }, + "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, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "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==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "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, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "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, + "optional": true, + "requires": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + } + }, + "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, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "optional": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "peer": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "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, + "requires": { + "ci-info": "^3.2.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "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 + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "optional": true, + "peer": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "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==" + }, + "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 + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "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==", + "optional": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "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==" + }, + "less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^2.3.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "requires": {} + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "marked": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.0.tgz", + "integrity": "sha512-z3/nBe7aTI8JDszlYLk7dDVNpngjw0o1ZJtrA9kIfkkHcIF+xH7mO23aISl4WxP83elU+MFROgahqdpd05lMEQ==" + }, + "marked-highlight": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.0.1.tgz", + "integrity": "sha512-LDUfR/zDvD+dJ+lQOWHkxvBLNxiXcaN8pBtwJ/i4pI0bkDC/Ef6Mz1qUrAuHXfnpdr2rabdMpVFhqFuU+5Mskg==", + "requires": {} + }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "optional": true, + "requires": { + "escape-string-regexp": "^4.0.0" + } + }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "muggle-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz", + "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "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, + "optional": true + }, + "node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "optional": true, + "peer": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "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 + }, + "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 + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "peer": true + }, + "pinia": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.3.tgz", + "integrity": "sha512-XNA/z/ye4P5rU1pieVmh0g/hSuDO98/a5UC8oSP0DNdvt6YtetJNHTrXwpwsQuflkGT34qKxAEcp7lSxXNjf/A==", + "requires": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "requires": {} + } + } + }, + "plist": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "dev": true, + "requires": { + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + } + }, + "postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "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==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true, + "peer": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "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==" + }, + "read-config-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", + "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "dev": true, + "requires": { + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + } + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "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" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "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 + }, + "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==" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "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==", + "optional": true, + "requires": { + "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" + } + }, + "rollup": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.1.tgz", + "integrity": "sha512-REHe5dx30ERBRFS0iENPHy+t6wtSEYkjrhwNsLyh3qpRaZ1+aylvMUdMBUHWUD/RjjLmLzEvY8Z9XRlpcdIkHA==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "optional": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "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, + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "sass": { + "version": "1.63.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.3.tgz", + "integrity": "sha512-ySdXN+DVpfwq49jG1+hmtDslYqpS7SkOR5GpF6o2bmb1RL/xS+wvPmegMvMywyfsmAV6p7TgwXYGrCZIFFbAHg==", + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "type-fest": "^0.13.1" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "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, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "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 + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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, + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "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, + "optional": true + }, + "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 + }, + "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==" + }, + "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, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "optional": true + }, + "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 + }, + "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==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "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, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "style-mod": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", + "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==" + }, + "sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "requires": { + "debug": "^4.1.0" + } + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "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, + "requires": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "requires": { + "tmp": "^0.2.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "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==", + "requires": { + "is-number": "^7.0.0" + } + }, + "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, + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, + "tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "optional": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "optional": true + }, + "typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "requires": { + "rxjs": "*" + } + }, + "typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "devOptional": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "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, + "optional": true + } + } + }, + "vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + } + }, + "vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "requires": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "vue-eslint-parser": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz", + "integrity": "sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "dependencies": { + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "vue-router": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz", + "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==", + "requires": { + "@vue/devtools-api": "^6.5.0" + } + }, + "vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "vue-tsc": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.6.5.tgz", + "integrity": "sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==", + "dev": true, + "requires": { + "@volar/vue-language-core": "1.6.5", + "@volar/vue-typescript": "1.6.5", + "semver": "^7.3.8" + } + }, + "w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "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, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "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" + } + }, + "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 + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "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 + }, + "zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "requires": { + "tslib": "2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + } + } +} diff --git a/blossom-editor/package.json b/blossom-editor/package.json new file mode 100644 index 0000000..3738caa --- /dev/null +++ b/blossom-editor/package.json @@ -0,0 +1,64 @@ +{ + "name": "blossom", + "version": "1.0.0", + "description": "A markdown editor", + "license": "MIT", + "main": "./out/main/index.js", + "author": "xzzz", + "homepage": "https://www.wangyunf.com", + "keywords": [ + "markdown", + "editor" + ], + "scripts": { + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev --watch", + "build": "npm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:win": "npm run build && electron-builder --win --config ", + "build:mac": "npm run build && electron-builder --mac --config", + "build:linux": "npm run build && electron-builder --linux --config" + }, + "dependencies": { + "@codemirror/lang-markdown": "^6.2.0", + "@codemirror/language-data": "^6.3.1", + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^1.0.2", + "@types/marked": "^5.0.0", + "axios": "^1.4.0", + "codemirror": "^6.0.1", + "echarts": "^5.4.2", + "electron-updater": "^5.3.0", + "element-plus": "^2.3.7", + "highlight.js": "^11.8.0", + "marked": "^5.1.0", + "marked-highlight": "^2.0.1", + "pinia": "^2.1.3", + "sass": "^1.63.3", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@electron/notarize": "^1.2.3", + "@rushstack/eslint-patch": "^1.3.0", + "@types/node": "^18.16.16", + "@vitejs/plugin-vue": "^4.2.3", + "@vue/eslint-config-prettier": "^7.1.0", + "@vue/eslint-config-typescript": "^11.0.3", + "electron": "^24.4.1", + "electron-builder": "^23.6.0", + "electron-vite": "^1.0.23", + "eslint": "^8.42.0", + "eslint-plugin-vue": "^9.14.1", + "prettier": "^2.8.8", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vue": "^3.3.4", + "vue-tsc": "^1.6.5" + } +} diff --git a/blossom-editor/resources/bak/icon.png b/blossom-editor/resources/bak/icon.png new file mode 100644 index 0000000..17f19ba Binary files /dev/null and b/blossom-editor/resources/bak/icon.png differ diff --git a/blossom-editor/resources/imgs/guardcat_logo.jpg b/blossom-editor/resources/imgs/guardcat_logo.jpg new file mode 100644 index 0000000..72f390d Binary files /dev/null and b/blossom-editor/resources/imgs/guardcat_logo.jpg differ diff --git a/blossom-editor/resources/imgs/icon.ico b/blossom-editor/resources/imgs/icon.ico new file mode 100644 index 0000000..3000a6c Binary files /dev/null and b/blossom-editor/resources/imgs/icon.ico differ diff --git a/blossom-editor/resources/imgs/icon.png b/blossom-editor/resources/imgs/icon.png new file mode 100644 index 0000000..c9e665f Binary files /dev/null and b/blossom-editor/resources/imgs/icon.png differ diff --git a/blossom-editor/resources/screenshot/PrScrn.dll b/blossom-editor/resources/screenshot/PrScrn.dll new file mode 100644 index 0000000..85695cd Binary files /dev/null and b/blossom-editor/resources/screenshot/PrScrn.dll differ diff --git a/blossom-editor/resources/screenshot/PrintScr.exe b/blossom-editor/resources/screenshot/PrintScr.exe new file mode 100644 index 0000000..fb2fa70 Binary files /dev/null and b/blossom-editor/resources/screenshot/PrintScr.exe differ diff --git a/blossom-editor/resources/screenshot/README.md b/blossom-editor/resources/screenshot/README.md new file mode 100644 index 0000000..ac40aab --- /dev/null +++ b/blossom-editor/resources/screenshot/README.md @@ -0,0 +1,12 @@ +## 前言 +聊天软件需要截图功能。我们找到了一个方法,QQ dll,包装一下生成 exe 文件,我用 Node.js 去调用完成截图。 + +1. 第一步先用 Node 执行微信封装的 exe,然后会把截图复制到剪切板 +2. 然后调用浏览把剪切板的内容复制出来 +``` + var screen_window = execFile(__dirname + '/screen/PrintScr.exe') + screen_window.on('exit', function (code) { + // 执行成功返回 1,返回 0 没有截图 + if (code) mainWindow.webContents.paste() + }) +``` diff --git a/blossom-editor/src/main/index.ts b/blossom-editor/src/main/index.ts new file mode 100644 index 0000000..b87b9db --- /dev/null +++ b/blossom-editor/src/main/index.ts @@ -0,0 +1,399 @@ +import { app, shell, ipcMain, BrowserWindow, Menu, IpcMainEvent, Tray, HandlerDetails } from 'electron' +import { join } from 'path' +import { electronApp, optimizer, is } from '@electron-toolkit/utils' +import icon from '../../resources/imgs/icon.ico?asset' +// 拓展功能 +import printScreen from "./printScreen" +import ShortcutRegistrant from './shortcut' + +// 主窗口 +let mainWindow: BrowserWindow | undefined +let tray: Tray +let blossomUserinfo: any + +// This method will be called when Electron has finished initialization and is ready to create browser windows. Some APIs can only be used after this event occurs. +/** + * ========================================================================================================================= + * APP 启动完成 + * ========================================================================================================================= + */ +app.whenReady().then(() => { + // Set app user model id for windows + electronApp.setAppUserModelId('com.electron') + /* + * Default open or close DevTools by F12 in development and ignore CommandOrControl + R in production. + * see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + */ + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) + + setTimeout(() => { + createMainWindow() + }, 300); + + + 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) createMainWindow() + }) + + // Quit when all windows are closed, except on macOS. There, it's common + // for applications and their menu bar to stay active until the user quits + // explicitly with Cmd + Q. + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } + }) +}) + + +/** + * ========================================================================================================================= + * 创建窗口 + * ========================================================================================================================= + */ +const buildWindow = (_title: string): BrowserWindow => { + Menu.setApplicationMenu(null) + let win = new BrowserWindow({ + show: true, + width: 1400, + height: 1000, + minWidth: 972, + minHeight: 700, + icon: icon, + frame: false,// 无边框 + transparent: false,// 透明窗口 + opacity: 1, + hasShadow: true, + autoHideMenuBar: false, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + // 是否沙盒模式, 沙盒模式下, 渲染进程可以直接与主进程通信 + sandbox: false + } + }); + return win; +} + +/** + * ========================================================================================================================= + * 创建主窗口 + * 官网API + * https://www.electronjs.org/zh/docs/latest/api/browser-window#new-browserwindowoptions + * ========================================================================================================================= + */ +function createMainWindow(): void { + if (mainWindow != undefined) { + return + } + mainWindow = buildWindow('Blossom') + mainWindow.setMenu(null) + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + // HMR for renderer base on electron-vite cli. Load the remote URL for development or the local html file for production. + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } + console.log('============================================================'); + // 开发环境自动打开控制台 + openDevToos(mainWindow) + // 创建 Tray + initTray() + // 创建焦点窗口事件 + initOnFocusedWindow() + // 主窗口监听事件 + initOnMainWindow(mainWindow) + // 注册全局快捷键 printScreen:截屏快捷键 + new ShortcutRegistrant(mainWindow).printScreen() + console.log('============================================================'); +} + +/** + * ========================================================================================================================= + * 托盘图标 Tray + * ========================================================================================================================= + */ +const initTray = () => { + console.log('1. 创建托盘 Tray'); + tray = new Tray(icon); + const contextMenu = Menu.buildFromTemplate([ + { label: '显示', click: () => mainWindow!.show() }, + { label: '退出', click: () => app.quit() } + ]) + tray.setToolTip('Blossom\n未登录') + tray.setContextMenu(contextMenu) + tray.addListener('double-click', () => { + mainWindow!.show() + }) +} + +/** + * ========================================================================================================================= + * 新的窗口 + * ========================================================================================================================= + */ +type WindowType = 'article' | 'wlIcon' | 'articleReference' +const newWindowMaps = new Map(); +function createNewWindow(windowType: WindowType, title: string, id?: number) { + console.log('打开新窗口, 窗口标题:', title); + let newWindow: BrowserWindow | undefined = newWindowMaps.get(windowType + '_' + title); + if (newWindow != undefined) { + newWindow.show(); + return; + } + newWindow = buildWindow(title); + console.log('窗口ID', newWindow.id); + + /** + * 新文章窗口 + */ + if (windowType === 'article') { + // HMR for renderer base on electron-vite cli. Load the remote URL for development or the local html file for production. + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + newWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/articleViewWindow?articleId=' + id); + } else { + // https://www.electronjs.org/zh/docs/latest/api/browser-window#winloadfilefilepath-options + newWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/articleViewWindow?articleId=' + id }) + } + } + /** + * 图标窗口 + */ + else if (windowType === 'wlIcon') { + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + newWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/iconListIndexWindow'); + } else { + newWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/iconListIndexWindow' }) + } + } + /** + * 文章引用网络窗口 + */ + else if (windowType === 'articleReference') { + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + newWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/articleReferenceWindow'); + } else { + newWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/articleReferenceWindow' }) + } + } + // 开发环境自动打开控制台 + openDevToos(newWindow) + initOnWindow(newWindow) + newWindow.on('closed', () => { + console.log('关闭新窗口, 窗口标题:', title); + newWindowMaps.delete(windowType + '_' + title) + }) + newWindowMaps.set(windowType + '_' + title, newWindow); +} + +// In this file you can include the rest of your app"s specific main process +// code. You can also put them in separate files and require them here. +/** + * 打开开发者工具 + * @param win 窗口 + */ +const openDevToos = (_win: BrowserWindow) => { + if (is.dev) { + // win.webContents.openDevTools({ mode: 'right' }) + } else { + // win.webContents.openDevTools({ mode: 'right' }) + } +} + +/** + * ========================================================================================================================= + * 主进程监听的事件 ipcMain + * ========================================================================================================================= + */ +const initOnFocusedWindow = (): void => { + console.log('2. 监听焦点窗口事件'); + /** + * 窗口最小化 + */ + ipcMain.on('window-min', () => { + BrowserWindow.getFocusedWindow()?.minimize(); + }) + /** + * 窗口最大化, 或变为原窗口大小 + */ + ipcMain.on('window-max', () => { + if (BrowserWindow.getFocusedWindow()?.isMaximized()) { + BrowserWindow.getFocusedWindow()?.unmaximize(); + } else { + BrowserWindow.getFocusedWindow()?.maximize() + } + }) + /** + * 隐藏窗口 + */ + ipcMain.on('window-hide', () => { + + if (BrowserWindow.getFocusedWindow()?.id != mainWindow!.id) { + BrowserWindow.getFocusedWindow()?.close() + } else { + BrowserWindow.getFocusedWindow()?.hide() + } + }) + /** + * 关闭窗口 + */ + ipcMain.on('window-close', () => { + BrowserWindow.getFocusedWindow()?.close() + }) + /** + * 打开控制台 + */ + ipcMain.on('openDevTools', () => { + if (BrowserWindow.getFocusedWindow()?.webContents.isDevToolsOpened()) { + BrowserWindow.getFocusedWindow()?.webContents.closeDevTools() + } else { + BrowserWindow.getFocusedWindow()?.webContents.openDevTools({ mode: 'right' }) + } + }) +} + +/** + * ========================================================================================================================= + * 主窗口监听的事件 mainWindow + * ========================================================================================================================= + */ +const initOnMainWindow = (mainWindow: BrowserWindow): void => { + console.log('3. 监听主窗口事件') + initOnWindow(mainWindow) + /** + * 窗口准备好后, 立即显示 + */ + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + /** + * 下载URL, 使用 mainWin.webContents.downloadURL(url) 触发 + * @param event + * @param item + * @param webContents + */ + mainWindow.webContents.session.on('will-download', (_event, item, _webContents) => { + console.log('准备下载文件'); + // 无参数时弹出文件选择框 + item.setSavePath('') + item.on('updated', (_event, state) => { + if (state === 'interrupted') { + console.log('Download is interrupted but can be resumed') + } else if (state === 'progressing') { + if (item.isPaused()) { + console.log('Download is paused') + } else { + console.log(`Received bytes: ${item.getReceivedBytes()}`) + } + } + }) + item.once('done', (_event, state) => { + if (state === 'completed') { + console.log('Download successfully') + } else { + console.log(`Download failed: ${state}`) + } + }) + }) + /* + * ========================================================================================================================= + * 主进程监听事件 + * ========================================================================================================================= + */ + /** + * 截屏 + */ + ipcMain.on('printScreen', () => { + printScreen(mainWindow) + }) + /** + * 登录后设置用户信息 + */ + ipcMain.on('set-userinfo', (_: IpcMainEvent, userinfo: any): void => { + blossomUserinfo = userinfo + console.log('当前登录用户:', userinfo); + tray.setToolTip(`Blossom\n用户: ${userinfo.username}\n昵称: ${userinfo.nickName}`) + }) + /** + * 新文章窗口 + */ + ipcMain.on('open-new-article-window', (_: IpcMainEvent, article: any): void => { + createNewWindow('article', article.name, article.id) + }) + /** + * 图标窗口 + */ + ipcMain.on('open-new-icon-window', (_: IpcMainEvent): void => { + createNewWindow('wlIcon', '图标') + }) + /** + * 文章引用网络 + */ + ipcMain.on('open-new-article-referece-window', (_: IpcMainEvent): void => { + createNewWindow('articleReference', '文章引用网络') + }) + /** + * 下载, 最终会调用 + * mainWin.webContents.session.on('will-download', (_event, item, _webContents) => {} + */ + ipcMain.on('download', (_event, url: string) => { + mainWindow.webContents.downloadURL(url) + }) +} + +/** + * ========================================================================================================================= + * 非主窗口的事件 + * ========================================================================================================================= + */ +export const initOnWindow = (window: BrowserWindow) => { + console.log('4. 监听非主窗口事件') + /** + * 拦截页面链接 + */ + window.webContents.on('will-navigate', (event: any, url: any) => { + interceptorATag(event, url) + }) + + window.webContents.setWindowOpenHandler((details: HandlerDetails): any => { + let url = (details.url as string) + if (url.startsWith(blossomUserinfo.params.WEB_ARTICLE_URL)) { + let articleId: string = url.replaceAll(blossomUserinfo.params.WEB_ARTICLE_URL, '') + createNewWindow('article', articleId, Number(articleId)) + } else { + shell.openExternal(url) + } + }) + +} + + +/** + * 拦截 a 标签 + * @param e + * @param url + */ +const interceptorATag = (e: any, url: any): boolean => { + e.preventDefault() + console.log(`[${new Date()}] electron 执行 标签拦截器`); + let innerUrl = (url as string) + if (innerUrl.startsWith(blossomUserinfo.params.WEB_ARTICLE_URL)) { + console.log(blossomUserinfo.params.WEB_ARTICLE_URL); + console.log(innerUrl); + let articleId: string = innerUrl.replaceAll(blossomUserinfo.params.WEB_ARTICLE_URL, '') + console.log(articleId); + createNewWindow('article', articleId, Number(articleId)) + } + else if (!is.dev) { + shell.openExternal(url) + } + return true; +} \ No newline at end of file diff --git a/blossom-editor/src/main/printScreen.ts b/blossom-editor/src/main/printScreen.ts new file mode 100644 index 0000000..d436639 --- /dev/null +++ b/blossom-editor/src/main/printScreen.ts @@ -0,0 +1,24 @@ +import { BrowserWindow } from 'electron' +// import { is } from '@electron-toolkit/utils' +import cp from "child_process" +import { join } from 'path'; + + +export const printScreen = (win: BrowserWindow) => { + let filePath = join(__dirname, '../../resources/screenshot/PrintScr.exe') + // 截屏时将页面隐藏 + win.hide() + const screen_window = cp.execFile(filePath) + let psCode = 0 + screen_window.on('exit', (code) => { + // 截图结果: 1:截图成功; 0:没有截图; + if (code) { + psCode = code + win.webContents.send('printScreenAfter', psCode) + } + win.show() + win.moveTop() + }) +} + +export default printScreen \ No newline at end of file diff --git a/blossom-editor/src/main/shortcut.ts b/blossom-editor/src/main/shortcut.ts new file mode 100644 index 0000000..7e2d426 --- /dev/null +++ b/blossom-editor/src/main/shortcut.ts @@ -0,0 +1,30 @@ +import { globalShortcut, BrowserWindow } from 'electron' +import { printScreen } from './printScreen' + +/** + * 全局快捷键注册器 + */ +class ShortcutRegistrant { + win: BrowserWindow; + + constructor(win: BrowserWindow) { + console.log('5. 注册快捷键'); + this.win = win; + } + + /** + * 注册截屏快捷键 + * CommandOrControl+Alt+Q + * @returns + */ + printScreen(): ShortcutRegistrant { + console.log(' 5.1 注册全局截图快捷键 [CommandOrControl+Alt+Q]'); + globalShortcut.register('CommandOrControl+Alt+Q', () => { + console.log('调用快捷键', 'CommandOrControl+Alt+Q'); + printScreen(this.win) + }) + return this; + } +} + +export default ShortcutRegistrant \ No newline at end of file diff --git a/blossom-editor/src/preload/index.d.ts b/blossom-editor/src/preload/index.d.ts new file mode 100644 index 0000000..8a65873 --- /dev/null +++ b/blossom-editor/src/preload/index.d.ts @@ -0,0 +1,9 @@ +import { ElectronAPI } from '@electron-toolkit/preload' + +declare global { + interface Window { + electron: ElectronAPI + electronAPI: ElectronAPI, + api: {} + } +} diff --git a/blossom-editor/src/preload/index.ts b/blossom-editor/src/preload/index.ts new file mode 100644 index 0000000..cf80a3c --- /dev/null +++ b/blossom-editor/src/preload/index.ts @@ -0,0 +1,121 @@ + +// @ts-ignore (define in dts) +import { contextBridge, ipcRenderer, clipboard, shell, OpenExternalOptions, NativeImage } from 'electron' +import { electronAPI } from '@electron-toolkit/preload' + +// Custom APIs for renderer +const api = { +} + +/** + * 主进程调用渲染进程方法 + */ +const ipcToRender = { + /** + * 截屏结束后, 主进程会调用该方法 + * @param callback 截屏回调 + * @returns + */ + printScreenAfter: (callback: any) => ipcRenderer.on('printScreenAfter', callback), + printLog: (callback: any) => ipcRenderer.on('printLog', callback), +} + + +/** + * 渲染进程调用主进程方法 + */ +const rednerToIpc = { + /** + * 窗口操作 + */ + windowMin: () => ipcRenderer.send('window-min'), + windowMax: () => ipcRenderer.send('window-max'), + windowHide: () => ipcRenderer.send('window-hide'), + windowClose: () => ipcRenderer.send('window-close'), + openDevTools: () => ipcRenderer.send('openDevTools'), + /** + * 设置用户信息 + * @param userinfo 用户信息 + * @returns + */ + setUserinfo: (userinfo: any) => ipcRenderer.send('set-userinfo', userinfo), + /** + * 下载文件 + * @param url 文件下载路径 + */ + printScreen: (): void => ipcRenderer.send('printScreen'), + /** + * 下载文件 + * @param url 文件下载路径 + */ + download: (url: string): void => ipcRenderer.send('download', url), + /** + * 在新窗口查看文章 + * @param article 新窗口查看文章 + */ + openNewArticleWindow: (article: any): void => ipcRenderer.send('open-new-article-window', article), + /** + * 新窗口查看图标 + * @returns + */ + openNewIconWindow: (): void => ipcRenderer.send('open-new-icon-window'), + /** + * 新窗口查看文章引用 + * @returns + */ + openNewArticleReferenceWindow: (): void => ipcRenderer.send('open-new-article-referece-window'), + /** + * 剪贴板, 读取文本 + * @returns 剪切板的文本 + */ + readText: (type?: 'selection' | 'clipboard'): string => clipboard.readText(type), + /** + * 剪切板, 读取图片, 并转换成 base64 字符串 + * @returns base64 字符串 + */ + readImageToDataUrl: (type?: 'selection' | 'clipboard'): string => clipboard.readImage(type).toDataURL(), + /** + * 剪切板, 读取图片, 并转换成 jpeg 图片 buffer + * @returns jpeg 图片 buffer + */ + readImageToJPEG: (type?: 'selection' | 'clipboard'): Buffer => clipboard.readImage(type).toJPEG(1), + /** + * 剪切板, 读取图片, 并转换成 jpeg 图片 buffer + * @returns jpeg 图片 buffer + */ + readImageToPNG: (type?: 'selection' | 'clipboard'): Buffer => clipboard.readImage(type).toPNG(), + /** + * 剪贴板, 写入文本 + * @param text 文本 + * @param type 类型 + * @returns + */ + writeText: (text: string, type?: 'selection' | 'clipboard') => clipboard.writeText(text, type), + /** + * 默认浏览器打开链接 + * @param url 链接路径 + * @param options + * @returns + */ + openExtenal: (url: string, options?: OpenExternalOptions) => shell.openExternal(url, options) +} + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('electronAPI', { ...rednerToIpc, ...ipcToRender }) + // contextBridge.exposeInMainWorld('electron', electronAPI) + // contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error(error) + } +} else { + // @ts-ignore (define in dts) + // window.electron = electronAPI + // @ts-ignore (define in dts) + window.api = api + // @ts-ignore (define in dts) + window.electronAPI = electronAPI +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/index.html b/blossom-editor/src/renderer/index.html new file mode 100644 index 0000000..bfa549d --- /dev/null +++ b/blossom-editor/src/renderer/index.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + +

    + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/App.vue b/blossom-editor/src/renderer/src/App.vue new file mode 100644 index 0000000..2675e5f --- /dev/null +++ b/blossom-editor/src/renderer/src/App.vue @@ -0,0 +1,31 @@ + + + + + + diff --git a/blossom-editor/src/renderer/src/api/auth.ts b/blossom-editor/src/renderer/src/api/auth.ts new file mode 100644 index 0000000..5709ee6 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/auth.ts @@ -0,0 +1,35 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +/** + * 登录 + * @param params + * @returns + */ +export const loginApi = (data?: object): Promise> => { + return rq.post>("/login", data); +}; + +export const logoutApi = (): Promise> => { + return rq.post>("/logout"); +}; + +export const checkApi = (): Promise> => { + return rq.get>("/check", {}); +}; + +export const userinfoApi = (params?: object): Promise> => { + return rq.get>("/user/info", { params }); +}; + +export const userUpdApi = (data?: object): Promise> => { + return rq.post("/user/upd", data); +}; + +export const userUpdPwdApi = (data?: object): Promise> => { + return rq.post("/user/upd/pwd", data); +}; + +export const userAddApi = (data?: object): Promise> => { + return rq.post("/user/add", data); +}; \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/blossom.ts b/blossom-editor/src/renderer/src/api/blossom.ts new file mode 100644 index 0000000..5605a08 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/blossom.ts @@ -0,0 +1,209 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +//#region ====================================================< sys >======================================================= + +/** + * 上传文件 + * @param data 文件 form + * @returns + */ +export const osconfigApi = (params?: object): Promise> => { + return rq.get>("/sys/osconfig", { params }); +} + +/** + * 上传文件 + * @param data 文件 form + * @returns + */ +export const uploadFileApiUrl = '/picture/file/upload' +export const uploadFileApi = (data?: object): Promise> => { + let config: object = { contentType: 'multipart/form-data;', } + return rq.post>(uploadFileApiUrl, data, config); +} + +//#endregion + +//#region ====================================================< doc >======================================================= + +/** + * + * @param params { + * onlyFolder: [Folder] 只查询文件夹, 包含文章文件夹和图片文件夹 + * onlyPicture: [Picture + Article]只查询图片文件夹, 以及含有图片的文章文件夹 + * onlyOpen: [Article]只查询公开的文章文件夹 + * onlySubject: [Article]只查询专题文件夹 + * onlyStar: [Article]问查询有 star 文章的文件夹 + * } + * @returns + */ +export const docTreeApi = (params?: object): Promise> => { + return rq.get>("/doc/trees", { params }); +} + +//#endregion + +//#region ====================================================< folder >==================================================== + +/** + * 查询文件夹详情 + * @param params + * @returns + */ +export const folderInfoApi = (params?: object): Promise> => { + return rq.get>("/folder/info", { params }); +} + +export const folderAddApi = (data?: object): Promise> => { + return rq.post>("/folder/add", data); +} + +export const folderUpdApi = (data?: object): Promise> => { + return rq.post>("/folder/upd", data); +} + +export const folderOpenApi = (data?: object): Promise> => { + return rq.post>("/folder/open", data); +} + +//#endregion + +//#region ====================================================< article >=================================================== + +/** + * 文章列表 + * @param params { + * starStatus: 0/1 + * openStatus: 0/1 + * } + * @returns + */ +export const articleListApi = (params?: object): Promise> => { + return rq.get>("/article/list", { params }); +} + +/** + * 查询文章详情, 如果文章为公开文章, 则会返回对应的公开信息, 如 openVersion, openTime 等 + *

    注意: 返回的正文信息永远是草稿正文, 公开版本的正文信息需要通过公开文章查询 {@link articleOpenApi} + * @param params { + * id: 文章ID, + * showToc: 返回 toc 目录, + * showMarkdown: 返回 markdown 正文, + * showHtml: 返回 html 正文 + * } + * @returns + */ +export const articleInfoApi = (params?: object): Promise> => { + return rq.get>("/article/info", { params }); +} + +/** + * 新增文章正文 + * @param data { pid: 0, name: "", icon: "", tags: "", sort: 0, cover: "", describes: ""} + * @returns + */ +export const articleAddApi = (data?: object): Promise> => { + return rq.post>("/article/add", data); +} + +/** + * 修改文章正文 + * @param data { id: 0, pid: 0, name: "", icon: "", tags: "", sort: 0, cover: "", describes: ""} + * @returns + */ +export const articleUpdApi = (data?: object): Promise> => { + return rq.post>("/article/upd", data); +} + +/** + * 修改文章正文 + * @param data { + * id: curDoc.value?.id, + * markdown: editor.getMarkdown(), + * html: editor.getHTML() + * } + * @returns + */ +export const articleUpdContentApi = (data?: object): Promise> => { + return rq.post>("/article/upd/content", data); +} + +export const articleStarApi = (data?: object): Promise> => { + return rq.post>("/article/star", data); +} + +export const articleOpenApi = (data?: object): Promise> => { + return rq.post>("/article/open", data); +} + +export const articleSyncApi = (data?: object): Promise> => { + return rq.post>("/article/open/sync", data); +} + +export const articleDownloadApi = (params?: object): Promise => { + let config = { params: params, responseType: 'blob' } + return rq.get("/article/download", config); +} + +export const subjectsApi = (params?: object): Promise> => { + return rq.get>("/folder/subjects", { params }); +} + +export const articleWordsApi = (params?: object): Promise> => { + return rq.get>("/article/stat/words", { params }); +} + +export const articleWordLineApi = (params?: object): Promise> => { + return rq.get>("/article/stat/line", { params }); +} + +export const articleHeatmapApi = (params?: object): Promise> => { + return rq.get>("/article/stat/heatmap", { params }); +} + + +export const articleRefListApi = (params?: object): Promise> => { + return rq.get>("/article/ref/list", { params }); +} + +//#endregion + +//#region ====================================================< picture >=================================================== + +/** + * 图片分页 + * @param params + * @returns + */ +export const picturePageApi = (params?: object): Promise> => { + return rq.get>("/picture/page", { params }); +} + +/** + * 星标图片 + * @param params {id:id} + * @returns + */ +export const pictureStarApi = (data?: object): Promise> => { + return rq.post>("/picture/star", data); +} + +/** + * 删除图片 + * @param params {id:id} + * @returns + */ +export const pictureDelApi = (data?: object): Promise> => { + return rq.post>("/picture/del", data); +} + +/** + * 删除图片 + * @param params {id:id} + * @returns + */ +export const pictureStatApi = (params?: object): Promise> => { + return rq.get>("/picture/stat", { params }); +} +//#endregion diff --git a/blossom-editor/src/renderer/src/api/config.ts b/blossom-editor/src/renderer/src/api/config.ts new file mode 100644 index 0000000..63c172a --- /dev/null +++ b/blossom-editor/src/renderer/src/api/config.ts @@ -0,0 +1,17 @@ +export class DomainConfig { + + /** + * 服务器地址 + */ + public static readonly domain: string = "https://www.wangyunf.com/xzserver/"; + + /** + * 生产环境地址 + */ + public static readonly domainPrd: string = "https://www.wangyunf.com/xzserver/"; + /** + * 开发环境地址 + */ + public static readonly domainLoc: string = "http://127.0.0.1:9999/"; + private constructor() { } +}; \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/note.ts b/blossom-editor/src/renderer/src/api/note.ts new file mode 100644 index 0000000..7676809 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/note.ts @@ -0,0 +1,18 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +export const noteAllApi = (params?: object): Promise> => { + return rq.get>("/note/list", { params }); +} + +export const noteAddApi = (data?: object): Promise> => { + return rq.post>("/note/add", data); +} + +export const noteDelApi = (data?: object): Promise> => { + return rq.post>("/note/del", data); +} + +export const noteTopApi = (data?: object): Promise> => { + return rq.post>("/note/top", data); +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/plan.ts b/blossom-editor/src/renderer/src/api/plan.ts new file mode 100644 index 0000000..b13cf97 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/plan.ts @@ -0,0 +1,22 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +export const planListDayApi = (params?: object): Promise> => { + return rq.get>("/plan/list/day", { params }); +} + +export const planListDailyApi = (params?: object): Promise> => { + return rq.get>("/plan/list/daily", { params }); +} + +export const planAddDailyApi = (data?: object): Promise> => { + return rq.post>("/plan/add/daily", data); +} + +export const planAddDayApi = (data?: object): Promise> => { + return rq.post>("/plan/add/day", data); +} + +export const planDelApi = (data?: object): Promise> => { + return rq.post>("/plan/del", data); +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/request.ts b/blossom-editor/src/renderer/src/api/request.ts new file mode 100644 index 0000000..38fd6d2 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/request.ts @@ -0,0 +1,141 @@ +// index.ts +import axios from "axios" +import pinia from "@renderer/stores/storeConfig" +import { toLogin } from "@renderer/router" +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios" +import { Local } from "@renderer/assets/utils/storage" +import { isNotNull } from "@renderer/assets/utils/obj" +import { storeKey as authKey, useUserStore } from '@renderer/stores/user' +import { storeKey as serverUrlKey } from '@renderer/stores/server' +import Notify from "@renderer/components/Notify" + +const userStore = useUserStore(pinia) + +export class Request { + /** axios 实例 */ + instance: AxiosInstance + /** 基础配置,url和超时时间 */ + baseConfig: AxiosRequestConfig = { + baseURL: Local.get(serverUrlKey), + timeout: 60000 + } + /** + * 构造方法中会设置拦截器逻辑,配置文件为可选项 + * @param config 配置文件内容,为可选项 + */ + constructor(config?: AxiosRequestConfig) { + this.instance = axios.create(Object.assign(this.baseConfig, config)) + /** + * 请求拦截 + */ + this.instance.interceptors.request.use( + (config: AxiosRequestConfig): any => { + let token: string = '' + let tokenCache = Local.get(authKey) + if (isNotNull(tokenCache) && isNotNull(tokenCache.token)) { + token = tokenCache.token + } + config.url = Local.get(serverUrlKey) + config.url + return { + ...config, + headers: { + "Authorization": 'Bearer ' + token + } + } + }, + (err: any) => { + return Promise.reject(err) + } + ) + /** + * 响应拦截 + */ + this.instance.interceptors.response.use( + (res: AxiosResponse) => { + const status = res.status + if (status !== 200) { + Promise.reject(res) + } + let data = res.data + // 本次响应是否正确 + let isSuccess = false + + // 返回文件流 + if (res.config.responseType === 'blob' || res.headers['content-type'] === 'application/force-download') { + // 返回文件流但仍然有JSON返回体(例如接口报错时), 仍要判断返回码 + if (isNotNull(data.code) && !isSuccessRCode(data.code)) { + isSuccess = false + } else { + isSuccess = true + return res + } + } + // 响应码为正确的直接返回 + if (isSuccessRCode(data.code)) { + isSuccess = true + } + + if (isSuccess) { + return data + } else if (data.code === 'AUTH-40101') { + /* 授权被拦截, 则需要退回登录页请求 */ + console.log('授权失败, 重置登录状态'); + userStore.reset() + toLogin() + return Promise.reject(res) + } else { + /* 其他接口报错, 直接拒绝并提示错误信息 */ + let errorResponse = data + errorResponse['url'] = res.config.url + Notify.error(data.msg, '请求失败') + return Promise.reject(res) + } + }, + (err: any) => { + let errorMsg = err.message + let code = err.code + if (code === "ERR_NETWORK") { + errorMsg = "网络错误,请检查您的网络是否通畅" + } + Notify.error(errorMsg, '请求失败') + return Promise.reject(err) + } + ); + } + + // 定义请求方法 + public request(config: AxiosRequestConfig): Promise { + return this.instance.request(config) + } + + public get(url: string, params?: object): Promise> { + return this.instance.get(url, params) + } + + public post(url: string, data?: object, config?: object): Promise> { + return this.instance.post(url, data, config) + } + + public delete(url: string, params?: object): Promise> { + return this.instance.delete(url, params) + } +} + +export interface R { + ok: boolean + code: number + msg: string + traceId: string + data?: T +} + +/** + * 判断接口响应码是否正确 + * @param code 接口响应码 + * @returns + */ +const isSuccessRCode = (code: string | number): boolean => { + return code === 0 || code === '0' || code === 20000 || code === '20000' +} + +export const defaultRequest = new Request() \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/sentinel.ts b/blossom-editor/src/renderer/src/api/sentinel.ts new file mode 100644 index 0000000..b81bb30 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/sentinel.ts @@ -0,0 +1,26 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +/** + * 应用集群流量详情 + */ +export const metricByAppApi = (params?: object): Promise> => { + // return rq.get>("${BSMS_URL}/sentinel/es/metric/app", params); + return rq.get>("${GATEWAY_URL}/${SERVICE_ID}/sentinel/metric/app", { params }); +}; + +/** + * 资源列表 + */ +export const resourcesApi = (params?: object): Promise> => { + // return rq.get>("${BSMS_URL}/sentinel/es/resources", params); + return rq.get>("${GATEWAY_URL}/${SERVICE_ID}/sentinel/resources", { params }); +}; + + +/** + * 流量直方图 + */ +export const metricLineApi = (params?: object): Promise> => { + return rq.get>("/sentinel/metric/line", { params }); +}; \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/weather.ts b/blossom-editor/src/renderer/src/api/weather.ts new file mode 100644 index 0000000..4ba8989 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/weather.ts @@ -0,0 +1,6 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +export const getAll = (params?: object): Promise> => { + return rq.get>("/weather", {params}); +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/api/web.ts b/blossom-editor/src/renderer/src/api/web.ts new file mode 100644 index 0000000..42fff61 --- /dev/null +++ b/blossom-editor/src/renderer/src/api/web.ts @@ -0,0 +1,14 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +export const webAllApi = (params?: object): Promise> => { + return rq.get>("/web/list", { params }); +} + +export const webSaveApi = (data?: object): Promise> => { + return rq.post>("/web/save", data); +} + +export const webDelApi = (data?: object): Promise> => { + return rq.post>("/web/del", data); +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/constants/env.ts b/blossom-editor/src/renderer/src/assets/constants/env.ts new file mode 100644 index 0000000..ef71117 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/constants/env.ts @@ -0,0 +1,17 @@ +export const LOC = { + envName: 'LOC', + bsmsUrl: 'http://localhost:19009', + gatewayUrl: 'http://localhost:19001' +} + +export const DEV = { + envName: 'DEV', + bsmsUrl: 'http://192.168.31.6:19009', + gatewayUrl: 'http://192.168.31.6:19001' +} + +export const PRD = { + envName: 'PRD', + bsmsUrl: 'https://www.wangyunf.com/bsms', + gatewayUrl: 'https://www.wangyunf.com/gateway' +} diff --git a/blossom-editor/src/renderer/src/assets/constants/menu.ts b/blossom-editor/src/renderer/src/assets/constants/menu.ts new file mode 100644 index 0000000..90830e3 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/constants/menu.ts @@ -0,0 +1,61 @@ +/** + * 菜单对象 + */ +export class AsideMenu { + // 路径 + routeName: string; + // 图标 + icon: string; + // 菜单名称 + menuName: string; + // 标签 + tags: string[]; + // 自菜单 + children: AsideMenu[]; + + constructor(routeName: string, icon: string, menuName: string, tags: string[], children: AsideMenu[]) { + this.routeName = routeName; + this.icon = icon; + this.menuName = menuName; + this.tags = tags; + this.children = children; + } +} + +/** + * 初始化静态菜单 + */ +export const asideMenuConstant: AsideMenu[] = + [ + new AsideMenu("home", "iconbl bl-home", "首页", [], []), + new AsideMenu("cluster", "iconbl bl-server", "集群管理", [], + [ + new AsideMenu("indexMonitorTree", "iconbl bl-node", "集群节点树", [], []), + new AsideMenu("indexMonitorNodes", "iconbl bl-node-2", "集群节点", [], []), + new AsideMenu("indexJvm", "iconbl bl-jvm", "节点监控", [], []), + new AsideMenu("indexMonitorLog", "iconbl bl-elasticsearch", "日志监控", [], []), + new AsideMenu("indexSentinel", "iconbl bl-flow", "流量监控", [], []), + new AsideMenu("indexTracker", "iconbl bl-trace", "链路追踪", [], []), + new AsideMenu("indexAppRelation", "iconbl bl-node-4", "应用拓扑", [], []), + new AsideMenu("indexRedis", "iconbl bl-redis2", "缓存监控", [], []), + new AsideMenu("indexGw", "iconbl bl-route", "路由管理", [], []), + new AsideMenu("apiDoc", "iconbl bl-interface", "接口文档", [], []), + ] + ), + // new AsideMenu("rbac", "iconbl bl-userkey", "RBAC", [], [ + // new AsideMenu("indexUser", "iconbl bl-user3", "用户管理", [], []), + // new AsideMenu("indexRole", "iconbl bl-roles", "角色管理", [], []), + // new AsideMenu("indexPerm", "iconbl bl-key1", "权限管理", [], []), + // ] + // ), + new AsideMenu("home9", "iconbl bl-bucket", "其他模块", [], [ + // new AsideMenu("home92", "iconbl bl-monitor1", "中间件监控", []), + new AsideMenu("test", "iconbl bl-bug", "功能测试用", [], []), + new AsideMenu("testMarkdownTuiEditor", "iconbl bl-bug", "Tui-Editor", [], []), + ] + ), + // new AsideMenu("Succubus", "iconbl bl-devil", "影视剧集", [ + // new AsideMenu("actorList", "iconbl bl-user-level", "演员列表", []), + // new AsideMenu("actorVideoSearch", "iconbl bl-user-search", "搜索", []), + // ]), + ] \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/constants/system.ts b/blossom-editor/src/renderer/src/assets/constants/system.ts new file mode 100644 index 0000000..567fbef --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/constants/system.ts @@ -0,0 +1,13 @@ +const blossom: any = { + /** + * 基础配置 + */ + SYS: { + NAME: 'Blossom', + FULL_NAME: 'BLOSSOM-EDITOR', + VERSION: 'v1.0.0', + DATETIME: '2023-07-27' + }, +} + +export default blossom \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/css/main.css b/blossom-editor/src/renderer/src/assets/css/main.css new file mode 100644 index 0000000..bb6672e --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/css/main.css @@ -0,0 +1,74 @@ +html, +body { + background-color: var(--xz-html-color); + overflow: hidden; +} + +* { + font-family: 'Jetbrains Mono', 'Consolas', 'Menlo', 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + box-sizing: border-box; +} + +img { + pointer-events: none; +} + +/* + 用于 iconfont symbol 方式 icon 的基础样式 + {@link @renderer/assets/iconfont/blossom} + {@link @renderer/assets/iconfont/tech} +*/ +.icon { + width: 1em; + height: 1em; + vertical-align: -0.15em; + fill: currentColor; + overflow: hidden; +} + +/** ================================== 滚动条 ================================== */ +/* 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸 */ +::-webkit-scrollbar { + /* 滚动条宽度 */ + width: 7px; + /* 滚动条高度 */ + height: 7px; + /* 滚动条里槽的背景色 */ + /* background-color: var(--el-color-info-light-9); */ + /* background-color: #eeeeee; */ +} + +::-webkit-scrollbar:hover { + width: 7px; + height: 7px; +} + +/* 定义滑块 内阴影+圆角 */ +::-webkit-scrollbar-thumb { + border-radius: 3px; + /* 滑块的背景色 */ + /* background-color: #cdcdcd; */ + background-color: var(--el-color-primary-light-7); + transition: 0.3s; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #ababab; +} + +/* 解决滚动条右下角出现白色方块 */ +::-webkit-scrollbar-corner { + background: transparent; + /* background-color: var(--el-color-info-light-9); */ + ; +} + + +::selection { + background: var(--el-color-primary-light-7) !important; +} + +/* ::selection { + background: #d3d3d3; + color: #292929; +} */ \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/css/theme.css b/blossom-editor/src/renderer/src/assets/css/theme.css new file mode 100644 index 0000000..03d7290 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/css/theme.css @@ -0,0 +1,293 @@ +:root { + + /* 数字越大, 颜色越淡 */ + --el-color-primary: #ad8cf2; + --el-color-primary-dark-2: rgba(173, 140, 242, 0.8); + --el-color-primary-light-1: rgba(173, 140, 242, 0.9); + --el-color-primary-light-2: rgba(173, 140, 242, 0.8); + --el-color-primary-light-3: rgba(173, 140, 242, 0.7); + --el-color-primary-light-4: rgba(173, 140, 242, 0.6); + --el-color-primary-light-5: rgba(173, 140, 242, 0.5); + --el-color-primary-light-6: rgba(173, 140, 242, 0.4); + --el-color-primary-light-7: rgba(173, 140, 242, 0.3); + --el-color-primary-light-8: rgba(173, 140, 242, 0.2); + --el-color-primary-light-9: rgba(173, 140, 242, 0.1); + + --el-color-success: #67c23a; + --el-color-success-light-3: #95d475; + --el-color-success-light-5: #b3e19d; + --el-color-success-light-7: #d1edc4; + --el-color-success-light-8: #e1f3d8; + --el-color-success-light-9: #f0f9eb; + --el-color-success-dark-2: #529b2e; + + --el-color-warning: #e6a23c; + --el-color-warning-light-3: #eebe77; + --el-color-warning-light-5: #f3d19e; + --el-color-warning-light-7: #f8e3c5; + --el-color-warning-light-8: #faecd8; + --el-color-warning-light-9: #fdf6ec; + --el-color-warning-dark-2: #b88230; + + --el-color-danger: #e35959; + --el-color-danger-light-3: #f89898; + --el-color-danger-light-5: #fab6b6; + --el-color-danger-light-7: #fcd3d3; + --el-color-danger-light-8: #fde2e2; + --el-color-danger-light-9: #fef0f0; + --el-color-danger-dark-2: #c45656; + + --el-color-error: #f56c6c; + --el-color-error-light-3: #f89898; + --el-color-error-light-5: #fab6b6; + --el-color-error-light-7: #fcd3d3; + --el-color-error-light-8: #fde2e2; + --el-color-error-light-9: #fef0f0; + --el-color-error-dark-2: #c45656; + + --el-color-info: #a4a8b0; + --el-color-info-light-3: #b1b3b8; + --el-color-info-light-5: #c8c9cc; + --el-color-info-light-7: #dedfe0; + --el-color-info-light-8: #e9e9eb; + --el-color-info-light-9: #f4f4f5; + + /* ======================================================= 文本颜色 =======================================================*/ + /* --el-text-color-primary: #303133; */ + --el-text-color-primary: #606266; + --el-text-color-regular: #606266; + --el-text-color-secondary: #909399; + --el-text-color-placeholder: #a8abb2; + --el-text-color-disabled: #c0c4cc; + + /* ======================================================= 背景颜色 =======================================================*/ + --el-bg-color: #f5f7fa; + + /* ======================================================= 边框颜色 =======================================================*/ + + /* 菜单文本的颜色 */ + --el-menu-text-color: #86888e; + --el-menu-active-color: var(--el-color-primary); + + --el-font-size-base: 12px; + + --el-menu-item-height: 40px; + --el-mask-color: rgba(255, 255, 255, 0.95); +} + +html.dark { + /* 数字越大, 颜色越淡 */ + --el-color-primary: #899911; + --el-color-primary-dark-2: rgba(165, 184, 20, 1); + --el-color-primary-light-1: rgba(165, 184, 20, 0.9); + --el-color-primary-light-2: rgba(165, 184, 20, 0.8); + --el-color-primary-light-3: rgba(165, 184, 20, 0.7); + --el-color-primary-light-5: rgba(165, 184, 20, 0.5); + --el-color-primary-light-6: rgba(165, 184, 20, 0.4); + --el-color-primary-light-7: rgba(165, 184, 20, 0.3); + --el-color-primary-light-8: rgba(165, 184, 20, 0.2); + --el-color-primary-light-9: rgba(165, 184, 20, 0.2); + + --el-color-danger: rgb(215, 68, 68); + --el-color-danger-light-3: rgba(215, 68, 68, 0.7); + --el-color-danger-light-5: rgba(215, 68, 68, 0.5); + --el-color-danger-light-7: rgba(215, 68, 68, 0.3); + --el-color-danger-light-8: rgba(215, 68, 68, 0.2); + --el-color-danger-light-9: rgba(215, 68, 68, 0.1); + --el-color-danger-dark-2: #8111118a; + + --el-color-info: #64666b; + /* ======================================================= 文本颜色 =======================================================*/ + --el-text-color-primary: #909399; + --el-text-color-regular: #a8a8a8; + --el-text-color-secondary: #A3A6AD; + --el-text-color-placeholder: #8D9095; + --el-text-color-disabled: #6C6E72; + + /* ======================================================= 背景颜色 =======================================================*/ + --el-bg-color: #262727; + + /* ======================================================= 边框颜色 =======================================================*/ + --el-border-color: #424342; + + /* 菜单文本的颜色 */ + --el-menu-text-color: #86888e; + --el-menu-active-color: #ffffff; + + /* ======================================================= 文本框样式 =======================================================*/ + --el-input-bg-color: #000000; + --el-input-border-color: #373737; + + /* 这招的颜色 */ + --el-mask-color: rgb(18, 18, 18, 0.95); + +} + +/* ================================= 自定义颜色 ================================== */ + +:root { + --xz-html-color: #ffffff; + /* 背景色 */ + --xz-bg-color: #f1f1f1; + + --xz-text-shadow: 2px 3px 4px rgba(107, 104, 104, 0.5); + + --xz-box-shadow: 3px 3px 10px 3px rgba(107, 104, 104, 0.3); + --xz-box-shadow-dialog: 3px 3px 10px 2px rgba(58, 47, 47, 0.5); + + /* ================================= switch 自定义颜色 ================================== */ + --xz-switch-action-color: #ffffff; + --xz-switch-action-color-checked: #ffffff; + --xz-switch-text-color: #ffffff; + --xz-switch-text-color-checked: #ffffff; + + /* ================================= iconbl 图标 ================================== */ + --xz-iconbl-text-shadow: 2px 3px 5px rgba(107, 104, 104, 0.4); + + --xz-subject-color1: #040EFF; +} + +html.dark { + --xz-html-color: #1e1e1e; + --xz-bg-color: #333434; + + --xz-text-shadow: 2px 3px 5px rgb(0, 0, 0); + + --xz-box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.3); + --xz-box-shadow-dialog: 3px 3px 10px 2px rgba(0, 0, 0, 1); + + /* ================================= switch 自定义颜色 ================================== */ + --xz-switch-action-color: rgb(58, 58, 58); + --xz-switch-action-color-checked: #ffffff; + --xz-switch-text-color: var(--el-color-primary); + --xz-switch-text-color-checked: #ffffff; + + /* ================================= iconbl 图标 ================================== */ + --xz-iconbl-text-shadow: 2px 3px 5px rgb(0, 0, 0); +} + + +:root { + /* */ + --bl-editor-color: #707070; + --bl-editor-bg-color: #FFFFFF; + + /* gutter */ + --bl-editor-gutters-bg-color: #f5f5f5; + --bl-editor-gutters-border-color: #DDDDDD00; + --bl-editor-active-line-gutter-bg-color: #f7f3fe; + + /* 各类关键字颜色 */ + --bl-editor-cc-color: #219; + --bl-editor-cm-color: #b6b6b6; + --bl-editor-cb-color: #ce5d00; + --bl-editor-cd-color: #689757; + --bl-editor-ce-color: #47ade5; + --bl-editor-ci-color: #ba7300; + --bl-editor-cg-color: #9876aa; + + --bl-editor-c5-color: #7843E9; + --bl-editor-c7-color: #7843E9; + --bl-editor-c7-bg-color: #00000000; + /* ============================================================= */ + + /* 预览的字体颜色 */ + --bl-preview-color: #2b2b2b; + --bl-preview-bg-color: #FFFFFF; + --bl-preview-border-radius: 4px; + + /* 目录 */ + --bl-preview-toc-border-color: #e0e0e0; + + /* 标题 */ + --bl-preview-h1-bg-color: #eaeaea; + --bl-preview-h1-box-shadow: 3px 3px 5px 1px #a8a8a8; + --bl-preview-h1-text-shadow: 3px 3px 3px rgb(148, 148, 148); + + /* 表格 */ + --bl-preview-table-border-color: #939393; + --bl-preview-table-thead-bg-color: #2b2b2b; + + /* 引用 */ + --bl-preview-blockquote-color: #7d7d7d; + --bl-preview-blockquote-bg-color: #f0f0f0; + --bl-preview-blockquote-border-color: #bebebe; + --bl-preview-blockquote-bg-green: #edf8db; + --bl-preview-blockquote-border-green: #bed609; + --bl-preview-blockquote-bg-yellow: #faf0d5; + --bl-preview-blockquote-border-yellow: #efc75e; + --bl-preview-blockquote-bg-red: #fbe6e9; + --bl-preview-blockquote-border-red: #ff9090; + --bl-preview-blockquote-bg-blue: #dfeefd; + --bl-preview-blockquote-border-blue: #81bbf8; + --bl-preview-blockquote-bg-purple: #ece4fb; + --bl-preview-blockquote-border-purple: #ba9bf2; + --bl-preview-blockquote-bg-black: rgba(0, 0, 0, 0.7); + --bl-preview-blockquote-border-black: #000000; + + /* 图片 */ + --bl-preview-img-box-shadow: 2px 2px 10px 3px #c6c6c6; + + /* 单行代码块 */ + --bl-preview-code-bg-color: #eaeaea; + /* 多行代码块 */ + --bl-preview-pre-bg-color: #2b2b2b; +} + +html.dark { + --bl-editor-color: #acacac; + --bl-editor-bg-color: #1e1e1e; + + --bl-editor-gutters-bg-color: #2d2d2d; + --bl-editor-gutters-border-color: #42434200; + --bl-editor-active-line-gutter-bg-color: #393d1c; + + --bl-editor-cc-color: #7d8a17; + --bl-editor-cm-color: #606366; + --bl-editor-cb-color: #cc7832; + --bl-editor-cd-color: #689757; + --bl-editor-ce-color: #69864b; + --bl-editor-ci-color: #ffc66d; + --bl-editor-cg-color: #9876aa; + + --bl-editor-c5-color: #7d8a17; + --bl-editor-c7-color: #7d8a17; + --bl-editor-c7-bg-color: #00000000; + /* ============================================================= */ + + --bl-preview-color: #cdcdcd; + + --bl-preview-color: #9d9d9d; + --bl-preview-bg-color: #1e1e1e; + --bl-preview-border-radius: 4px; + + --bl-preview-toc-border-color: #525252; + + --bl-preview-h1-bg-color: #545454; + --bl-preview-h1-box-shadow: 3px 3px 5px 1px #000000; + --bl-preview-h1-text-shadow: 3px 3px 3px #000000; + + --bl-preview-table-border-color: #525252; + --bl-preview-table-thead-bg-color: #454545; + + --bl-preview-blockquote-color: #b9b9b9; + --bl-preview-blockquote-bg-color: #3a3a3a; + --bl-preview-blockquote-border-color: #6e6e6e; + --bl-preview-blockquote-bg-green: #385b00; + --bl-preview-blockquote-border-green: #7c8c00; + --bl-preview-blockquote-bg-yellow: #644900; + --bl-preview-blockquote-border-yellow: #956b00; + --bl-preview-blockquote-bg-red: #4f000c; + --bl-preview-blockquote-border-red: #9a0000; + --bl-preview-blockquote-bg-blue: #00366c; + --bl-preview-blockquote-border-blue: #004fa3; + --bl-preview-blockquote-bg-purple: #240065; + --bl-preview-blockquote-border-purple: #4600c8; + --bl-preview-blockquote-bg-black: #181818; + --bl-preview-blockquote-border-black: #000000; + + --bl-preview-img-box-shadow: 2px 2px 10px 3px #000000; + + --bl-preview-code-bg-color: #414141; + --bl-preview-pre-bg-color: #131313; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Bold.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Bold.woff2 new file mode 100644 index 0000000..4917f43 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Bold.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-ExtraLight.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-ExtraLight.woff2 new file mode 100644 index 0000000..b97239f Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-ExtraLight.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Light.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Light.woff2 new file mode 100644 index 0000000..6538498 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Light.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Medium.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Medium.woff2 new file mode 100644 index 0000000..669d04c Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Medium.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Regular.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 0000000..40da427 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Regular.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-SemiBold.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-SemiBold.woff2 new file mode 100644 index 0000000..5ead7b0 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-SemiBold.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Thin.woff2 b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Thin.woff2 new file mode 100644 index 0000000..17270e4 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/fonts/JetBrainsMono-Thin.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/fonts/config.scss b/blossom-editor/src/renderer/src/assets/fonts/config.scss new file mode 100644 index 0000000..bfecc0d --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/fonts/config.scss @@ -0,0 +1,42 @@ + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-Bold.woff2'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-Medium.woff2'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-Regular.woff2'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-Light.woff2'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-ExtraLight.woff2'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Jetbrains Mono'; + src: url('@renderer/assets/fonts/JetBrainsMono-Thin.woff2'); + font-weight: 100; + font-style: normal; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.css b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.css new file mode 100644 index 0000000..42b684f --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.css @@ -0,0 +1,595 @@ +@font-face { + font-family: "iconbl"; /* Project id 4118609 */ + src: url('iconfont.woff2?t=1690552792272') format('woff2'), + url('iconfont.woff?t=1690552792272') format('woff'), + url('iconfont.ttf?t=1690552792272') format('truetype'); +} + +.iconbl { + font-family: "iconbl" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bl-zanwushuju:before { + content: "\e618"; +} + +.bl-hengxiangzhuzhuangtu:before { + content: "\e6d0"; +} + +.bl-shuipingduoxiliezhuzhuangtu:before { + content: "\e628"; +} + +.bl-construction:before { + content: "\e63d"; +} + +.bl-correlation-line:before { + content: "\ea25"; +} + +.bl-a-relation1-line:before { + content: "\ea28"; +} + +.bl-a-relation2-line:before { + content: "\ea2b"; +} + +.bl-a-relation3-line:before { + content: "\ea2c"; +} + +.bl-a-pageadd-line:before { + content: "\ea22"; +} + +.bl-a-pagelevel-line:before { + content: "\ea23"; +} + +.bl-a-templatelist2-line:before { + content: "\ea45"; +} + +.bl-a-colorpalette-line:before { + content: "\ea1b"; +} + +.bl-date-line:before { + content: "\ea24"; +} + +.bl-a-packageadd-line:before { + content: "\ea26"; +} + +.bl-a-templateadd-line:before { + content: "\ea44"; +} + +.bl-a-addline-line:before { + content: "\e9d4"; +} + +.bl-a-leftdirection-line:before { + content: "\ea17"; +} + +.bl-a-leftdirection-fill:before { + content: "\ea18"; +} + +.bl-record-line:before { + content: "\ea19"; +} + +.bl-send-line:before { + content: "\ea21"; +} + +.bl-a-IDcardfront-line:before { + content: "\ea14"; +} + +.bl-note-line:before { + content: "\ea15"; +} + +.bl-reader-line:before { + content: "\ea16"; +} + +.bl-tiaoxingma:before { + content: "\e812"; +} + +.bl-a-barcode-line:before { + content: "\e9d5"; +} + +.bl-folding-line:before { + content: "\ea13"; +} + +.bl-refresh-smile:before { + content: "\e64f"; +} + +.bl-sendmail-line:before { + content: "\ea20"; +} + +.bl-a-historicalrecord-line:before { + content: "\ea12"; +} + +.bl-books-line:before { + content: "\ea10"; +} + +.bl-a-cloudstorage-line:before { + content: "\ea0f"; +} + +.bl-sd-line:before { + content: "\ea1a"; +} + +.bl-a-texteditorstickup-line:before { + content: "\ea43"; +} + +.bl-calendar-line:before { + content: "\e9db"; +} + +.bl-a-folderset-line:before { + content: "\ea0e"; +} + +.bl-blossom-logo-dark:before { + content: "\e601"; +} + +.bl-blossom-logo-grey:before { + content: "\e602"; +} + +.bl-blossom-logo:before { + content: "\62"; +} + +.bl-delete-line:before { + content: "\e9e1"; +} + +.bl-a-online-line:before { + content: "\ea0b"; +} + +.bl-problem-line:before { + content: "\ea0c"; +} + +.bl-a-radiochoose-line:before { + content: "\ea0d"; +} + +.bl-a-rightto-line:before { + content: "\ea11"; +} + +.bl-switch-line:before { + content: "\ea40"; +} + +.bl-a-underline-line:before { + content: "\ea41"; +} + +.bl-a-doubleunderline-line:before { + content: "\ea09"; +} + +.bl-a-doubleonline-line:before { + content: "\ea0a"; +} + +.bl-a-texteditorback-line:before { + content: "\ea3a"; +} + +.bl-a-texteditorsuperscript-line:before { + content: "\ea3b"; +} + +.bl-a-texteditorsubscript-line:before { + content: "\ea3c"; +} + +.bl-a-texteditorforward-line:before { + content: "\ea3e"; +} + +.bl-a-underbox-line:before { + content: "\ea3f"; +} + +.bl-a-icon_jiandaojianqie:before { + content: "\e650"; +} + +.bl-jianpan-xianxing:before { + content: "\e8ce"; +} + +.bl-admonish-line:before { + content: "\e9d2"; +} + +.bl-a-eyeclose-line:before { + content: "\ea01"; +} + +.bl-a-fileadd-fill:before { + content: "\ea02"; +} + +.bl-a-listview-line:before { + content: "\ea08"; +} + +.bl-login-circle-line:before { + content: "\e70e"; +} + +.bl-logout-circle-line:before { + content: "\e71a"; +} + +.bl-a-Securitypermissions-line:before { + content: "\ea1e"; +} + +.bl-a-servercloud-line:before { + content: "\ea1f"; +} + +.bl-user-line:before { + content: "\ea3d"; +} + +.bl-effect-line:before { + content: "\e9e3"; +} + +.bl-expansion-line:before { + content: "\e9ec"; +} + +.bl-eye-line:before { + content: "\e9ee"; +} + +.bl-a-Informationquestions-line:before { + content: "\e9ff"; +} + +.bl-like-line:before { + content: "\e9fe"; +} + +.bl-read-line:before { + content: "\ea07"; +} + +.bl-search-line:before { + content: "\ea1c"; +} + +.bl-a-texteditorpastetext-line:before { + content: "\ea37"; +} + +.bl-transcript-line:before { + content: "\ea38"; +} + +.bl-transcript-fill:before { + content: "\ea39"; +} + +.bl-a-computerend-line:before { + content: "\e9fd"; +} + +.bl-cloud-fill:before { + content: "\e9e4"; +} + +.bl-a-lowerrightpage-line:before { + content: "\e9f9"; +} + +.bl-a-lowerrightpage-fill:before { + content: "\e9fa"; +} + +.bl-model-fill:before { + content: "\e9fb"; +} + +.bl-model-line:before { + content: "\e9fc"; +} + +.bl-a-filetext-line:before { + content: "\e9f7"; +} + +.bl-a-folderon-line:before { + content: "\e9f8"; +} + +.bl-a-linkspread-line:before { + content: "\e9f6"; +} + +.bl-planet-line:before { + content: "\ea05"; +} + +.bl-a-qrcode1-line:before { + content: "\ea06"; +} + +.bl-a-filechoose-line:before { + content: "\e9f1"; +} + +.bl-a-filehistory-line:before { + content: "\e9f2"; +} + +.bl-a-filelink-line:before { + content: "\e9f3"; +} + +.bl-a-fileswitch-line13:before { + content: "\e9f4"; +} + +.bl-a-fileseal-line:before { + content: "\e9f5"; +} + +.bl-a-clock3-line:before { + content: "\e9e5"; +} + +.bl-a-fileadd-line:before { + content: "\e9e6"; +} + +.bl-a-filecycle-line:before { + content: "\e9e7"; +} + +.bl-a-fileedit-line:before { + content: "\e9e8"; +} + +.bl-a-filemedal-line:before { + content: "\e9e9"; +} + +.bl-a-fileprohibit-line:before { + content: "\e9ea"; +} + +.bl-a-filesend-line:before { + content: "\e9eb"; +} + +.bl-a-fileminus-line:before { + content: "\e9ef"; +} + +.bl-a-filestar-line:before { + content: "\e9f0"; +} + +.bl-pen-line:before { + content: "\ea03"; +} + +.bl-sendmail-fill:before { + content: "\ea1d"; +} + +.bl-star-fill:before { + content: "\ea29"; +} + +.bl-star-line:before { + content: "\ea2a"; +} + +.bl-a-switchoff-line:before { + content: "\ea35"; +} + +.bl-a-switchon-line:before { + content: "\ea36"; +} + +.bl-a-rightsmallline-line:before { + content: "\ea99"; +} + +.bl-add-line:before { + content: "\e9d3"; +} + +.bl-a-cloudrefresh-line:before { + content: "\e9e2"; +} + +.bl-tally-line:before { + content: "\ea31"; +} + +.bl-a-texteditorsave-line:before { + content: "\ea34"; +} + +.bl-adhibition-line:before { + content: "\e9d1"; +} + +.bl-wifi-line:before { + content: "\ea42"; +} + +.bl-a-clouddownload-line:before { + content: "\e9dc"; +} + +.bl-a-cloudchoose-line:before { + content: "\e9dd"; +} + +.bl-cloud-line:before { + content: "\e9de"; +} + +.bl-a-cloudupload-line:before { + content: "\e9e0"; +} + +.bl-task-line:before { + content: "\e675"; +} + +.bl-layout-column-line:before { + content: "\e638"; +} + +.bl-bold:before { + content: "\e678"; +} + +.bl-italic:before { + content: "\e679"; +} + +.bl-heading:before { + content: "\e67a"; +} + +.bl-indent-increase:before { + content: "\e67b"; +} + +.bl-double-quotes-r:before { + content: "\e67c"; +} + +.bl-list-unordered:before { + content: "\e67d"; +} + +.bl-link-m:before { + content: "\e67e"; +} + +.bl-indent-decrease:before { + content: "\e67f"; +} + +.bl-separator:before { + content: "\e680"; +} + +.bl-list-ordered:before { + content: "\e681"; +} + +.bl-code-view:before { + content: "\e682"; +} + +.bl-strikethrough:before { + content: "\e683"; +} + +.bl-single-quotes-r:before { + content: "\e684"; +} + +.bl-table-:before { + content: "\e685"; +} + +.bl-image--line:before { + content: "\e6d3"; +} + +.bl-apps-line:before { + content: "\e6ed"; +} + +.bl-refresh-line:before { + content: "\e713"; +} + +.bl-copy-line:before { + content: "\e9df"; +} + +.bl-bug-line:before { + content: "\e9da"; +} + +.bl-shrink-line:before { + content: "\ea27"; +} + +.bl-subtract-line:before { + content: "\ea30"; +} + +.bl-box-line:before { + content: "\e9d9"; +} + +.bl-a-closeline-line:before { + content: "\e2ec"; +} + +.bl-a-home1-line:before { + content: "\e9ed"; +} + +.bl-a-package2-line:before { + content: "\ea00"; +} + +.bl-picture-line:before { + content: "\ea04"; +} + +.bl-a-texteditorhighlightcolor-line:before { + content: "\ea32"; +} + +.bl-tier-line:before { + content: "\ea33"; +} + diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.js b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.js new file mode 100644 index 0000000..25221ff --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_4118609='',function(h){var a=(a=document.getElementsByTagName("script"))[a.length-1],l=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var v,m,z,i,o,t=function(a,l){l.parentNode.insertBefore(a,l)};if(l&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}v=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_4118609,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?t(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(v,0):(m=function(){document.removeEventListener("DOMContentLoaded",m,!1),v()},document.addEventListener("DOMContentLoaded",m,!1)):document.attachEvent&&(z=v,i=h.document,o=!1,e(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,c())})}function c(){o||(o=!0,z())}function e(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(e,50)}c()}}(window); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.json b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.json new file mode 100644 index 0000000..ac14ceb --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.json @@ -0,0 +1,1024 @@ +{ + "id": "4118609", + "name": "blossom", + "font_family": "iconbl", + "css_prefix_text": "bl-", + "description": "", + "glyphs": [ + { + "icon_id": "4735972", + "name": "暂无数据", + "font_class": "zanwushuju", + "unicode": "e618", + "unicode_decimal": 58904 + }, + { + "icon_id": "9874495", + "name": "KHCFDC_横向柱状图", + "font_class": "hengxiangzhuzhuangtu", + "unicode": "e6d0", + "unicode_decimal": 59088 + }, + { + "icon_id": "10451698", + "name": "水平多系列柱状图", + "font_class": "shuipingduoxiliezhuzhuangtu", + "unicode": "e628", + "unicode_decimal": 58920 + }, + { + "icon_id": "12824105", + "name": "施工", + "font_class": "construction", + "unicode": "e63d", + "unicode_decimal": 58941 + }, + { + "icon_id": "24341448", + "name": "correlation-line", + "font_class": "correlation-line", + "unicode": "ea25", + "unicode_decimal": 59941 + }, + { + "icon_id": "24342175", + "name": "relation 1-line", + "font_class": "a-relation1-line", + "unicode": "ea28", + "unicode_decimal": 59944 + }, + { + "icon_id": "24342181", + "name": "relation 2-line", + "font_class": "a-relation2-line", + "unicode": "ea2b", + "unicode_decimal": 59947 + }, + { + "icon_id": "24342182", + "name": "relation 3-line", + "font_class": "a-relation3-line", + "unicode": "ea2c", + "unicode_decimal": 59948 + }, + { + "icon_id": "24342071", + "name": "page add-line", + "font_class": "a-pageadd-line", + "unicode": "ea22", + "unicode_decimal": 59938 + }, + { + "icon_id": "24342073", + "name": "page level-line", + "font_class": "a-pagelevel-line", + "unicode": "ea23", + "unicode_decimal": 59939 + }, + { + "icon_id": "24342538", + "name": "template list 2-line", + "font_class": "a-templatelist2-line", + "unicode": "ea45", + "unicode_decimal": 59973 + }, + { + "icon_id": "24341395", + "name": "color palette-line", + "font_class": "a-colorpalette-line", + "unicode": "ea1b", + "unicode_decimal": 59931 + }, + { + "icon_id": "24341474", + "name": "date-line", + "font_class": "date-line", + "unicode": "ea24", + "unicode_decimal": 59940 + }, + { + "icon_id": "24342066", + "name": "package add-line", + "font_class": "a-packageadd-line", + "unicode": "ea26", + "unicode_decimal": 59942 + }, + { + "icon_id": "24342556", + "name": "template add-line", + "font_class": "a-templateadd-line", + "unicode": "ea44", + "unicode_decimal": 59972 + }, + { + "icon_id": "24341078", + "name": "add line-line", + "font_class": "a-addline-line", + "unicode": "e9d4", + "unicode_decimal": 59860 + }, + { + "icon_id": "24341891", + "name": "left direction-line", + "font_class": "a-leftdirection-line", + "unicode": "ea17", + "unicode_decimal": 59927 + }, + { + "icon_id": "24341919", + "name": "left direction-fill", + "font_class": "a-leftdirection-fill", + "unicode": "ea18", + "unicode_decimal": 59928 + }, + { + "icon_id": "24342162", + "name": "record-line", + "font_class": "record-line", + "unicode": "ea19", + "unicode_decimal": 59929 + }, + { + "icon_id": "24342361", + "name": "send-line", + "font_class": "send-line", + "unicode": "ea21", + "unicode_decimal": 59937 + }, + { + "icon_id": "24341787", + "name": "ID card front-line", + "font_class": "a-IDcardfront-line", + "unicode": "ea14", + "unicode_decimal": 59924 + }, + { + "icon_id": "24342020", + "name": "note-line", + "font_class": "note-line", + "unicode": "ea15", + "unicode_decimal": 59925 + }, + { + "icon_id": "24342157", + "name": "reader-line", + "font_class": "reader-line", + "unicode": "ea16", + "unicode_decimal": 59926 + }, + { + "icon_id": "12568844", + "name": "条形码", + "font_class": "tiaoxingma", + "unicode": "e812", + "unicode_decimal": 59410 + }, + { + "icon_id": "24341130", + "name": "bar code-line", + "font_class": "a-barcode-line", + "unicode": "e9d5", + "unicode_decimal": 59861 + }, + { + "icon_id": "24341704", + "name": "folding-line", + "font_class": "folding-line", + "unicode": "ea13", + "unicode_decimal": 59923 + }, + { + "icon_id": "777840", + "name": "刷新", + "font_class": "refresh-smile", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "24342370", + "name": "sendmail-line", + "font_class": "sendmail-line", + "unicode": "ea20", + "unicode_decimal": 59936 + }, + { + "icon_id": "24341768", + "name": "historical record-line", + "font_class": "a-historicalrecord-line", + "unicode": "ea12", + "unicode_decimal": 59922 + }, + { + "icon_id": "24341229", + "name": "books-line", + "font_class": "books-line", + "unicode": "ea10", + "unicode_decimal": 59920 + }, + { + "icon_id": "24341374", + "name": "cloud storage-line", + "font_class": "a-cloudstorage-line", + "unicode": "ea0f", + "unicode_decimal": 59919 + }, + { + "icon_id": "24342332", + "name": "sd-line", + "font_class": "sd-line", + "unicode": "ea1a", + "unicode_decimal": 59930 + }, + { + "icon_id": "24342577", + "name": "text editor stickup-line", + "font_class": "a-texteditorstickup-line", + "unicode": "ea43", + "unicode_decimal": 59971 + }, + { + "icon_id": "24341278", + "name": "calendar-line", + "font_class": "calendar-line", + "unicode": "e9db", + "unicode_decimal": 59867 + }, + { + "icon_id": "24341670", + "name": "folder set-line", + "font_class": "a-folderset-line", + "unicode": "ea0e", + "unicode_decimal": 59918 + }, + { + "icon_id": "36393612", + "name": "blossom-logo-dark", + "font_class": "blossom-logo-dark", + "unicode": "e601", + "unicode_decimal": 58881 + }, + { + "icon_id": "36393841", + "name": "blossom-logo-grey", + "font_class": "blossom-logo-grey", + "unicode": "e602", + "unicode_decimal": 58882 + }, + { + "icon_id": "36393559", + "name": "blossom-logo", + "font_class": "blossom-logo", + "unicode": "62", + "unicode_decimal": 98 + }, + { + "icon_id": "24341467", + "name": "delete-line", + "font_class": "delete-line", + "unicode": "e9e1", + "unicode_decimal": 59873 + }, + { + "icon_id": "24342038", + "name": "on line-line", + "font_class": "a-online-line", + "unicode": "ea0b", + "unicode_decimal": 59915 + }, + { + "icon_id": "24342123", + "name": "problem-line", + "font_class": "problem-line", + "unicode": "ea0c", + "unicode_decimal": 59916 + }, + { + "icon_id": "24342130", + "name": "radio choose-line", + "font_class": "a-radiochoose-line", + "unicode": "ea0d", + "unicode_decimal": 59917 + }, + { + "icon_id": "24342232", + "name": "right to -line", + "font_class": "a-rightto-line", + "unicode": "ea11", + "unicode_decimal": 59921 + }, + { + "icon_id": "24342546", + "name": "switch-line", + "font_class": "switch-line", + "unicode": "ea40", + "unicode_decimal": 59968 + }, + { + "icon_id": "24342612", + "name": "under line-line", + "font_class": "a-underline-line", + "unicode": "ea41", + "unicode_decimal": 59969 + }, + { + "icon_id": "24341524", + "name": "double under line-line", + "font_class": "a-doubleunderline-line", + "unicode": "ea09", + "unicode_decimal": 59913 + }, + { + "icon_id": "24341526", + "name": "double on line-line", + "font_class": "a-doubleonline-line", + "unicode": "ea0a", + "unicode_decimal": 59914 + }, + { + "icon_id": "24342547", + "name": "text editor back-line", + "font_class": "a-texteditorback-line", + "unicode": "ea3a", + "unicode_decimal": 59962 + }, + { + "icon_id": "24342564", + "name": "text editor superscript-line", + "font_class": "a-texteditorsuperscript-line", + "unicode": "ea3b", + "unicode_decimal": 59963 + }, + { + "icon_id": "24342567", + "name": "text editor subscript-line", + "font_class": "a-texteditorsubscript-line", + "unicode": "ea3c", + "unicode_decimal": 59964 + }, + { + "icon_id": "24342588", + "name": "text editor forward-line", + "font_class": "a-texteditorforward-line", + "unicode": "ea3e", + "unicode_decimal": 59966 + }, + { + "icon_id": "24342602", + "name": "under box-line", + "font_class": "a-underbox-line", + "unicode": "ea3f", + "unicode_decimal": 59967 + }, + { + "icon_id": "26393489", + "name": "icon_剪刀、剪切", + "font_class": "a-icon_jiandaojianqie", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "1727472", + "name": "233键盘-线性", + "font_class": "jianpan-xianxing", + "unicode": "e8ce", + "unicode_decimal": 59598 + }, + { + "icon_id": "24341085", + "name": "admonish-line", + "font_class": "admonish-line", + "unicode": "e9d2", + "unicode_decimal": 59858 + }, + { + "icon_id": "24341552", + "name": "eye close-line", + "font_class": "a-eyeclose-line", + "unicode": "ea01", + "unicode_decimal": 59905 + }, + { + "icon_id": "24341563", + "name": "file add-fill", + "font_class": "a-fileadd-fill", + "unicode": "ea02", + "unicode_decimal": 59906 + }, + { + "icon_id": "24341939", + "name": "list view-line", + "font_class": "a-listview-line", + "unicode": "ea08", + "unicode_decimal": 59912 + }, + { + "icon_id": "10933089", + "name": "login-circle-line", + "font_class": "login-circle-line", + "unicode": "e70e", + "unicode_decimal": 59150 + }, + { + "icon_id": "10933130", + "name": "logout-circle-line", + "font_class": "logout-circle-line", + "unicode": "e71a", + "unicode_decimal": 59162 + }, + { + "icon_id": "24342352", + "name": "Security permissions-line", + "font_class": "a-Securitypermissions-line", + "unicode": "ea1e", + "unicode_decimal": 59934 + }, + { + "icon_id": "24342368", + "name": "server cloud-line", + "font_class": "a-servercloud-line", + "unicode": "ea1f", + "unicode_decimal": 59935 + }, + { + "icon_id": "24342677", + "name": "user-line", + "font_class": "user-line", + "unicode": "ea3d", + "unicode_decimal": 59965 + }, + { + "icon_id": "24341523", + "name": "effect-line", + "font_class": "effect-line", + "unicode": "e9e3", + "unicode_decimal": 59875 + }, + { + "icon_id": "24341542", + "name": "expansion-line", + "font_class": "expansion-line", + "unicode": "e9ec", + "unicode_decimal": 59884 + }, + { + "icon_id": "24341566", + "name": "eye-line", + "font_class": "eye-line", + "unicode": "e9ee", + "unicode_decimal": 59886 + }, + { + "icon_id": "24341795", + "name": "Information questions-line", + "font_class": "a-Informationquestions-line", + "unicode": "e9ff", + "unicode_decimal": 59903 + }, + { + "icon_id": "24341950", + "name": "like-line", + "font_class": "like-line", + "unicode": "e9fe", + "unicode_decimal": 59902 + }, + { + "icon_id": "24342148", + "name": "read-line", + "font_class": "read-line", + "unicode": "ea07", + "unicode_decimal": 59911 + }, + { + "icon_id": "24342342", + "name": "search-line", + "font_class": "search-line", + "unicode": "ea1c", + "unicode_decimal": 59932 + }, + { + "icon_id": "24342551", + "name": "text editor paste text-line", + "font_class": "a-texteditorpastetext-line", + "unicode": "ea37", + "unicode_decimal": 59959 + }, + { + "icon_id": "24342600", + "name": "transcript-line", + "font_class": "transcript-line", + "unicode": "ea38", + "unicode_decimal": 59960 + }, + { + "icon_id": "24342601", + "name": "transcript-fill", + "font_class": "transcript-fill", + "unicode": "ea39", + "unicode_decimal": 59961 + }, + { + "icon_id": "24341375", + "name": "computer end-line", + "font_class": "a-computerend-line", + "unicode": "e9fd", + "unicode_decimal": 59901 + }, + { + "icon_id": "24341372", + "name": "cloud-fill", + "font_class": "cloud-fill", + "unicode": "e9e4", + "unicode_decimal": 59876 + }, + { + "icon_id": "24341928", + "name": "lower right page-line", + "font_class": "a-lowerrightpage-line", + "unicode": "e9f9", + "unicode_decimal": 59897 + }, + { + "icon_id": "24341943", + "name": "lower right page-fill", + "font_class": "a-lowerrightpage-fill", + "unicode": "e9fa", + "unicode_decimal": 59898 + }, + { + "icon_id": "24341982", + "name": "model-fill", + "font_class": "model-fill", + "unicode": "e9fb", + "unicode_decimal": 59899 + }, + { + "icon_id": "24341984", + "name": "model-line", + "font_class": "model-line", + "unicode": "e9fc", + "unicode_decimal": 59900 + }, + { + "icon_id": "24341597", + "name": "file text -line", + "font_class": "a-filetext-line", + "unicode": "e9f7", + "unicode_decimal": 59895 + }, + { + "icon_id": "24341723", + "name": "folder on-line", + "font_class": "a-folderon-line", + "unicode": "e9f8", + "unicode_decimal": 59896 + }, + { + "icon_id": "24341887", + "name": "link spread-line", + "font_class": "a-linkspread-line", + "unicode": "e9f6", + "unicode_decimal": 59894 + }, + { + "icon_id": "24342111", + "name": "planet-line", + "font_class": "planet-line", + "unicode": "ea05", + "unicode_decimal": 59909 + }, + { + "icon_id": "24342152", + "name": "qr code 1-line", + "font_class": "a-qrcode1-line", + "unicode": "ea06", + "unicode_decimal": 59910 + }, + { + "icon_id": "24341576", + "name": "file choose-line", + "font_class": "a-filechoose-line", + "unicode": "e9f1", + "unicode_decimal": 59889 + }, + { + "icon_id": "24341618", + "name": "file history-line", + "font_class": "a-filehistory-line", + "unicode": "e9f2", + "unicode_decimal": 59890 + }, + { + "icon_id": "24341621", + "name": "file link-line", + "font_class": "a-filelink-line", + "unicode": "e9f3", + "unicode_decimal": 59891 + }, + { + "icon_id": "24341629", + "name": "file switch-line13", + "font_class": "a-fileswitch-line13", + "unicode": "e9f4", + "unicode_decimal": 59892 + }, + { + "icon_id": "24341641", + "name": "file seal-line", + "font_class": "a-fileseal-line", + "unicode": "e9f5", + "unicode_decimal": 59893 + }, + { + "icon_id": "24341337", + "name": "clock 3-line", + "font_class": "a-clock3-line", + "unicode": "e9e5", + "unicode_decimal": 59877 + }, + { + "icon_id": "24341569", + "name": "file add-line", + "font_class": "a-fileadd-line", + "unicode": "e9e6", + "unicode_decimal": 59878 + }, + { + "icon_id": "24341580", + "name": "file cycle-line", + "font_class": "a-filecycle-line", + "unicode": "e9e7", + "unicode_decimal": 59879 + }, + { + "icon_id": "24341587", + "name": "file edit-line", + "font_class": "a-fileedit-line", + "unicode": "e9e8", + "unicode_decimal": 59880 + }, + { + "icon_id": "24341601", + "name": "file medal-line", + "font_class": "a-filemedal-line", + "unicode": "e9e9", + "unicode_decimal": 59881 + }, + { + "icon_id": "24341603", + "name": "file prohibit-line", + "font_class": "a-fileprohibit-line", + "unicode": "e9ea", + "unicode_decimal": 59882 + }, + { + "icon_id": "24341638", + "name": "file send-line", + "font_class": "a-filesend-line", + "unicode": "e9eb", + "unicode_decimal": 59883 + }, + { + "icon_id": "24341682", + "name": "file minus-line", + "font_class": "a-fileminus-line", + "unicode": "e9ef", + "unicode_decimal": 59887 + }, + { + "icon_id": "24341688", + "name": "file star-line", + "font_class": "a-filestar-line", + "unicode": "e9f0", + "unicode_decimal": 59888 + }, + { + "icon_id": "24342096", + "name": "pen-line", + "font_class": "pen-line", + "unicode": "ea03", + "unicode_decimal": 59907 + }, + { + "icon_id": "24342360", + "name": "sendmail-fill", + "font_class": "sendmail-fill", + "unicode": "ea1d", + "unicode_decimal": 59933 + }, + { + "icon_id": "24342451", + "name": "star-fill", + "font_class": "star-fill", + "unicode": "ea29", + "unicode_decimal": 59945 + }, + { + "icon_id": "24342460", + "name": "star-line", + "font_class": "star-line", + "unicode": "ea2a", + "unicode_decimal": 59946 + }, + { + "icon_id": "24342513", + "name": "switch off-line", + "font_class": "a-switchoff-line", + "unicode": "ea35", + "unicode_decimal": 59957 + }, + { + "icon_id": "24342515", + "name": "switch on-line", + "font_class": "a-switchon-line", + "unicode": "ea36", + "unicode_decimal": 59958 + }, + { + "icon_id": "24526419", + "name": "right small line-line", + "font_class": "a-rightsmallline-line", + "unicode": "ea99", + "unicode_decimal": 60057 + }, + { + "icon_id": "24341076", + "name": "add-line", + "font_class": "add-line", + "unicode": "e9d3", + "unicode_decimal": 59859 + }, + { + "icon_id": "24341348", + "name": "cloud refresh-line", + "font_class": "a-cloudrefresh-line", + "unicode": "e9e2", + "unicode_decimal": 59874 + }, + { + "icon_id": "24342536", + "name": "tally-line", + "font_class": "tally-line", + "unicode": "ea31", + "unicode_decimal": 59953 + }, + { + "icon_id": "24342596", + "name": "text editor save-line", + "font_class": "a-texteditorsave-line", + "unicode": "ea34", + "unicode_decimal": 59956 + }, + { + "icon_id": "24341084", + "name": "adhibition-line", + "font_class": "adhibition-line", + "unicode": "e9d1", + "unicode_decimal": 59857 + }, + { + "icon_id": "24342712", + "name": "wifi-line", + "font_class": "wifi-line", + "unicode": "ea42", + "unicode_decimal": 59970 + }, + { + "icon_id": "24341313", + "name": "cloud download-line", + "font_class": "a-clouddownload-line", + "unicode": "e9dc", + "unicode_decimal": 59868 + }, + { + "icon_id": "24341341", + "name": "cloud choose-line", + "font_class": "a-cloudchoose-line", + "unicode": "e9dd", + "unicode_decimal": 59869 + }, + { + "icon_id": "24341362", + "name": "cloud-line", + "font_class": "cloud-line", + "unicode": "e9de", + "unicode_decimal": 59870 + }, + { + "icon_id": "24341414", + "name": "cloud upload-line", + "font_class": "a-cloudupload-line", + "unicode": "e9e0", + "unicode_decimal": 59872 + }, + { + "icon_id": "10932214", + "name": "task-line", + "font_class": "task-line", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "10931794", + "name": "layout-column-line", + "font_class": "layout-column-line", + "unicode": "e638", + "unicode_decimal": 58936 + }, + { + "icon_id": "10932228", + "name": "bold", + "font_class": "bold", + "unicode": "e678", + "unicode_decimal": 59000 + }, + { + "icon_id": "10932232", + "name": "italic", + "font_class": "italic", + "unicode": "e679", + "unicode_decimal": 59001 + }, + { + "icon_id": "10932234", + "name": "heading", + "font_class": "heading", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "10932237", + "name": "indent-increase", + "font_class": "indent-increase", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "10932238", + "name": "double-quotes-r", + "font_class": "double-quotes-r", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "10932239", + "name": "list-unordered", + "font_class": "list-unordered", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "10932242", + "name": "link-m", + "font_class": "link-m", + "unicode": "e67e", + "unicode_decimal": 59006 + }, + { + "icon_id": "10932244", + "name": "indent-decrease", + "font_class": "indent-decrease", + "unicode": "e67f", + "unicode_decimal": 59007 + }, + { + "icon_id": "10932246", + "name": "separator", + "font_class": "separator", + "unicode": "e680", + "unicode_decimal": 59008 + }, + { + "icon_id": "10932252", + "name": "list-ordered", + "font_class": "list-ordered", + "unicode": "e681", + "unicode_decimal": 59009 + }, + { + "icon_id": "10932253", + "name": "code-view", + "font_class": "code-view", + "unicode": "e682", + "unicode_decimal": 59010 + }, + { + "icon_id": "10932256", + "name": "strikethrough", + "font_class": "strikethrough", + "unicode": "e683", + "unicode_decimal": 59011 + }, + { + "icon_id": "10932257", + "name": "single-quotes-r", + "font_class": "single-quotes-r", + "unicode": "e684", + "unicode_decimal": 59012 + }, + { + "icon_id": "10932267", + "name": "table-2", + "font_class": "table-", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "10932769", + "name": "image-2-line", + "font_class": "image--line", + "unicode": "e6d3", + "unicode_decimal": 59091 + }, + { + "icon_id": "10932928", + "name": "apps-line", + "font_class": "apps-line", + "unicode": "e6ed", + "unicode_decimal": 59117 + }, + { + "icon_id": "10933117", + "name": "refresh-line", + "font_class": "refresh-line", + "unicode": "e713", + "unicode_decimal": 59155 + }, + { + "icon_id": "24341456", + "name": "copy-line", + "font_class": "copy-line", + "unicode": "e9df", + "unicode_decimal": 59871 + }, + { + "icon_id": "24341245", + "name": "bug-line", + "font_class": "bug-line", + "unicode": "e9da", + "unicode_decimal": 59866 + }, + { + "icon_id": "24342434", + "name": "shrink-line", + "font_class": "shrink-line", + "unicode": "ea27", + "unicode_decimal": 59943 + }, + { + "icon_id": "24342502", + "name": "subtract-line", + "font_class": "subtract-line", + "unicode": "ea30", + "unicode_decimal": 59952 + }, + { + "icon_id": "24341239", + "name": "box-line", + "font_class": "box-line", + "unicode": "e9d9", + "unicode_decimal": 59865 + }, + { + "icon_id": "24341285", + "name": "close line-line", + "font_class": "a-closeline-line", + "unicode": "e2ec", + "unicode_decimal": 58092 + }, + { + "icon_id": "24341755", + "name": "home 1-line", + "font_class": "a-home1-line", + "unicode": "e9ed", + "unicode_decimal": 59885 + }, + { + "icon_id": "24342057", + "name": "package 2-line", + "font_class": "a-package2-line", + "unicode": "ea00", + "unicode_decimal": 59904 + }, + { + "icon_id": "24342097", + "name": "picture-line", + "font_class": "picture-line", + "unicode": "ea04", + "unicode_decimal": 59908 + }, + { + "icon_id": "24342584", + "name": "text editor highlight color-line", + "font_class": "a-texteditorhighlightcolor-line", + "unicode": "ea32", + "unicode_decimal": 59954 + }, + { + "icon_id": "24342587", + "name": "tier-line", + "font_class": "tier-line", + "unicode": "ea33", + "unicode_decimal": 59955 + } + ] +} diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.ttf b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.ttf new file mode 100644 index 0000000..7206595 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.ttf differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff new file mode 100644 index 0000000..c56912d Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff2 b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff2 new file mode 100644 index 0000000..8d8e445 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/blossom/iconfont.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/tech/iconfont.js b/blossom-editor/src/renderer/src/assets/iconfont/tech/iconfont.js new file mode 100644 index 0000000..d0d374f --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/tech/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_1972496='',function(h){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var a,z,t,p,v,i=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}a=function(){var l,c=document.createElement("div");c.innerHTML=h._iconfont_svg_string_1972496,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?i(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),a()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(t=a,p=h.document,v=!1,M(),p.onreadystatechange=function(){"complete"==p.readyState&&(p.onreadystatechange=null,m())})}function m(){v||(v=!0,t())}function M(){try{p.documentElement.doScroll("left")}catch(l){return void setTimeout(M,50)}m()}}(window); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.css b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.css new file mode 100644 index 0000000..2c6636d --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.css @@ -0,0 +1,223 @@ +@font-face { + font-family: "iconwt"; /* Project id 1919115 */ + src: url('iconfont.woff2?t=1689156734710') format('woff2'), + url('iconfont.woff?t=1689156734710') format('woff'), + url('iconfont.ttf?t=1689156734710') format('truetype'); +} + +.iconwt { + font-family: "iconwt" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.wt-yintian1:before { + content: "\e758"; +} + +.wt-qingtian:before { + content: "\e606"; +} + +.wt-baoxue:before { + content: "\e607"; +} + +.wt-dafeng1:before { + content: "\e608"; +} + +.wt-bingbao:before { + content: "\e609"; +} + +.wt-baoyu1:before { + content: "\e60a"; +} + +.wt-daxue:before { + content: "\e60b"; +} + +.wt-fuchen:before { + content: "\e60c"; +} + +.wt-leizhenyu:before { + content: "\e60d"; +} + +.wt-duoyun1:before { + content: "\e60e"; +} + +.wt-qingduwumai:before { + content: "\e60f"; +} + +.wt-shachen:before { + content: "\e610"; +} + +.wt-wanduoyun:before { + content: "\e611"; +} + +.wt-wanqingtian:before { + content: "\e612"; +} + +.wt-wu:before { + content: "\e613"; +} + +.wt-dayu:before { + content: "\e614"; +} + +.wt-xiaoxue:before { + content: "\e615"; +} + +.wt-xiaoyu1:before { + content: "\e616"; +} + +.wt-yintian:before { + content: "\e617"; +} + +.wt-zhongyu1:before { + content: "\e618"; +} + +.wt-zhongduwumai:before { + content: "\e619"; +} + +.wt-zhongxue:before { + content: "\e61a"; +} + +.wt-zhongduwumai1:before { + content: "\e61b"; +} + +.wt-yujiaxue:before { + content: "\e61c"; +} + +.wt-baoyu:before { + content: "\e6c6"; +} + +.wt-dalei:before { + content: "\e6c7"; +} + +.wt-dafeng:before { + content: "\e6c8"; +} + +.wt-caihong:before { + content: "\e6c9"; +} + +.wt-feng:before { + content: "\e6ca"; +} + +.wt-leijiayu:before { + content: "\e6cb"; +} + +.wt-gongshui:before { + content: "\e6cc"; +} + +.wt-duoyun:before { + content: "\e6cd"; +} + +.wt-longjuanfeng:before { + content: "\e6ce"; +} + +.wt-diwen:before { + content: "\e6cf"; +} + +.wt-gaowen:before { + content: "\e6d0"; +} + +.wt-rila:before { + content: "\e6d1"; +} + +.wt-qingduoyun:before { + content: "\e6d2"; +} + +.wt-qing:before { + content: "\e6d3"; +} + +.wt-qingjiafeng:before { + content: "\e6d4"; +} + +.wt-rishi:before { + content: "\e6d5"; +} + +.wt-richu:before { + content: "\e6d6"; +} + +.wt-xiaoyu:before { + content: "\e6d7"; +} + +.wt-yu:before { + content: "\e6d8"; +} + +.wt-yejianduoyun:before { + content: "\e6d9"; +} + +.wt-shuilao:before { + content: "\e6da"; +} + +.wt-du:before { + content: "\e6db"; +} + +.wt-zhongyu:before { + content: "\e6dc"; +} + +.wt-xue:before { + content: "\e6dd"; +} + +.wt-avalanche:before { + content: "\e6de"; +} + +.wt-earthquake:before { + content: "\e6df"; +} + +.wt-eruption:before { + content: "\e6e0"; +} + +.wt-yueliang:before { + content: "\e6e1"; +} + diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.js b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.js new file mode 100644 index 0000000..05a28d7 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_1919115='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],l=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var h,p,i,z,d,M=function(a,l){l.parentNode.insertBefore(a,l)};if(l&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}h=function(){var a,l=document.createElement("div");l.innerHTML=t._iconfont_svg_string_1919115,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?M(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(p=function(){document.removeEventListener("DOMContentLoaded",p,!1),h()},document.addEventListener("DOMContentLoaded",p,!1)):document.attachEvent&&(i=h,z=t.document,d=!1,o(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,f())})}function f(){d||(d=!0,i())}function o(){try{z.documentElement.doScroll("left")}catch(a){return void setTimeout(o,50)}f()}}(window); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.json b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.json new file mode 100644 index 0000000..ede61f7 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.json @@ -0,0 +1,373 @@ +{ + "id": "1919115", + "name": "天气", + "font_family": "iconwt", + "css_prefix_text": "wt-", + "description": "", + "glyphs": [ + { + "icon_id": "9829569", + "name": "阴天", + "font_class": "yintian1", + "unicode": "e758", + "unicode_decimal": 59224 + }, + { + "icon_id": "13635183", + "name": "晴天", + "font_class": "qingtian", + "unicode": "e606", + "unicode_decimal": 58886 + }, + { + "icon_id": "13635209", + "name": "暴雪", + "font_class": "baoxue", + "unicode": "e607", + "unicode_decimal": 58887 + }, + { + "icon_id": "13635217", + "name": "大风", + "font_class": "dafeng1", + "unicode": "e608", + "unicode_decimal": 58888 + }, + { + "icon_id": "13635218", + "name": "冰雹", + "font_class": "bingbao", + "unicode": "e609", + "unicode_decimal": 58889 + }, + { + "icon_id": "13635219", + "name": "暴雨", + "font_class": "baoyu1", + "unicode": "e60a", + "unicode_decimal": 58890 + }, + { + "icon_id": "13635220", + "name": "大雪", + "font_class": "daxue", + "unicode": "e60b", + "unicode_decimal": 58891 + }, + { + "icon_id": "13635227", + "name": "浮尘", + "font_class": "fuchen", + "unicode": "e60c", + "unicode_decimal": 58892 + }, + { + "icon_id": "13635228", + "name": "雷阵雨", + "font_class": "leizhenyu", + "unicode": "e60d", + "unicode_decimal": 58893 + }, + { + "icon_id": "13635229", + "name": "多云", + "font_class": "duoyun1", + "unicode": "e60e", + "unicode_decimal": 58894 + }, + { + "icon_id": "13635230", + "name": "轻度雾霾", + "font_class": "qingduwumai", + "unicode": "e60f", + "unicode_decimal": 58895 + }, + { + "icon_id": "13635231", + "name": "沙尘", + "font_class": "shachen", + "unicode": "e610", + "unicode_decimal": 58896 + }, + { + "icon_id": "13635232", + "name": "晚多云", + "font_class": "wanduoyun", + "unicode": "e611", + "unicode_decimal": 58897 + }, + { + "icon_id": "13635233", + "name": "晚晴天", + "font_class": "wanqingtian", + "unicode": "e612", + "unicode_decimal": 58898 + }, + { + "icon_id": "13635234", + "name": "雾", + "font_class": "wu", + "unicode": "e613", + "unicode_decimal": 58899 + }, + { + "icon_id": "13635244", + "name": "大雨", + "font_class": "dayu", + "unicode": "e614", + "unicode_decimal": 58900 + }, + { + "icon_id": "13635247", + "name": "小雪", + "font_class": "xiaoxue", + "unicode": "e615", + "unicode_decimal": 58901 + }, + { + "icon_id": "13635248", + "name": "小雨", + "font_class": "xiaoyu1", + "unicode": "e616", + "unicode_decimal": 58902 + }, + { + "icon_id": "13635249", + "name": "阴天", + "font_class": "yintian", + "unicode": "e617", + "unicode_decimal": 58903 + }, + { + "icon_id": "13635250", + "name": "中雨", + "font_class": "zhongyu1", + "unicode": "e618", + "unicode_decimal": 58904 + }, + { + "icon_id": "13635251", + "name": "中度雾霾", + "font_class": "zhongduwumai", + "unicode": "e619", + "unicode_decimal": 58905 + }, + { + "icon_id": "13635252", + "name": "中雪", + "font_class": "zhongxue", + "unicode": "e61a", + "unicode_decimal": 58906 + }, + { + "icon_id": "13635253", + "name": "重度雾霾", + "font_class": "zhongduwumai1", + "unicode": "e61b", + "unicode_decimal": 58907 + }, + { + "icon_id": "13635254", + "name": "雨夹雪", + "font_class": "yujiaxue", + "unicode": "e61c", + "unicode_decimal": 58908 + }, + { + "icon_id": "9594812", + "name": "暴雨", + "font_class": "baoyu", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "9594813", + "name": "打雷", + "font_class": "dalei", + "unicode": "e6c7", + "unicode_decimal": 59079 + }, + { + "icon_id": "9594814", + "name": "大风", + "font_class": "dafeng", + "unicode": "e6c8", + "unicode_decimal": 59080 + }, + { + "icon_id": "9594815", + "name": "彩虹", + "font_class": "caihong", + "unicode": "e6c9", + "unicode_decimal": 59081 + }, + { + "icon_id": "9594816", + "name": "风", + "font_class": "feng", + "unicode": "e6ca", + "unicode_decimal": 59082 + }, + { + "icon_id": "9594817", + "name": "雷加雨", + "font_class": "leijiayu", + "unicode": "e6cb", + "unicode_decimal": 59083 + }, + { + "icon_id": "9594818", + "name": "供水", + "font_class": "gongshui", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, + { + "icon_id": "9594819", + "name": "多云", + "font_class": "duoyun", + "unicode": "e6cd", + "unicode_decimal": 59085 + }, + { + "icon_id": "9594820", + "name": "龙卷风", + "font_class": "longjuanfeng", + "unicode": "e6ce", + "unicode_decimal": 59086 + }, + { + "icon_id": "9594821", + "name": "低温", + "font_class": "diwen", + "unicode": "e6cf", + "unicode_decimal": 59087 + }, + { + "icon_id": "9594822", + "name": "高温", + "font_class": "gaowen", + "unicode": "e6d0", + "unicode_decimal": 59088 + }, + { + "icon_id": "9594823", + "name": "日落", + "font_class": "rila", + "unicode": "e6d1", + "unicode_decimal": 59089 + }, + { + "icon_id": "9594824", + "name": "晴多云", + "font_class": "qingduoyun", + "unicode": "e6d2", + "unicode_decimal": 59090 + }, + { + "icon_id": "9594825", + "name": "晴", + "font_class": "qing", + "unicode": "e6d3", + "unicode_decimal": 59091 + }, + { + "icon_id": "9594826", + "name": "晴加风", + "font_class": "qingjiafeng", + "unicode": "e6d4", + "unicode_decimal": 59092 + }, + { + "icon_id": "9594827", + "name": "日食", + "font_class": "rishi", + "unicode": "e6d5", + "unicode_decimal": 59093 + }, + { + "icon_id": "9594828", + "name": "日出", + "font_class": "richu", + "unicode": "e6d6", + "unicode_decimal": 59094 + }, + { + "icon_id": "9594829", + "name": "小雨", + "font_class": "xiaoyu", + "unicode": "e6d7", + "unicode_decimal": 59095 + }, + { + "icon_id": "9594830", + "name": "雨", + "font_class": "yu", + "unicode": "e6d8", + "unicode_decimal": 59096 + }, + { + "icon_id": "9594831", + "name": "夜间多云", + "font_class": "yejianduoyun", + "unicode": "e6d9", + "unicode_decimal": 59097 + }, + { + "icon_id": "9594832", + "name": "水涝", + "font_class": "shuilao", + "unicode": "e6da", + "unicode_decimal": 59098 + }, + { + "icon_id": "9594833", + "name": "0度", + "font_class": "du", + "unicode": "e6db", + "unicode_decimal": 59099 + }, + { + "icon_id": "9594834", + "name": "中雨", + "font_class": "zhongyu", + "unicode": "e6dc", + "unicode_decimal": 59100 + }, + { + "icon_id": "9594835", + "name": "雪", + "font_class": "xue", + "unicode": "e6dd", + "unicode_decimal": 59101 + }, + { + "icon_id": "9594836", + "name": "avalanche", + "font_class": "avalanche", + "unicode": "e6de", + "unicode_decimal": 59102 + }, + { + "icon_id": "9594837", + "name": "earthquake", + "font_class": "earthquake", + "unicode": "e6df", + "unicode_decimal": 59103 + }, + { + "icon_id": "9594838", + "name": "eruption", + "font_class": "eruption", + "unicode": "e6e0", + "unicode_decimal": 59104 + }, + { + "icon_id": "9594839", + "name": "月亮", + "font_class": "yueliang", + "unicode": "e6e1", + "unicode_decimal": 59105 + } + ] +} diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.ttf b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.ttf new file mode 100644 index 0000000..af926b2 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.ttf differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff new file mode 100644 index 0000000..48e9540 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff2 b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff2 new file mode 100644 index 0000000..0ca2847 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weather/iconfont.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.css b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.css new file mode 100644 index 0000000..806bfcc --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.css @@ -0,0 +1,1343 @@ +@font-face { + font-family: "iconwl"; /* Project id 4163717 */ + src: url('iconfont.woff2?t=1690277133920') format('woff2'), + url('iconfont.woff?t=1690277133920') format('woff'), + url('iconfont.ttf?t=1690277133920') format('truetype'); +} + +.iconwl { + font-family: "iconwl" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.wl-emoji:before { + content: "\e709"; +} + +.wl-navicat:before { + content: "\e678"; +} + +.wl-everything:before { + content: "\e6ab"; +} + +.wl-placeholder:before { + content: "\e6df"; +} + +.wl-placeholder1:before { + content: "\e7dd"; +} + +.wl-logitech:before { + content: "\e6f2"; +} + +.wl-playstation:before { + content: "\e6f3"; +} + +.wl-fushi:before { + content: "\e6f4"; +} + +.wl-gopro:before { + content: "\e6f5"; +} + +.wl-canon:before { + content: "\e6f6"; +} + +.wl-sony:before { + content: "\e6f8"; +} + +.wl-philips:before { + content: "\e6f9"; +} + +.wl-panasonic:before { + content: "\e6fa"; +} + +.wl-playstation1:before { + content: "\ec38"; +} + +.wl-xbox2:before { + content: "\ec3a"; +} + +.wl-xbox:before { + content: "\e6fc"; +} + +.wl-casio:before { + content: "\e6fd"; +} + +.wl-dji:before { + content: "\e6fe"; +} + +.wl-vlc:before { + content: "\e6ff"; +} + +.wl-logitech1:before { + content: "\e7c6"; +} + +.wl-sennheiser:before { + content: "\e7ed"; +} + +.wl-bose:before { + content: "\e701"; +} + +.wl-xbox1:before { + content: "\e702"; +} + +.wl-playstation2:before { + content: "\e703"; +} + +.wl-jbl:before { + content: "\e70b"; +} + +.wl-nikon:before { + content: "\e70d"; +} + +.wl-msi:before { + content: "\e6d1"; +} + +.wl-acer:before { + content: "\e6d2"; +} + +.wl-chinapost:before { + content: "\e6d3"; +} + +.wl-bank-ccb:before { + content: "\e6d5"; +} + +.wl-f5:before { + content: "\e6d7"; +} + +.wl-memcached:before { + content: "\e771"; +} + +.wl-websphere:before { + content: "\e7c3"; +} + +.wl-asus:before { + content: "\e6db"; +} + +.wl-weblogic:before { + content: "\e6dc"; +} + +.wl-go-jaeger:before { + content: "\e6dd"; +} + +.wl-lenovo:before { + content: "\e6de"; +} + +.wl-tomcat:before { + content: "\e74e"; +} + +.wl-mongodb:before { + content: "\e745"; +} + +.wl-slack1:before { + content: "\ea04"; +} + +.wl-consul:before { + content: "\e6e0"; +} + +.wl-istio:before { + content: "\e6e1"; +} + +.wl-google-cloud1:before { + content: "\e6e2"; +} + +.wl-vmware:before { + content: "\e6e3"; +} + +.wl-elastic-search1:before { + content: "\e882"; +} + +.wl-huasan:before { + content: "\e6e5"; +} + +.wl-memcached1:before { + content: "\e6e6"; +} + +.wl-elastic-beats:before { + content: "\e6e7"; +} + +.wl-xshell:before { + content: "\e9db"; +} + +.wl-graphite:before { + content: "\e6e8"; +} + +.wl-yanfa:before { + content: "\e7cf"; +} + +.wl-cisco:before { + content: "\e6e9"; +} + +.wl-icingaweb:before { + content: "\e6ea"; +} + +.wl-microsoft-iis:before { + content: "\e7e6"; +} + +.wl-beatsbydre:before { + content: "\e909"; +} + +.wl-google-cloud2:before { + content: "\e6eb"; +} + +.wl-reactor-netty:before { + content: "\e8e4"; +} + +.wl-elastic-cloud:before { + content: "\e6ec"; +} + +.wl-elastic-x-pack:before { + content: "\e6ed"; +} + +.wl-opentelemetry:before { + content: "\e75e"; +} + +.wl-fluentd:before { + content: "\e797"; +} + +.wl-microsoft-azure:before { + content: "\e6ee"; +} + +.wl-activemq:before { + content: "\e6ef"; +} + +.wl-envoy:before { + content: "\e83a"; +} + +.wl-mqtt:before { + content: "\e6f0"; +} + +.wl-activemq1:before { + content: "\e6f1"; +} + +.wl-mybatis:before { + content: "\e6cf"; +} + +.wl-ffmpeg:before { + content: "\e64a"; +} + +.wl-ffmpeg2:before { + content: "\e6ce"; +} + +.wl-codeopen:before { + content: "\e6cd"; +} + +.wl-oracle2:before { + content: "\ea23"; +} + +.wl-oracle1:before { + content: "\e84a"; +} + +.wl-wenxinyiyan:before { + content: "\e77e"; +} + +.wl-convertio:before { + content: "\e6cc"; +} + +.wl-zhanku2:before { + content: "\e614"; +} + +.wl-zhanku:before { + content: "\e8b9"; +} + +.wl-zhanku1:before { + content: "\e6c7"; +} + +.wl-linux1:before { + content: "\e631"; +} + +.wl-prometheus1:before { + content: "\e6a8"; +} + +.wl-redis1:before { + content: "\e6c1"; +} + +.wl-springcloud1:before { + content: "\e666"; +} + +.wl-shimo:before { + content: "\e6c2"; +} + +.wl-flutter:before { + content: "\e6c3"; +} + +.wl-baiduyun:before { + content: "\e6c5"; +} + +.wl-huabanwang1:before { + content: "\e6c6"; +} + +.wl-prometheus:before { + content: "\e6c8"; +} + +.wl-openai:before { + content: "\e6c9"; +} + +.wl-chatgpt:before { + content: "\e6ca"; +} + +.wl-chatgpt1:before { + content: "\e6cb"; +} + +.wl-ibm:before { + content: "\ea0d"; +} + +.wl-nvidia2:before { + content: "\e6a1"; +} + +.wl-bootstrap1:before { + content: "\e6a2"; +} + +.wl-electron1:before { + content: "\e6a4"; +} + +.wl-docker2:before { + content: "\e6a6"; +} + +.wl-nvidia:before { + content: "\e6ae"; +} + +.wl-amazon:before { + content: "\ea09"; +} + +.wl-github2:before { + content: "\ea0b"; +} + +.wl-nacos:before { + content: "\e6af"; +} + +.wl-amd:before { + content: "\e6b1"; +} + +.wl-clash:before { + content: "\e6b3"; +} + +.wl-SkyWalking:before { + content: "\e6b9"; +} + +.wl-ruixingkafei:before { + content: "\e6b5"; +} + +.wl-intel:before { + content: "\e6b6"; +} + +.wl-qiniu:before { + content: "\e6b7"; +} + +.wl-kafka:before { + content: "\e6b8"; +} + +.wl-maven:before { + content: "\e700"; +} + +.wl-baidu2:before { + content: "\e6bb"; +} + +.wl-nvidia1:before { + content: "\eca6"; +} + +.wl-rabbitmq1:before { + content: "\ecd0"; +} + +.wl-vite:before { + content: "\e6bc"; +} + +.wl-qqyinyue:before { + content: "\e6bd"; +} + +.wl-leetcode:before { + content: "\e6be"; +} + +.wl-echarts:before { + content: "\e6bf"; +} + +.wl-feign:before { + content: "\e6c0"; +} + +.wl-microsoft-bing:before { + content: "\e6ac"; +} + +.wl-tianqi:before { + content: "\e6a3"; +} + +.wl-tianqi1:before { + content: "\e6a5"; +} + +.wl-Amd:before { + content: "\edd7"; +} + +.wl-computercase:before { + content: "\ebd8"; +} + +.wl-epic:before { + content: "\eb8a"; +} + +.wl-radarchart:before { + content: "\ebca"; +} + +.wl-huawei:before { + content: "\e6a9"; +} + +.wl-huawei1:before { + content: "\e6aa"; +} + +.wl-guava:before { + content: "\e6a0"; +} + +.wl-sass1:before { + content: "\e6d0"; +} + +.wl-bootstrap:before { + content: "\e6d4"; +} + +.wl-sass:before { + content: "\e6d9"; +} + +.wl-xampp:before { + content: "\e6da"; +} + +.wl-javascript:before { + content: "\e704"; +} + +.wl-css-outline:before { + content: "\e706"; +} + +.wl-html-outline:before { + content: "\e708"; +} + +.wl-html1:before { + content: "\e70a"; +} + +.wl-autoCAD:before { + content: "\e74a"; +} + +.wl-angular:before { + content: "\e798"; +} + +.wl-webpack:before { + content: "\e799"; +} + +.wl-jquery:before { + content: "\e79a"; +} + +.wl-nodejs1:before { + content: "\e79b"; +} + +.wl-spark1:before { + content: "\e895"; +} + +.wl-postgresql:before { + content: "\e8b7"; +} + +.wl-hbase:before { + content: "\e8b8"; +} + +.wl-sketch:before { + content: "\e8c4"; +} + +.wl-figma:before { + content: "\e8c5"; +} + +.wl-zeplin:before { + content: "\e8c6"; +} + +.wl-principle:before { + content: "\e8c7"; +} + +.wl-adobe-premiere:before { + content: "\e61b"; +} + +.wl-adobe-acrobat-reader:before { + content: "\e646"; +} + +.wl-adobe-audition:before { + content: "\e687"; +} + +.wl-adobe-animate:before { + content: "\e689"; +} + +.wl-adobe-bridge:before { + content: "\e68a"; +} + +.wl-adobe-after-effects:before { + content: "\e68b"; +} + +.wl-adobe-character-animator:before { + content: "\e68c"; +} + +.wl-adobe-experience-cloud:before { + content: "\e68e"; +} + +.wl-adobe-default-app:before { + content: "\e690"; +} + +.wl-adobe-dreamweaver:before { + content: "\e691"; +} + +.wl-adobe-incopy:before { + content: "\e692"; +} + +.wl-adobe-illustrator:before { + content: "\e693"; +} + +.wl-adobe-indesign:before { + content: "\e694"; +} + +.wl-adobe-lightroom-classic:before { + content: "\e695"; +} + +.wl-adobe-photoshop1:before { + content: "\e696"; +} + +.wl-adobe-substance-3d-painter:before { + content: "\e697"; +} + +.wl-adobe-media-encoder:before { + content: "\e698"; +} + +.wl-adobe-photoshop:before { + content: "\e699"; +} + +.wl-adobe-prelude:before { + content: "\e69a"; +} + +.wl-adobe-substance-3d-designer:before { + content: "\e69b"; +} + +.wl-adobe-substance-3d-sampler:before { + content: "\e69c"; +} + +.wl-adobe-xd:before { + content: "\e69d"; +} + +.wl-adobe-substance-3d-stager:before { + content: "\e69f"; +} + +.wl-blossom:before { + content: "\e684"; +} + +.wl-guardcat:before { + content: "\e685"; +} + +.wl-whatsapp:before { + content: "\e66f"; +} + +.wl-reddit:before { + content: "\e671"; +} + +.wl-weibo:before { + content: "\e672"; +} + +.wl-baidu-tieba:before { + content: "\e674"; +} + +.wl-steam:before { + content: "\e675"; +} + +.wl-douyin:before { + content: "\e602"; +} + +.wl-xiaomi:before { + content: "\e676"; +} + +.wl-keep:before { + content: "\e679"; +} + +.wl-qq:before { + content: "\e67a"; +} + +.wl-youku:before { + content: "\e67b"; +} + +.wl-baidu1:before { + content: "\e67c"; +} + +.wl-bilibili3:before { + content: "\e67d"; +} + +.wl-tengxunhuiyi:before { + content: "\e67e"; +} + +.wl-wangyiyunyinle:before { + content: "\e67f"; +} + +.wl-ditu:before { + content: "\e680"; +} + +.wl-docker:before { + content: "\e681"; +} + +.wl-baiduwangpan:before { + content: "\e682"; +} + +.wl-yinxiangbiji:before { + content: "\e683"; +} + +.wl-fanyi:before { + content: "\e6e4"; +} + +.wl-bilibili1:before { + content: "\e6b4"; +} + +.wl-oss:before { + content: "\e63e"; +} + +.wl-notion:before { + content: "\ec37"; +} + +.wl-douban:before { + content: "\e611"; +} + +.wl-buysellads:before { + content: "\e650"; +} + +.wl-cnblogs:before { + content: "\e655"; +} + +.wl-codepen:before { + content: "\e658"; +} + +.wl-linkedin:before { + content: "\e659"; +} + +.wl-dingtalk:before { + content: "\e65a"; +} + +.wl-facebook:before { + content: "\e65b"; +} + +.wl-soundcloud:before { + content: "\e65d"; +} + +.wl-microsoft:before { + content: "\e65e"; +} + +.wl-oschina:before { + content: "\e65f"; +} + +.wl-telegram:before { + content: "\e661"; +} + +.wl-google-gmail:before { + content: "\e662"; +} + +.wl-gitee:before { + content: "\e663"; +} + +.wl-dribbble:before { + content: "\e664"; +} + +.wl-line:before { + content: "\e665"; +} + +.wl-linkedin-simple:before { + content: "\e668"; +} + +.wl-slack:before { + content: "\e66b"; +} + +.wl-tumblr:before { + content: "\e66c"; +} + +.wl-jianshu:before { + content: "\e66d"; +} + +.wl-vk:before { + content: "\e66e"; +} + +.wl-xiaochengxu:before { + content: "\e653"; +} + +.wl-yuque:before { + content: "\e654"; +} + +.wl-yuque1:before { + content: "\e732"; +} + +.wl-huabanwang:before { + content: "\e7bc"; +} + +.wl-microsoft-windows2:before { + content: "\ea06"; +} + +.wl-css:before { + content: "\e63b"; +} + +.wl-microsoft-onedrive:before { + content: "\e60d"; +} + +.wl-jetbrains-clion:before { + content: "\e648"; +} + +.wl-jetbrains-phpstorm:before { + content: "\e64b"; +} + +.wl-jetbrains-pycharm:before { + content: "\e64c"; +} + +.wl-jetbrains-webstorm:before { + content: "\e64d"; +} + +.wl-awesomevue:before { + content: "\e64e"; +} + +.wl-typora:before { + content: "\e64f"; +} + +.wl-utools:before { + content: "\e652"; +} + +.wl-springboot:before { + content: "\e604"; +} + +.wl-springcloud:before { + content: "\e61d"; +} + +.wl-rabbitmq:before { + content: "\e6ad"; +} + +.wl-kafka1:before { + content: "\e69e"; +} + +.wl-html:before { + content: "\e61e"; +} + +.wl-redis:before { + content: "\e673"; +} + +.wl-linux:before { + content: "\e630"; +} + +.wl-go:before { + content: "\e61f"; +} + +.wl-docker1:before { + content: "\e7b9"; +} + +.wl-electron:before { + content: "\e62b"; +} + +.wl-javascript1:before { + content: "\e7ca"; +} + +.wl-elastic-search:before { + content: "\e6f7"; +} + +.wl-postman:before { + content: "\e621"; +} + +.wl-spring:before { + content: "\e622"; +} + +.wl-nginx:before { + content: "\e707"; +} + +.wl-java1:before { + content: "\e624"; +} + +.wl-android:before { + content: "\e625"; +} + +.wl-lianjie:before { + content: "\e6b2"; +} + +.wl-jetbrains:before { + content: "\e759"; +} + +.wl-map:before { + content: "\e6fb"; +} + +.wl-hive:before { + content: "\e626"; +} + +.wl-spark:before { + content: "\e627"; +} + +.wl-git:before { + content: "\e628"; +} + +.wl-zookeeper:before { + content: "\e629"; +} + +.wl-ocr:before { + content: "\e7b1"; +} + +.wl-google-chrome:before { + content: "\e62a"; +} + +.wl-weixin:before { + content: "\e62c"; +} + +.wl-cyuyan:before { + content: "\e62d"; +} + +.wl-ios:before { + content: "\e62e"; +} + +.wl-react:before { + content: "\e62f"; +} + +.wl-k8s:before { + content: "\e632"; +} + +.wl-microsoft-vscode:before { + content: "\e633"; +} + +.wl-grafana:before { + content: "\e634"; +} + +.wl-apache:before { + content: "\eb86"; +} + +.wl-vue:before { + content: "\e6ba"; +} + +.wl-npm:before { + content: "\e669"; +} + +.wl-nodejs:before { + content: "\e66a"; +} + +.wl-qcloud:before { + content: "\e635"; +} + +.wl-aliyun1:before { + content: "\e660"; +} + +.wl-vb:before { + content: "\e636"; +} + +.wl-php:before { + content: "\e6d8"; +} + +.wl-swift:before { + content: "\e70c"; +} + +.wl-kotlin:before { + content: "\e657"; +} + +.wl-perl:before { + content: "\e670"; +} + +.wl-python:before { + content: "\e601"; +} + +.wl-ruby:before { + content: "\e637"; +} + +.wl-rust:before { + content: "\e68f"; +} + +.wl-lua:before { + content: "\e638"; +} + +.wl-oracle:before { + content: "\e639"; +} + +.wl-excel:before { + content: "\e63a"; +} + +.wl-java2:before { + content: "\eb89"; +} + +.wl-wu:before { + content: "\e686"; +} + +.wl-computergame:before { + content: "\ec32"; +} + +.wl-mysql:before { + content: "\e667"; +} + +.wl-elastic-kibana:before { + content: "\e63c"; +} + +.wl-elastic-logstash:before { + content: "\e63d"; +} + +.wl-skywalking:before { + content: "\e63f"; +} + +.wl-rancher:before { + content: "\e640"; +} + +.wl-apollo1:before { + content: "\e641"; +} + +.wl-rocketmq:before { + content: "\e642"; +} + +.wl-gitlab:before { + content: "\e65c"; +} + +.wl-jenkins:before { + content: "\e6a7"; +} + +.wl-sentry:before { + content: "\e643"; +} + +.wl-xxljob:before { + content: "\e72f"; +} + +.wl-job:before { + content: "\e644"; +} + +.wl-chandao:before { + content: "\e7ad"; +} + +.wl-apollo:before { + content: "\ec93"; +} + +.wl-superset:before { + content: "\e645"; +} + +.wl-lanhu:before { + content: "\e705"; +} + +.wl-dubbo:before { + content: "\e75d"; +} + +.wl-sentinel:before { + content: "\e647"; +} + +.wl-java:before { + content: "\e6d6"; +} + +.wl-lianlu:before { + content: "\e68d"; +} + +.wl-yishouquan:before { + content: "\e985"; +} + +.wl-quanxianguanli:before { + content: "\e6c4"; +} + +.wl-microsoft-windows:before { + content: "\e649"; +} + +.wl-iphone:before { + content: "\e7f0"; +} + +.wl-microsoft-terminal:before { + content: "\e608"; +} + +.wl-microsoft-visualstudio:before { + content: "\e615"; +} + +.wl-microsoft-edge:before { + content: "\e619"; +} + +.wl-jetbrains-idea:before { + content: "\e61c"; +} + +.wl-social-instagram:before { + content: "\e613"; +} + +.wl-neuxs-maven:before { + content: "\e606"; +} + +.wl-wikipedia:before { + content: "\e688"; +} + +.wl-douyu:before { + content: "\e60b"; +} + +.wl-baidu:before { + content: "\e60e"; +} + +.wl-mayun:before { + content: "\e603"; +} + +.wl-qiniuyun:before { + content: "\e618"; +} + +.wl-elementui:before { + content: "\e605"; +} + +.wl-processon:before { + content: "\e791"; +} + +.wl-youdaoyunbiji:before { + content: "\eb33"; +} + +.wl-stackoverflow:before { + content: "\e612"; +} + +.wl-shejiguifan:before { + content: "\e610"; +} + +.wl-youminxingkong:before { + content: "\e60c"; +} + +.wl-github:before { + content: "\e609"; +} + +.wl-github1:before { + content: "\e60a"; +} + +.wl-zhihu:before { + content: "\e607"; +} + +.wl-iconfont:before { + content: "\e600"; +} + +.wl-tengxunyun1:before { + content: "\e651"; +} + +.wl-asciidoc:before { + content: "\e623"; +} + +.wl-google:before { + content: "\e616"; +} + +.wl-antd:before { + content: "\e677"; +} + +.wl-aliyun:before { + content: "\e6b0"; +} + +.wl-bilibili:before { + content: "\e617"; +} + +.wl-juejin:before { + content: "\e61a"; +} + +.wl-teambition:before { + content: "\eb88"; +} + +.wl-twitter:before { + content: "\ea90"; +} + diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.js b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.js new file mode 100644 index 0000000..95296ea --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_4163717='',function(h){var l=(l=document.getElementsByTagName("script"))[l.length-1],a=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var c,t,z,p,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}c=function(){var l,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_4163717,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(z=c,p=h.document,v=!1,M(),p.onreadystatechange=function(){"complete"==p.readyState&&(p.onreadystatechange=null,m())})}function m(){v||(v=!0,z())}function M(){try{p.documentElement.doScroll("left")}catch(l){return void setTimeout(M,50)}m()}}(window); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.json b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.json new file mode 100644 index 0000000..838c5f7 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.json @@ -0,0 +1,2333 @@ +{ + "id": "4163717", + "name": "weblogo", + "font_family": "iconwl", + "css_prefix_text": "wl-", + "description": "各个网站的LOGO", + "glyphs": [ + { + "icon_id": "5379879", + "name": "emoji", + "font_class": "emoji", + "unicode": "e709", + "unicode_decimal": 59145 + }, + { + "icon_id": "28219700", + "name": "Navicat", + "font_class": "navicat", + "unicode": "e678", + "unicode_decimal": 59000 + }, + { + "icon_id": "29782837", + "name": "everything", + "font_class": "everything", + "unicode": "e6ab", + "unicode_decimal": 59051 + }, + { + "icon_id": "6895955", + "name": "placeholder", + "font_class": "placeholder", + "unicode": "e6df", + "unicode_decimal": 59103 + }, + { + "icon_id": "20563399", + "name": "placeholder", + "font_class": "placeholder1", + "unicode": "e7dd", + "unicode_decimal": 59357 + }, + { + "icon_id": "866947", + "name": "logitech", + "font_class": "logitech", + "unicode": "e6f2", + "unicode_decimal": 59122 + }, + { + "icon_id": "1718580", + "name": "playstation", + "font_class": "playstation", + "unicode": "e6f3", + "unicode_decimal": 59123 + }, + { + "icon_id": "7530693", + "name": "富士", + "font_class": "fushi", + "unicode": "e6f4", + "unicode_decimal": 59124 + }, + { + "icon_id": "9241006", + "name": "gopro", + "font_class": "gopro", + "unicode": "e6f5", + "unicode_decimal": 59125 + }, + { + "icon_id": "9582948", + "name": "canon", + "font_class": "canon", + "unicode": "e6f6", + "unicode_decimal": 59126 + }, + { + "icon_id": "9828925", + "name": "logo-sony", + "font_class": "sony", + "unicode": "e6f8", + "unicode_decimal": 59128 + }, + { + "icon_id": "9941078", + "name": "philips", + "font_class": "philips", + "unicode": "e6f9", + "unicode_decimal": 59129 + }, + { + "icon_id": "10298529", + "name": "panasonic", + "font_class": "panasonic", + "unicode": "e6fa", + "unicode_decimal": 59130 + }, + { + "icon_id": "12194398", + "name": "playstation 4", + "font_class": "playstation1", + "unicode": "ec38", + "unicode_decimal": 60472 + }, + { + "icon_id": "12194404", + "name": "xbox one", + "font_class": "xbox2", + "unicode": "ec3a", + "unicode_decimal": 60474 + }, + { + "icon_id": "13126309", + "name": "xbox", + "font_class": "xbox", + "unicode": "e6fc", + "unicode_decimal": 59132 + }, + { + "icon_id": "13329322", + "name": "casio-logo", + "font_class": "casio", + "unicode": "e6fd", + "unicode_decimal": 59133 + }, + { + "icon_id": "13980882", + "name": "DJI大疆", + "font_class": "dji", + "unicode": "e6fe", + "unicode_decimal": 59134 + }, + { + "icon_id": "14235504", + "name": "vlc", + "font_class": "vlc", + "unicode": "e6ff", + "unicode_decimal": 59135 + }, + { + "icon_id": "19116730", + "name": "logitech", + "font_class": "logitech1", + "unicode": "e7c6", + "unicode_decimal": 59334 + }, + { + "icon_id": "19116912", + "name": "sennheiser", + "font_class": "sennheiser", + "unicode": "e7ed", + "unicode_decimal": 59373 + }, + { + "icon_id": "19609435", + "name": "bose", + "font_class": "bose", + "unicode": "e701", + "unicode_decimal": 59137 + }, + { + "icon_id": "21169590", + "name": "xbox", + "font_class": "xbox1", + "unicode": "e702", + "unicode_decimal": 59138 + }, + { + "icon_id": "21731164", + "name": "Playstationlogo", + "font_class": "playstation2", + "unicode": "e703", + "unicode_decimal": 59139 + }, + { + "icon_id": "24347170", + "name": "jbl", + "font_class": "jbl", + "unicode": "e70b", + "unicode_decimal": 59147 + }, + { + "icon_id": "31403276", + "name": "nikon", + "font_class": "nikon", + "unicode": "e70d", + "unicode_decimal": 59149 + }, + { + "icon_id": "598970", + "name": "微星", + "font_class": "msi", + "unicode": "e6d1", + "unicode_decimal": 59089 + }, + { + "icon_id": "1013165", + "name": "宏碁", + "font_class": "acer", + "unicode": "e6d2", + "unicode_decimal": 59090 + }, + { + "icon_id": "1963045", + "name": "China_Post", + "font_class": "chinapost", + "unicode": "e6d3", + "unicode_decimal": 59091 + }, + { + "icon_id": "1963049", + "name": "China_Constuction_Bank", + "font_class": "bank-ccb", + "unicode": "e6d5", + "unicode_decimal": 59093 + }, + { + "icon_id": "2778738", + "name": "F5", + "font_class": "f5", + "unicode": "e6d7", + "unicode_decimal": 59095 + }, + { + "icon_id": "3065812", + "name": "soft_A00_Memcached", + "font_class": "memcached", + "unicode": "e771", + "unicode_decimal": 59249 + }, + { + "icon_id": "3171954", + "name": "websphere", + "font_class": "websphere", + "unicode": "e7c3", + "unicode_decimal": 59331 + }, + { + "icon_id": "4341414", + "name": "华硕", + "font_class": "asus", + "unicode": "e6db", + "unicode_decimal": 59099 + }, + { + "icon_id": "7464598", + "name": "WebLogic", + "font_class": "weblogic", + "unicode": "e6dc", + "unicode_decimal": 59100 + }, + { + "icon_id": "7610392", + "name": "jaeger", + "font_class": "go-jaeger", + "unicode": "e6dd", + "unicode_decimal": 59101 + }, + { + "icon_id": "8142305", + "name": "联想", + "font_class": "lenovo", + "unicode": "e6de", + "unicode_decimal": 59102 + }, + { + "icon_id": "9132645", + "name": "Tomcat", + "font_class": "tomcat", + "unicode": "e74e", + "unicode_decimal": 59214 + }, + { + "icon_id": "9561741", + "name": "MongoDB", + "font_class": "mongodb", + "unicode": "e745", + "unicode_decimal": 59205 + }, + { + "icon_id": "11983513", + "name": "slack", + "font_class": "slack1", + "unicode": "ea04", + "unicode_decimal": 59908 + }, + { + "icon_id": "12326378", + "name": "consul-", + "font_class": "consul", + "unicode": "e6e0", + "unicode_decimal": 59104 + }, + { + "icon_id": "13382715", + "name": "istio", + "font_class": "istio", + "unicode": "e6e1", + "unicode_decimal": 59105 + }, + { + "icon_id": "16023872", + "name": "google_cloud-icon", + "font_class": "google-cloud1", + "unicode": "e6e2", + "unicode_decimal": 59106 + }, + { + "icon_id": "16383206", + "name": "VMware Vsphere对接数据源", + "font_class": "vmware", + "unicode": "e6e3", + "unicode_decimal": 59107 + }, + { + "icon_id": "16403407", + "name": "elastic", + "font_class": "elastic-search1", + "unicode": "e882", + "unicode_decimal": 59522 + }, + { + "icon_id": "17998650", + "name": "华三", + "font_class": "huasan", + "unicode": "e6e5", + "unicode_decimal": 59109 + }, + { + "icon_id": "18186097", + "name": "icon_memcached", + "font_class": "memcached1", + "unicode": "e6e6", + "unicode_decimal": 59110 + }, + { + "icon_id": "18878548", + "name": "logo-beats-64-color", + "font_class": "elastic-beats", + "unicode": "e6e7", + "unicode_decimal": 59111 + }, + { + "icon_id": "20029353", + "name": "xshell", + "font_class": "xshell", + "unicode": "e9db", + "unicode_decimal": 59867 + }, + { + "icon_id": "20611345", + "name": "graphite", + "font_class": "graphite", + "unicode": "e6e8", + "unicode_decimal": 59112 + }, + { + "icon_id": "21363742", + "name": "研发", + "font_class": "yanfa", + "unicode": "e7cf", + "unicode_decimal": 59343 + }, + { + "icon_id": "22542329", + "name": "思科", + "font_class": "cisco", + "unicode": "e6e9", + "unicode_decimal": 59113 + }, + { + "icon_id": "23105416", + "name": "icingaweb", + "font_class": "icingaweb", + "unicode": "e6ea", + "unicode_decimal": 59114 + }, + { + "icon_id": "26770691", + "name": "IIS", + "font_class": "microsoft-iis", + "unicode": "e7e6", + "unicode_decimal": 59366 + }, + { + "icon_id": "27267520", + "name": "beatsbydre_", + "font_class": "beatsbydre", + "unicode": "e909", + "unicode_decimal": 59657 + }, + { + "icon_id": "27547203", + "name": "云厂商-Google cloud platform_24", + "font_class": "google-cloud2", + "unicode": "e6eb", + "unicode_decimal": 59115 + }, + { + "icon_id": "27702024", + "name": "Reactor Netty", + "font_class": "reactor-netty", + "unicode": "e8e4", + "unicode_decimal": 59620 + }, + { + "icon_id": "28204517", + "name": "elastic-cloud", + "font_class": "elastic-cloud", + "unicode": "e6ec", + "unicode_decimal": 59116 + }, + { + "icon_id": "28204532", + "name": "elastic-x-pack", + "font_class": "elastic-x-pack", + "unicode": "e6ed", + "unicode_decimal": 59117 + }, + { + "icon_id": "30956046", + "name": "OpenTelemetry", + "font_class": "opentelemetry", + "unicode": "e75e", + "unicode_decimal": 59230 + }, + { + "icon_id": "31410232", + "name": "fluentd", + "font_class": "fluentd", + "unicode": "e797", + "unicode_decimal": 59287 + }, + { + "icon_id": "31426582", + "name": "AZure", + "font_class": "microsoft-azure", + "unicode": "e6ee", + "unicode_decimal": 59118 + }, + { + "icon_id": "32862576", + "name": "ActiveMQ", + "font_class": "activemq", + "unicode": "e6ef", + "unicode_decimal": 59119 + }, + { + "icon_id": "35014938", + "name": "envoy", + "font_class": "envoy", + "unicode": "e83a", + "unicode_decimal": 59450 + }, + { + "icon_id": "35015235", + "name": "mqtt", + "font_class": "mqtt", + "unicode": "e6f0", + "unicode_decimal": 59120 + }, + { + "icon_id": "35639793", + "name": "ActiveMQ", + "font_class": "activemq1", + "unicode": "e6f1", + "unicode_decimal": 59121 + }, + { + "icon_id": "31340261", + "name": "MYBATIS", + "font_class": "mybatis", + "unicode": "e6cf", + "unicode_decimal": 59087 + }, + { + "icon_id": "17869546", + "name": "FFmpeg", + "font_class": "ffmpeg", + "unicode": "e64a", + "unicode_decimal": 58954 + }, + { + "icon_id": "20299380", + "name": "ffmpeg", + "font_class": "ffmpeg2", + "unicode": "e6ce", + "unicode_decimal": 59086 + }, + { + "icon_id": "377061", + "name": "codeopen", + "font_class": "codeopen", + "unicode": "e6cd", + "unicode_decimal": 59085 + }, + { + "icon_id": "3176822", + "name": "Oracle", + "font_class": "oracle2", + "unicode": "ea23", + "unicode_decimal": 59939 + }, + { + "icon_id": "27244125", + "name": "oracle", + "font_class": "oracle1", + "unicode": "e84a", + "unicode_decimal": 59466 + }, + { + "icon_id": "35398266", + "name": "文心一言", + "font_class": "wenxinyiyan", + "unicode": "e77e", + "unicode_decimal": 59262 + }, + { + "icon_id": "24564044", + "name": "convertio", + "font_class": "convertio", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, + { + "icon_id": "1320104", + "name": "站酷123", + "font_class": "zhanku2", + "unicode": "e614", + "unicode_decimal": 58900 + }, + { + "icon_id": "2781253", + "name": "站酷", + "font_class": "zhanku", + "unicode": "e8b9", + "unicode_decimal": 59577 + }, + { + "icon_id": "3966477", + "name": "站酷", + "font_class": "zhanku1", + "unicode": "e6c7", + "unicode_decimal": 59079 + }, + { + "icon_id": "7099991", + "name": "linux", + "font_class": "linux1", + "unicode": "e631", + "unicode_decimal": 58929 + }, + { + "icon_id": "10949346", + "name": "Prometheus", + "font_class": "prometheus1", + "unicode": "e6a8", + "unicode_decimal": 59048 + }, + { + "icon_id": "17777073", + "name": "redis", + "font_class": "redis1", + "unicode": "e6c1", + "unicode_decimal": 59073 + }, + { + "icon_id": "18862494", + "name": "springcloud", + "font_class": "springcloud1", + "unicode": "e666", + "unicode_decimal": 58982 + }, + { + "icon_id": "23253537", + "name": "石墨文档", + "font_class": "shimo", + "unicode": "e6c2", + "unicode_decimal": 59074 + }, + { + "icon_id": "24210159", + "name": "flutter", + "font_class": "flutter", + "unicode": "e6c3", + "unicode_decimal": 59075 + }, + { + "icon_id": "24247022", + "name": "百度云", + "font_class": "baiduyun", + "unicode": "e6c5", + "unicode_decimal": 59077 + }, + { + "icon_id": "28640199", + "name": "花瓣网", + "font_class": "huabanwang1", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "30078260", + "name": "prometheus", + "font_class": "prometheus", + "unicode": "e6c8", + "unicode_decimal": 59080 + }, + { + "icon_id": "33392911", + "name": "openai-logo", + "font_class": "openai", + "unicode": "e6c9", + "unicode_decimal": 59081 + }, + { + "icon_id": "34999706", + "name": "ChatGPT_24", + "font_class": "chatgpt", + "unicode": "e6ca", + "unicode_decimal": 59082 + }, + { + "icon_id": "35505889", + "name": "chatgpticon", + "font_class": "chatgpt1", + "unicode": "e6cb", + "unicode_decimal": 59083 + }, + { + "icon_id": "3176650", + "name": "IBM", + "font_class": "ibm", + "unicode": "ea0d", + "unicode_decimal": 59917 + }, + { + "icon_id": "4070417", + "name": "NVIDIA-Logo", + "font_class": "nvidia2", + "unicode": "e6a1", + "unicode_decimal": 59041 + }, + { + "icon_id": "6883558", + "name": "bootstrap", + "font_class": "bootstrap1", + "unicode": "e6a2", + "unicode_decimal": 59042 + }, + { + "icon_id": "7370960", + "name": "electron", + "font_class": "electron1", + "unicode": "e6a4", + "unicode_decimal": 59044 + }, + { + "icon_id": "7470494", + "name": "Docker", + "font_class": "docker2", + "unicode": "e6a6", + "unicode_decimal": 59046 + }, + { + "icon_id": "9941062", + "name": "nvidia", + "font_class": "nvidia", + "unicode": "e6ae", + "unicode_decimal": 59054 + }, + { + "icon_id": "11983536", + "name": "amazon", + "font_class": "amazon", + "unicode": "ea09", + "unicode_decimal": 59913 + }, + { + "icon_id": "11983543", + "name": "github", + "font_class": "github2", + "unicode": "ea0b", + "unicode_decimal": 59915 + }, + { + "icon_id": "12248961", + "name": "service-nacos", + "font_class": "nacos", + "unicode": "e6af", + "unicode_decimal": 59055 + }, + { + "icon_id": "13469300", + "name": "amd", + "font_class": "amd", + "unicode": "e6b1", + "unicode_decimal": 59057 + }, + { + "icon_id": "13612313", + "name": "clash-lan", + "font_class": "clash", + "unicode": "e6b3", + "unicode_decimal": 59059 + }, + { + "icon_id": "17134692", + "name": "SkyWalking", + "font_class": "SkyWalking", + "unicode": "e6b9", + "unicode_decimal": 59065 + }, + { + "icon_id": "18208987", + "name": "icon_瑞幸咖啡", + "font_class": "ruixingkafei", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "19582949", + "name": "联想", + "font_class": "intel", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "21766884", + "name": "qiniu", + "font_class": "qiniu", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24818777", + "name": "Kafka", + "font_class": "kafka", + "unicode": "e6b8", + "unicode_decimal": 59064 + }, + { + "icon_id": "25853103", + "name": "box_maven", + "font_class": "maven", + "unicode": "e700", + "unicode_decimal": 59136 + }, + { + "icon_id": "26267540", + "name": "baidu", + "font_class": "baidu2", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "27272589", + "name": "nvidia_", + "font_class": "nvidia1", + "unicode": "eca6", + "unicode_decimal": 60582 + }, + { + "icon_id": "27272709", + "name": "rabbitmq_", + "font_class": "rabbitmq1", + "unicode": "ecd0", + "unicode_decimal": 60624 + }, + { + "icon_id": "27430639", + "name": "vite", + "font_class": "vite", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "28768537", + "name": "QQ音乐", + "font_class": "qqyinyue", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "32430459", + "name": "leetcode", + "font_class": "leetcode", + "unicode": "e6be", + "unicode_decimal": 59070 + }, + { + "icon_id": "36241293", + "name": "echarts-copy", + "font_class": "echarts", + "unicode": "e6bf", + "unicode_decimal": 59071 + }, + { + "icon_id": "36366683", + "name": "feign", + "font_class": "feign", + "unicode": "e6c0", + "unicode_decimal": 59072 + }, + { + "icon_id": "3876349", + "name": "bing", + "font_class": "microsoft-bing", + "unicode": "e6ac", + "unicode_decimal": 59052 + }, + { + "icon_id": "4893439", + "name": "天气", + "font_class": "tianqi", + "unicode": "e6a3", + "unicode_decimal": 59043 + }, + { + "icon_id": "17516142", + "name": "天气", + "font_class": "tianqi1", + "unicode": "e6a5", + "unicode_decimal": 59045 + }, + { + "icon_id": "6808507", + "name": "Amd", + "font_class": "Amd", + "unicode": "edd7", + "unicode_decimal": 60887 + }, + { + "icon_id": "12193524", + "name": "computer case", + "font_class": "computercase", + "unicode": "ebd8", + "unicode_decimal": 60376 + }, + { + "icon_id": "15378348", + "name": "epicgames", + "font_class": "epic", + "unicode": "eb8a", + "unicode_decimal": 60298 + }, + { + "icon_id": "19705257", + "name": "雷达图", + "font_class": "radarchart", + "unicode": "ebca", + "unicode_decimal": 60362 + }, + { + "icon_id": "24164616", + "name": "华为", + "font_class": "huawei", + "unicode": "e6a9", + "unicode_decimal": 59049 + }, + { + "icon_id": "24164622", + "name": "华为1", + "font_class": "huawei1", + "unicode": "e6aa", + "unicode_decimal": 59050 + }, + { + "icon_id": "13244903", + "name": "guava", + "font_class": "guava", + "unicode": "e6a0", + "unicode_decimal": 59040 + }, + { + "icon_id": "3964842", + "name": "Sass", + "font_class": "sass1", + "unicode": "e6d0", + "unicode_decimal": 59088 + }, + { + "icon_id": "3965519", + "name": "Bootstrap", + "font_class": "bootstrap", + "unicode": "e6d4", + "unicode_decimal": 59092 + }, + { + "icon_id": "3965533", + "name": "SASS", + "font_class": "sass", + "unicode": "e6d9", + "unicode_decimal": 59097 + }, + { + "icon_id": "3965535", + "name": "Xampp", + "font_class": "xampp", + "unicode": "e6da", + "unicode_decimal": 59098 + }, + { + "icon_id": "3970670", + "name": "JavaScript", + "font_class": "javascript", + "unicode": "e704", + "unicode_decimal": 59140 + }, + { + "icon_id": "3970850", + "name": "CSS3-outline", + "font_class": "css-outline", + "unicode": "e706", + "unicode_decimal": 59142 + }, + { + "icon_id": "3970851", + "name": "HTML5-outline", + "font_class": "html-outline", + "unicode": "e708", + "unicode_decimal": 59144 + }, + { + "icon_id": "3970853", + "name": "HTML5", + "font_class": "html1", + "unicode": "e70a", + "unicode_decimal": 59146 + }, + { + "icon_id": "4540684", + "name": "AutoCAD", + "font_class": "autoCAD", + "unicode": "e74a", + "unicode_decimal": 59210 + }, + { + "icon_id": "6496024", + "name": "Angular", + "font_class": "angular", + "unicode": "e798", + "unicode_decimal": 59288 + }, + { + "icon_id": "6496025", + "name": "Webpack", + "font_class": "webpack", + "unicode": "e799", + "unicode_decimal": 59289 + }, + { + "icon_id": "6496028", + "name": "jQuery", + "font_class": "jquery", + "unicode": "e79a", + "unicode_decimal": 59290 + }, + { + "icon_id": "6496056", + "name": "Nodejs", + "font_class": "nodejs1", + "unicode": "e79b", + "unicode_decimal": 59291 + }, + { + "icon_id": "21434230", + "name": "Spark", + "font_class": "spark1", + "unicode": "e895", + "unicode_decimal": 59541 + }, + { + "icon_id": "22442993", + "name": "PostgreSQL", + "font_class": "postgresql", + "unicode": "e8b7", + "unicode_decimal": 59575 + }, + { + "icon_id": "22442994", + "name": "HBASE", + "font_class": "hbase", + "unicode": "e8b8", + "unicode_decimal": 59576 + }, + { + "icon_id": "22992495", + "name": "sketch", + "font_class": "sketch", + "unicode": "e8c4", + "unicode_decimal": 59588 + }, + { + "icon_id": "22994382", + "name": "Figma", + "font_class": "figma", + "unicode": "e8c5", + "unicode_decimal": 59589 + }, + { + "icon_id": "22994383", + "name": "Zeplin", + "font_class": "zeplin", + "unicode": "e8c6", + "unicode_decimal": 59590 + }, + { + "icon_id": "22994384", + "name": "Principle", + "font_class": "principle", + "unicode": "e8c7", + "unicode_decimal": 59591 + }, + { + "icon_id": "23416966", + "name": "premiere", + "font_class": "adobe-premiere", + "unicode": "e61b", + "unicode_decimal": 58907 + }, + { + "icon_id": "23417029", + "name": "acrobat-reader", + "font_class": "adobe-acrobat-reader", + "unicode": "e646", + "unicode_decimal": 58950 + }, + { + "icon_id": "23417030", + "name": "audition", + "font_class": "adobe-audition", + "unicode": "e687", + "unicode_decimal": 59015 + }, + { + "icon_id": "23417031", + "name": "animate", + "font_class": "adobe-animate", + "unicode": "e689", + "unicode_decimal": 59017 + }, + { + "icon_id": "23417032", + "name": "bridge", + "font_class": "adobe-bridge", + "unicode": "e68a", + "unicode_decimal": 59018 + }, + { + "icon_id": "23417033", + "name": "after-effects", + "font_class": "adobe-after-effects", + "unicode": "e68b", + "unicode_decimal": 59019 + }, + { + "icon_id": "23417034", + "name": "character-animator", + "font_class": "adobe-character-animator", + "unicode": "e68c", + "unicode_decimal": 59020 + }, + { + "icon_id": "23417035", + "name": "experience-cloud", + "font_class": "adobe-experience-cloud", + "unicode": "e68e", + "unicode_decimal": 59022 + }, + { + "icon_id": "23417036", + "name": "default-app-icon", + "font_class": "adobe-default-app", + "unicode": "e690", + "unicode_decimal": 59024 + }, + { + "icon_id": "23417037", + "name": "dreamweaver", + "font_class": "adobe-dreamweaver", + "unicode": "e691", + "unicode_decimal": 59025 + }, + { + "icon_id": "23417038", + "name": "incopy", + "font_class": "adobe-incopy", + "unicode": "e692", + "unicode_decimal": 59026 + }, + { + "icon_id": "23417039", + "name": "illustrator", + "font_class": "adobe-illustrator", + "unicode": "e693", + "unicode_decimal": 59027 + }, + { + "icon_id": "23417040", + "name": "indesign", + "font_class": "adobe-indesign", + "unicode": "e694", + "unicode_decimal": 59028 + }, + { + "icon_id": "23417041", + "name": "lightroom-classic", + "font_class": "adobe-lightroom-classic", + "unicode": "e695", + "unicode_decimal": 59029 + }, + { + "icon_id": "23417042", + "name": "photoshop", + "font_class": "adobe-photoshop1", + "unicode": "e696", + "unicode_decimal": 59030 + }, + { + "icon_id": "23417044", + "name": "substance-3d-painter", + "font_class": "adobe-substance-3d-painter", + "unicode": "e697", + "unicode_decimal": 59031 + }, + { + "icon_id": "23417045", + "name": "media-encoder", + "font_class": "adobe-media-encoder", + "unicode": "e698", + "unicode_decimal": 59032 + }, + { + "icon_id": "23417046", + "name": "photoshop-express", + "font_class": "adobe-photoshop", + "unicode": "e699", + "unicode_decimal": 59033 + }, + { + "icon_id": "23417047", + "name": "prelude", + "font_class": "adobe-prelude", + "unicode": "e69a", + "unicode_decimal": 59034 + }, + { + "icon_id": "23417048", + "name": "substance-3d-designer", + "font_class": "adobe-substance-3d-designer", + "unicode": "e69b", + "unicode_decimal": 59035 + }, + { + "icon_id": "23417049", + "name": "substance-3d-sampler", + "font_class": "adobe-substance-3d-sampler", + "unicode": "e69c", + "unicode_decimal": 59036 + }, + { + "icon_id": "23417050", + "name": "xd", + "font_class": "adobe-xd", + "unicode": "e69d", + "unicode_decimal": 59037 + }, + { + "icon_id": "23417051", + "name": "substance-3d-stager", + "font_class": "adobe-substance-3d-stager", + "unicode": "e69f", + "unicode_decimal": 59039 + }, + { + "icon_id": "36393612", + "name": "blossom-logo-dark", + "font_class": "blossom", + "unicode": "e684", + "unicode_decimal": 59012 + }, + { + "icon_id": "26455168", + "name": "三花猫", + "font_class": "guardcat", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "26267609", + "name": "whatsapp", + "font_class": "whatsapp", + "unicode": "e66f", + "unicode_decimal": 58991 + }, + { + "icon_id": "26267612", + "name": "reddit", + "font_class": "reddit", + "unicode": "e671", + "unicode_decimal": 58993 + }, + { + "icon_id": "26267615", + "name": "weibo", + "font_class": "weibo", + "unicode": "e672", + "unicode_decimal": 58994 + }, + { + "icon_id": "26267619", + "name": "tieba", + "font_class": "baidu-tieba", + "unicode": "e674", + "unicode_decimal": 58996 + }, + { + "icon_id": "26267623", + "name": "steam", + "font_class": "steam", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "34064566", + "name": "抖音", + "font_class": "douyin", + "unicode": "e602", + "unicode_decimal": 58882 + }, + { + "icon_id": "34064568", + "name": "小米", + "font_class": "xiaomi", + "unicode": "e676", + "unicode_decimal": 58998 + }, + { + "icon_id": "34064571", + "name": "Keep", + "font_class": "keep", + "unicode": "e679", + "unicode_decimal": 59001 + }, + { + "icon_id": "34064575", + "name": "QQ", + "font_class": "qq", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "34064576", + "name": "优酷", + "font_class": "youku", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "34064688", + "name": "百度地图", + "font_class": "baidu1", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "34064732", + "name": "哔哩哔哩", + "font_class": "bilibili3", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "34064769", + "name": "腾讯会议", + "font_class": "tengxunhuiyi", + "unicode": "e67e", + "unicode_decimal": 59006 + }, + { + "icon_id": "34064856", + "name": "网易云音乐", + "font_class": "wangyiyunyinle", + "unicode": "e67f", + "unicode_decimal": 59007 + }, + { + "icon_id": "34065120", + "name": "地图", + "font_class": "ditu", + "unicode": "e680", + "unicode_decimal": 59008 + }, + { + "icon_id": "34065146", + "name": "Docker", + "font_class": "docker", + "unicode": "e681", + "unicode_decimal": 59009 + }, + { + "icon_id": "34066905", + "name": "百度网盘", + "font_class": "baiduwangpan", + "unicode": "e682", + "unicode_decimal": 59010 + }, + { + "icon_id": "34066906", + "name": "印象笔记", + "font_class": "yinxiangbiji", + "unicode": "e683", + "unicode_decimal": 59011 + }, + { + "icon_id": "35999321", + "name": "翻译", + "font_class": "fanyi", + "unicode": "e6e4", + "unicode_decimal": 59108 + }, + { + "icon_id": "2306764", + "name": "BILIBILI_LOGO", + "font_class": "bilibili1", + "unicode": "e6b4", + "unicode_decimal": 59060 + }, + { + "icon_id": "13533336", + "name": "oss 对象存储", + "font_class": "oss", + "unicode": "e63e", + "unicode_decimal": 58942 + }, + { + "icon_id": "15378639", + "name": "notion", + "font_class": "notion", + "unicode": "ec37", + "unicode_decimal": 60471 + }, + { + "icon_id": "26267541", + "name": "douban", + "font_class": "douban", + "unicode": "e611", + "unicode_decimal": 58897 + }, + { + "icon_id": "26267543", + "name": "buysellads", + "font_class": "buysellads", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "26267545", + "name": "cnblogs", + "font_class": "cnblogs", + "unicode": "e655", + "unicode_decimal": 58965 + }, + { + "icon_id": "26267547", + "name": "codepen", + "font_class": "codepen", + "unicode": "e658", + "unicode_decimal": 58968 + }, + { + "icon_id": "26267552", + "name": "linkedin", + "font_class": "linkedin", + "unicode": "e659", + "unicode_decimal": 58969 + }, + { + "icon_id": "26267557", + "name": "dingtalk", + "font_class": "dingtalk", + "unicode": "e65a", + "unicode_decimal": 58970 + }, + { + "icon_id": "26267558", + "name": "facebook", + "font_class": "facebook", + "unicode": "e65b", + "unicode_decimal": 58971 + }, + { + "icon_id": "26267559", + "name": "soundcloud", + "font_class": "soundcloud", + "unicode": "e65d", + "unicode_decimal": 58973 + }, + { + "icon_id": "26267564", + "name": "microsoft", + "font_class": "microsoft", + "unicode": "e65e", + "unicode_decimal": 58974 + }, + { + "icon_id": "26267565", + "name": "oschina", + "font_class": "oschina", + "unicode": "e65f", + "unicode_decimal": 58975 + }, + { + "icon_id": "26267569", + "name": "telegram", + "font_class": "telegram", + "unicode": "e661", + "unicode_decimal": 58977 + }, + { + "icon_id": "26267571", + "name": "gmail", + "font_class": "google-gmail", + "unicode": "e662", + "unicode_decimal": 58978 + }, + { + "icon_id": "26267572", + "name": "gitee", + "font_class": "gitee", + "unicode": "e663", + "unicode_decimal": 58979 + }, + { + "icon_id": "26267573", + "name": "dribbble", + "font_class": "dribbble", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "26267576", + "name": "line", + "font_class": "line", + "unicode": "e665", + "unicode_decimal": 58981 + }, + { + "icon_id": "26267599", + "name": "linkedin-simple", + "font_class": "linkedin-simple", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "26267600", + "name": "slack", + "font_class": "slack", + "unicode": "e66b", + "unicode_decimal": 58987 + }, + { + "icon_id": "26267601", + "name": "tumblr", + "font_class": "tumblr", + "unicode": "e66c", + "unicode_decimal": 58988 + }, + { + "icon_id": "26267604", + "name": "jianshu", + "font_class": "jianshu", + "unicode": "e66d", + "unicode_decimal": 58989 + }, + { + "icon_id": "26267608", + "name": "vk", + "font_class": "vk", + "unicode": "e66e", + "unicode_decimal": 58990 + }, + { + "icon_id": "10369305", + "name": "小程序", + "font_class": "xiaochengxu", + "unicode": "e653", + "unicode_decimal": 58963 + }, + { + "icon_id": "11741910", + "name": "语雀", + "font_class": "yuque", + "unicode": "e654", + "unicode_decimal": 58964 + }, + { + "icon_id": "12176818", + "name": "语雀 logo", + "font_class": "yuque1", + "unicode": "e732", + "unicode_decimal": 59186 + }, + { + "icon_id": "3060363", + "name": "花瓣网", + "font_class": "huabanwang", + "unicode": "e7bc", + "unicode_decimal": 59324 + }, + { + "icon_id": "11983514", + "name": "windows", + "font_class": "microsoft-windows2", + "unicode": "ea06", + "unicode_decimal": 59910 + }, + { + "icon_id": "12600657", + "name": "css", + "font_class": "css", + "unicode": "e63b", + "unicode_decimal": 58939 + }, + { + "icon_id": "16623776", + "name": "OneDrive", + "font_class": "microsoft-onedrive", + "unicode": "e60d", + "unicode_decimal": 58893 + }, + { + "icon_id": "16627101", + "name": "Clion", + "font_class": "jetbrains-clion", + "unicode": "e648", + "unicode_decimal": 58952 + }, + { + "icon_id": "16627209", + "name": "Phpstorm", + "font_class": "jetbrains-phpstorm", + "unicode": "e64b", + "unicode_decimal": 58955 + }, + { + "icon_id": "16627245", + "name": "Pycharm", + "font_class": "jetbrains-pycharm", + "unicode": "e64c", + "unicode_decimal": 58956 + }, + { + "icon_id": "16627611", + "name": "Webstorm", + "font_class": "jetbrains-webstorm", + "unicode": "e64d", + "unicode_decimal": 58957 + }, + { + "icon_id": "16642921", + "name": "AwesomeVue", + "font_class": "awesomevue", + "unicode": "e64e", + "unicode_decimal": 58958 + }, + { + "icon_id": "16644161", + "name": "Typora", + "font_class": "typora", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "16646664", + "name": "UTools", + "font_class": "utools", + "unicode": "e652", + "unicode_decimal": 58962 + }, + { + "icon_id": "6497669", + "name": "icon-spring-boot", + "font_class": "springboot", + "unicode": "e604", + "unicode_decimal": 58884 + }, + { + "icon_id": "6958556", + "name": "spring cloud", + "font_class": "springcloud", + "unicode": "e61d", + "unicode_decimal": 58909 + }, + { + "icon_id": "7040707", + "name": "RabbitMQ1", + "font_class": "rabbitmq", + "unicode": "e6ad", + "unicode_decimal": 59053 + }, + { + "icon_id": "8185085", + "name": "kafka", + "font_class": "kafka1", + "unicode": "e69e", + "unicode_decimal": 59038 + }, + { + "icon_id": "9383108", + "name": "HTML", + "font_class": "html", + "unicode": "e61e", + "unicode_decimal": 58910 + }, + { + "icon_id": "10928140", + "name": "redis", + "font_class": "redis", + "unicode": "e673", + "unicode_decimal": 58995 + }, + { + "icon_id": "11176337", + "name": "Linux", + "font_class": "linux", + "unicode": "e630", + "unicode_decimal": 58928 + }, + { + "icon_id": "11180531", + "name": "golang", + "font_class": "go", + "unicode": "e61f", + "unicode_decimal": 58911 + }, + { + "icon_id": "11463859", + "name": "docker", + "font_class": "docker1", + "unicode": "e7b9", + "unicode_decimal": 59321 + }, + { + "icon_id": "12502624", + "name": "electron", + "font_class": "electron", + "unicode": "e62b", + "unicode_decimal": 58923 + }, + { + "icon_id": "12695230", + "name": "js", + "font_class": "javascript1", + "unicode": "e7ca", + "unicode_decimal": 59338 + }, + { + "icon_id": "13253421", + "name": "Elasticsearch", + "font_class": "elastic-search", + "unicode": "e6f7", + "unicode_decimal": 59127 + }, + { + "icon_id": "13469835", + "name": "postman", + "font_class": "postman", + "unicode": "e621", + "unicode_decimal": 58913 + }, + { + "icon_id": "13469997", + "name": "spring", + "font_class": "spring", + "unicode": "e622", + "unicode_decimal": 58914 + }, + { + "icon_id": "13524613", + "name": "Nginx", + "font_class": "nginx", + "unicode": "e707", + "unicode_decimal": 59143 + }, + { + "icon_id": "13881631", + "name": "java", + "font_class": "java1", + "unicode": "e624", + "unicode_decimal": 58916 + }, + { + "icon_id": "13907563", + "name": "spring-android", + "font_class": "android", + "unicode": "e625", + "unicode_decimal": 58917 + }, + { + "icon_id": "14379119", + "name": "链接", + "font_class": "lianjie", + "unicode": "e6b2", + "unicode_decimal": 59058 + }, + { + "icon_id": "15276027", + "name": "cib-jetbrains", + "font_class": "jetbrains", + "unicode": "e759", + "unicode_decimal": 59225 + }, + { + "icon_id": "4365777", + "name": "地图", + "font_class": "map", + "unicode": "e6fb", + "unicode_decimal": 59131 + }, + { + "icon_id": "8117978", + "name": "Apache_Hive_logo", + "font_class": "hive", + "unicode": "e626", + "unicode_decimal": 58918 + }, + { + "icon_id": "9708927", + "name": "Apache_Spark_logo", + "font_class": "spark", + "unicode": "e627", + "unicode_decimal": 58919 + }, + { + "icon_id": "9870920", + "name": "GIT", + "font_class": "git", + "unicode": "e628", + "unicode_decimal": 58920 + }, + { + "icon_id": "10181728", + "name": "zookeeper", + "font_class": "zookeeper", + "unicode": "e629", + "unicode_decimal": 58921 + }, + { + "icon_id": "10191294", + "name": "OCR识别", + "font_class": "ocr", + "unicode": "e7b1", + "unicode_decimal": 59313 + }, + { + "icon_id": "10410817", + "name": "谷歌", + "font_class": "google-chrome", + "unicode": "e62a", + "unicode_decimal": 58922 + }, + { + "icon_id": "11019996", + "name": "微信", + "font_class": "weixin", + "unicode": "e62c", + "unicode_decimal": 58924 + }, + { + "icon_id": "11275378", + "name": "C语言", + "font_class": "cyuyan", + "unicode": "e62d", + "unicode_decimal": 58925 + }, + { + "icon_id": "12069696", + "name": "ios", + "font_class": "ios", + "unicode": "e62e", + "unicode_decimal": 58926 + }, + { + "icon_id": "12786037", + "name": "react (3)", + "font_class": "react", + "unicode": "e62f", + "unicode_decimal": 58927 + }, + { + "icon_id": "12997356", + "name": "k8s", + "font_class": "k8s", + "unicode": "e632", + "unicode_decimal": 58930 + }, + { + "icon_id": "13326069", + "name": "file_type_vscode", + "font_class": "microsoft-vscode", + "unicode": "e633", + "unicode_decimal": 58931 + }, + { + "icon_id": "13469632", + "name": "grafana", + "font_class": "grafana", + "unicode": "e634", + "unicode_decimal": 58932 + }, + { + "icon_id": "14621647", + "name": "apache-line-wordmark", + "font_class": "apache", + "unicode": "eb86", + "unicode_decimal": 60294 + }, + { + "icon_id": "14628830", + "name": "vue", + "font_class": "vue", + "unicode": "e6ba", + "unicode_decimal": 59066 + }, + { + "icon_id": "5824443", + "name": "npm", + "font_class": "npm", + "unicode": "e669", + "unicode_decimal": 58985 + }, + { + "icon_id": "5824446", + "name": "nodejs", + "font_class": "nodejs", + "unicode": "e66a", + "unicode_decimal": 58986 + }, + { + "icon_id": "15732257", + "name": "腾讯云", + "font_class": "qcloud", + "unicode": "e635", + "unicode_decimal": 58933 + }, + { + "icon_id": "16084316", + "name": "阿里云", + "font_class": "aliyun1", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "434719", + "name": "VB", + "font_class": "vb", + "unicode": "e636", + "unicode_decimal": 58934 + }, + { + "icon_id": "3994691", + "name": "php", + "font_class": "php", + "unicode": "e6d8", + "unicode_decimal": 59096 + }, + { + "icon_id": "3994824", + "name": "swift", + "font_class": "swift", + "unicode": "e70c", + "unicode_decimal": 59148 + }, + { + "icon_id": "5824414", + "name": "kotlin", + "font_class": "kotlin", + "unicode": "e657", + "unicode_decimal": 58967 + }, + { + "icon_id": "5824458", + "name": "perl", + "font_class": "perl", + "unicode": "e670", + "unicode_decimal": 58992 + }, + { + "icon_id": "6840956", + "name": "Python", + "font_class": "python", + "unicode": "e601", + "unicode_decimal": 58881 + }, + { + "icon_id": "9882228", + "name": "ruby-plain", + "font_class": "ruby", + "unicode": "e637", + "unicode_decimal": 58935 + }, + { + "icon_id": "13304036", + "name": "rust", + "font_class": "rust", + "unicode": "e68f", + "unicode_decimal": 59023 + }, + { + "icon_id": "13469732", + "name": "lua", + "font_class": "lua", + "unicode": "e638", + "unicode_decimal": 58936 + }, + { + "icon_id": "10486973", + "name": "oracle", + "font_class": "oracle", + "unicode": "e639", + "unicode_decimal": 58937 + }, + { + "icon_id": "11227995", + "name": "excel", + "font_class": "excel", + "unicode": "e63a", + "unicode_decimal": 58938 + }, + { + "icon_id": "17333405", + "name": "java", + "font_class": "java2", + "unicode": "eb89", + "unicode_decimal": 60297 + }, + { + "icon_id": "4378015", + "name": "无", + "font_class": "wu", + "unicode": "e686", + "unicode_decimal": 59014 + }, + { + "icon_id": "12194367", + "name": "computer game", + "font_class": "computergame", + "unicode": "ec32", + "unicode_decimal": 60466 + }, + { + "icon_id": "10363590", + "name": "mysql", + "font_class": "mysql", + "unicode": "e667", + "unicode_decimal": 58983 + }, + { + "icon_id": "5820622", + "name": "icon-kibana-bb", + "font_class": "elastic-kibana", + "unicode": "e63c", + "unicode_decimal": 58940 + }, + { + "icon_id": "7274611", + "name": "icon-logstash-bb cop", + "font_class": "elastic-logstash", + "unicode": "e63d", + "unicode_decimal": 58941 + }, + { + "icon_id": "15011296", + "name": "skywalking", + "font_class": "skywalking", + "unicode": "e63f", + "unicode_decimal": 58943 + }, + { + "icon_id": "9404211", + "name": "Rancher", + "font_class": "rancher", + "unicode": "e640", + "unicode_decimal": 58944 + }, + { + "icon_id": "16346237", + "name": "Apollo Icon", + "font_class": "apollo1", + "unicode": "e641", + "unicode_decimal": 58945 + }, + { + "icon_id": "22940067", + "name": "rocketmq", + "font_class": "rocketmq", + "unicode": "e642", + "unicode_decimal": 58946 + }, + { + "icon_id": "13303504", + "name": "gitlab", + "font_class": "gitlab", + "unicode": "e65c", + "unicode_decimal": 58972 + }, + { + "icon_id": "22745002", + "name": "jenkins", + "font_class": "jenkins", + "unicode": "e6a7", + "unicode_decimal": 59047 + }, + { + "icon_id": "23973744", + "name": "sentry", + "font_class": "sentry", + "unicode": "e643", + "unicode_decimal": 58947 + }, + { + "icon_id": "25288586", + "name": "XXL-JOB", + "font_class": "xxljob", + "unicode": "e72f", + "unicode_decimal": 59183 + }, + { + "icon_id": "8833142", + "name": "job", + "font_class": "job", + "unicode": "e644", + "unicode_decimal": 58948 + }, + { + "icon_id": "16120211", + "name": "禅道", + "font_class": "chandao", + "unicode": "e7ad", + "unicode_decimal": 59309 + }, + { + "icon_id": "20611489", + "name": "icon_service_apollo", + "font_class": "apollo", + "unicode": "ec93", + "unicode_decimal": 60563 + }, + { + "icon_id": "8120461", + "name": "superset", + "font_class": "superset", + "unicode": "e645", + "unicode_decimal": 58949 + }, + { + "icon_id": "12932471", + "name": "蓝湖", + "font_class": "lanhu", + "unicode": "e705", + "unicode_decimal": 59141 + }, + { + "icon_id": "15830058", + "name": "dubbo", + "font_class": "dubbo", + "unicode": "e75d", + "unicode_decimal": 59229 + }, + { + "icon_id": "18758267", + "name": "sentinel", + "font_class": "sentinel", + "unicode": "e647", + "unicode_decimal": 58951 + }, + { + "icon_id": "18907995", + "name": "java", + "font_class": "java", + "unicode": "e6d6", + "unicode_decimal": 59094 + }, + { + "icon_id": "7058058", + "name": "链路追踪", + "font_class": "lianlu", + "unicode": "e68d", + "unicode_decimal": 59021 + }, + { + "icon_id": "8073428", + "name": "已授权", + "font_class": "yishouquan", + "unicode": "e985", + "unicode_decimal": 59781 + }, + { + "icon_id": "26357553", + "name": "权限管理", + "font_class": "quanxianguanli", + "unicode": "e6c4", + "unicode_decimal": 59076 + }, + { + "icon_id": "11845474", + "name": "windows", + "font_class": "microsoft-windows", + "unicode": "e649", + "unicode_decimal": 58953 + }, + { + "icon_id": "6847236", + "name": "Mac", + "font_class": "iphone", + "unicode": "e7f0", + "unicode_decimal": 59376 + }, + { + "icon_id": "16623001", + "name": "Windows Terminal_PRE", + "font_class": "microsoft-terminal", + "unicode": "e608", + "unicode_decimal": 58888 + }, + { + "icon_id": "16624559", + "name": "Visual Studio", + "font_class": "microsoft-visualstudio", + "unicode": "e615", + "unicode_decimal": 58901 + }, + { + "icon_id": "16627017", + "name": "Edge", + "font_class": "microsoft-edge", + "unicode": "e619", + "unicode_decimal": 58905 + }, + { + "icon_id": "16627193", + "name": "Idea", + "font_class": "jetbrains-idea", + "unicode": "e61c", + "unicode_decimal": 58908 + }, + { + "icon_id": "1238428", + "name": "social-instagram", + "font_class": "social-instagram", + "unicode": "e613", + "unicode_decimal": 58899 + }, + { + "icon_id": "3197090", + "name": "maven", + "font_class": "neuxs-maven", + "unicode": "e606", + "unicode_decimal": 58886 + }, + { + "icon_id": "3876583", + "name": "wikipedia", + "font_class": "wikipedia", + "unicode": "e688", + "unicode_decimal": 59016 + }, + { + "icon_id": "6068277", + "name": "斗鱼", + "font_class": "douyu", + "unicode": "e60b", + "unicode_decimal": 58891 + }, + { + "icon_id": "6068392", + "name": "百度", + "font_class": "baidu", + "unicode": "e60e", + "unicode_decimal": 58894 + }, + { + "icon_id": "6793861", + "name": "码云", + "font_class": "mayun", + "unicode": "e603", + "unicode_decimal": 58883 + }, + { + "icon_id": "6995376", + "name": "七牛云", + "font_class": "qiniuyun", + "unicode": "e618", + "unicode_decimal": 58904 + }, + { + "icon_id": "7380660", + "name": "elementui", + "font_class": "elementui", + "unicode": "e605", + "unicode_decimal": 58885 + }, + { + "icon_id": "7967767", + "name": "processon", + "font_class": "processon", + "unicode": "e791", + "unicode_decimal": 59281 + }, + { + "icon_id": "8034778", + "name": "有道云笔记", + "font_class": "youdaoyunbiji", + "unicode": "eb33", + "unicode_decimal": 60211 + }, + { + "icon_id": "8105643", + "name": "stackoverflow", + "font_class": "stackoverflow", + "unicode": "e612", + "unicode_decimal": 58898 + }, + { + "icon_id": "8795235", + "name": "设计规范", + "font_class": "shejiguifan", + "unicode": "e610", + "unicode_decimal": 58896 + }, + { + "icon_id": "9418208", + "name": "游民星空", + "font_class": "youminxingkong", + "unicode": "e60c", + "unicode_decimal": 58892 + }, + { + "icon_id": "9613101", + "name": "GitHub", + "font_class": "github", + "unicode": "e609", + "unicode_decimal": 58889 + }, + { + "icon_id": "11180201", + "name": "github", + "font_class": "github1", + "unicode": "e60a", + "unicode_decimal": 58890 + }, + { + "icon_id": "12149258", + "name": "知乎", + "font_class": "zhihu", + "unicode": "e607", + "unicode_decimal": 58887 + }, + { + "icon_id": "12261147", + "name": "iconfont", + "font_class": "iconfont", + "unicode": "e600", + "unicode_decimal": 58880 + }, + { + "icon_id": "12959160", + "name": "腾讯云", + "font_class": "tengxunyun1", + "unicode": "e651", + "unicode_decimal": 58961 + }, + { + "icon_id": "13302913", + "name": "asciidoc", + "font_class": "asciidoc", + "unicode": "e623", + "unicode_decimal": 58915 + }, + { + "icon_id": "13746933", + "name": "google", + "font_class": "google", + "unicode": "e616", + "unicode_decimal": 58902 + }, + { + "icon_id": "13925239", + "name": "antd", + "font_class": "antd", + "unicode": "e677", + "unicode_decimal": 58999 + }, + { + "icon_id": "14281612", + "name": "阿里云", + "font_class": "aliyun", + "unicode": "e6b0", + "unicode_decimal": 59056 + }, + { + "icon_id": "14470070", + "name": "bilibili", + "font_class": "bilibili", + "unicode": "e617", + "unicode_decimal": 58903 + }, + { + "icon_id": "15453465", + "name": "掘金", + "font_class": "juejin", + "unicode": "e61a", + "unicode_decimal": 58906 + }, + { + "icon_id": "15917319", + "name": "teambition", + "font_class": "teambition", + "unicode": "eb88", + "unicode_decimal": 60296 + }, + { + "icon_id": "19446213", + "name": "推特", + "font_class": "twitter", + "unicode": "ea90", + "unicode_decimal": 60048 + } + ] +} diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.ttf b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.ttf new file mode 100644 index 0000000..db7bd54 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.ttf differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff new file mode 100644 index 0000000..7bcbbde Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff differ diff --git a/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff2 b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff2 new file mode 100644 index 0000000..22c01d3 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/iconfont/weblogo/iconfont.woff2 differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/blossom_logo.png b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo.png new file mode 100644 index 0000000..c9e665f Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_dark.png b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_dark.png new file mode 100644 index 0000000..7771dea Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_dark.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_grey.png b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_grey.png new file mode 100644 index 0000000..da3ed92 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/blossom_logo_grey.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/default_user_avatar.jpg b/blossom-editor/src/renderer/src/assets/imgs/default_user_avatar.jpg new file mode 100644 index 0000000..8e005a6 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/default_user_avatar.jpg differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/note/cd.png b/blossom-editor/src/renderer/src/assets/imgs/note/cd.png new file mode 100644 index 0000000..5cf60da Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/note/cd.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/note/dustbin.png b/blossom-editor/src/renderer/src/assets/imgs/note/dustbin.png new file mode 100644 index 0000000..01becea Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/note/dustbin.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/note/note.png b/blossom-editor/src/renderer/src/assets/imgs/note/note.png new file mode 100644 index 0000000..5ee2f39 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/note/note.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/note/pin.png b/blossom-editor/src/renderer/src/assets/imgs/note/pin.png new file mode 100644 index 0000000..d8ba8b8 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/note/pin.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/note/plane.png b/blossom-editor/src/renderer/src/assets/imgs/note/plane.png new file mode 100644 index 0000000..151f239 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/note/plane.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/headset-dark.png b/blossom-editor/src/renderer/src/assets/imgs/pe/headset-dark.png new file mode 100644 index 0000000..c828294 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/headset-dark.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/headset.png b/blossom-editor/src/renderer/src/assets/imgs/pe/headset.png new file mode 100644 index 0000000..ff5b6f2 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/headset.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/laptop-dark.png b/blossom-editor/src/renderer/src/assets/imgs/pe/laptop-dark.png new file mode 100644 index 0000000..97f903f Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/laptop-dark.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/laptop.png b/blossom-editor/src/renderer/src/assets/imgs/pe/laptop.png new file mode 100644 index 0000000..f321c3d Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/laptop.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/phone.png b/blossom-editor/src/renderer/src/assets/imgs/pe/phone.png new file mode 100644 index 0000000..8e4beb4 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/phone.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/sound-dark.png b/blossom-editor/src/renderer/src/assets/imgs/pe/sound-dark.png new file mode 100644 index 0000000..8a6cd29 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/sound-dark.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/watch-dark.png b/blossom-editor/src/renderer/src/assets/imgs/pe/watch-dark.png new file mode 100644 index 0000000..0bec09f Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/watch-dark.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/pe/watch.png b/blossom-editor/src/renderer/src/assets/imgs/pe/watch.png new file mode 100644 index 0000000..1fa3677 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/pe/watch.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/ball.png b/blossom-editor/src/renderer/src/assets/imgs/plan/ball.png new file mode 100644 index 0000000..8b86fa1 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/ball.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/base-awesome.png b/blossom-editor/src/renderer/src/assets/imgs/plan/base-awesome.png new file mode 100644 index 0000000..2a67e04 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/base-awesome.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/base-cool.png b/blossom-editor/src/renderer/src/assets/imgs/plan/base-cool.png new file mode 100644 index 0000000..f980e4c Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/base-cool.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/base-cry.png b/blossom-editor/src/renderer/src/assets/imgs/plan/base-cry.png new file mode 100644 index 0000000..b8550ef Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/base-cry.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/base-learning.png b/blossom-editor/src/renderer/src/assets/imgs/plan/base-learning.png new file mode 100644 index 0000000..c99807d Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/base-learning.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/base-seesee.png b/blossom-editor/src/renderer/src/assets/imgs/plan/base-seesee.png new file mode 100644 index 0000000..f0a165e Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/base-seesee.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/beer.png b/blossom-editor/src/renderer/src/assets/imgs/plan/beer.png new file mode 100644 index 0000000..5db9302 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/beer.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/bike.png b/blossom-editor/src/renderer/src/assets/imgs/plan/bike.png new file mode 100644 index 0000000..8bc0956 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/bike.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/cat-kiss.png b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-kiss.png new file mode 100644 index 0000000..2475da7 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-kiss.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/cat-nice.png b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-nice.png new file mode 100644 index 0000000..97fd722 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-nice.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/cat-shock.png b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-shock.png new file mode 100644 index 0000000..cbb7d35 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-shock.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/cat-smile.png b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-smile.png new file mode 100644 index 0000000..8315949 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/cat-smile.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/cat.png b/blossom-editor/src/renderer/src/assets/imgs/plan/cat.png new file mode 100644 index 0000000..696feba Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/cat.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/coffee.png b/blossom-editor/src/renderer/src/assets/imgs/plan/coffee.png new file mode 100644 index 0000000..20ac46a Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/coffee.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/computer.png b/blossom-editor/src/renderer/src/assets/imgs/plan/computer.png new file mode 100644 index 0000000..f8a9f08 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/computer.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/crossbones.png b/blossom-editor/src/renderer/src/assets/imgs/plan/crossbones.png new file mode 100644 index 0000000..3b81e86 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/crossbones.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/dog.png b/blossom-editor/src/renderer/src/assets/imgs/plan/dog.png new file mode 100644 index 0000000..582b451 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/dog.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/hand-ok.png b/blossom-editor/src/renderer/src/assets/imgs/plan/hand-ok.png new file mode 100644 index 0000000..7dbb024 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/hand-ok.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/hand-write.png b/blossom-editor/src/renderer/src/assets/imgs/plan/hand-write.png new file mode 100644 index 0000000..c874c04 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/hand-write.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/juice.png b/blossom-editor/src/renderer/src/assets/imgs/plan/juice.png new file mode 100644 index 0000000..f9070fc Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/juice.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/love.png b/blossom-editor/src/renderer/src/assets/imgs/plan/love.png new file mode 100644 index 0000000..da4f566 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/love.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plan/sleep.png b/blossom-editor/src/renderer/src/assets/imgs/plan/sleep.png new file mode 100644 index 0000000..b117bef Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plan/sleep.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plant/02.png b/blossom-editor/src/renderer/src/assets/imgs/plant/02.png new file mode 100644 index 0000000..ff3661c Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plant/02.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plant/08.png b/blossom-editor/src/renderer/src/assets/imgs/plant/08.png new file mode 100644 index 0000000..af74570 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plant/08.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/plant/cactus.png b/blossom-editor/src/renderer/src/assets/imgs/plant/cactus.png new file mode 100644 index 0000000..0651894 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/plant/cactus.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/daxue-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/daxue-s.png new file mode 100644 index 0000000..2152c89 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/daxue-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/daxue.png b/blossom-editor/src/renderer/src/assets/imgs/weather/daxue.png new file mode 100644 index 0000000..c5b4175 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/daxue.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun-s.png new file mode 100644 index 0000000..fecd59d Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun.png b/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun.png new file mode 100644 index 0000000..e9bc622 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/duoyun.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/feng-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/feng-s.png new file mode 100644 index 0000000..4498dec Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/feng-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/feng.png b/blossom-editor/src/renderer/src/assets/imgs/weather/feng.png new file mode 100644 index 0000000..9c9a1d3 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/feng.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu-s.png new file mode 100644 index 0000000..332bdcf Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu.png b/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu.png new file mode 100644 index 0000000..2536321 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/leijiayu.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot-s.png new file mode 100644 index 0000000..2271cac Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot.png new file mode 100644 index 0000000..5e3ebe7 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-hot.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qing-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-s.png new file mode 100644 index 0000000..322c9c1 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qing-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qing.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qing.png new file mode 100644 index 0000000..732fb76 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qing.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun-s.png new file mode 100644 index 0000000..83860d4 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun.png b/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun.png new file mode 100644 index 0000000..f578ff0 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/qingduoyun.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu-s.png new file mode 100644 index 0000000..1279320 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu.png b/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu.png new file mode 100644 index 0000000..0563aea Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/taiyangyu.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/wu-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/wu-s.png new file mode 100644 index 0000000..1919189 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/wu-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/wu.png b/blossom-editor/src/renderer/src/assets/imgs/weather/wu.png new file mode 100644 index 0000000..b6d213e Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/wu.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/xue-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/xue-s.png new file mode 100644 index 0000000..31e4d03 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/xue-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/xue.png b/blossom-editor/src/renderer/src/assets/imgs/weather/xue.png new file mode 100644 index 0000000..3b8c21c Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/xue.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yin-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yin-s.png new file mode 100644 index 0000000..c46cc52 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yin-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yin.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yin.png new file mode 100644 index 0000000..f6c802c Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yin.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yu-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yu-s.png new file mode 100644 index 0000000..e097980 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yu-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yu.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yu.png new file mode 100644 index 0000000..fd11ed7 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yu.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yun-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yun-s.png new file mode 100644 index 0000000..e9a93c8 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yun-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/yun.png b/blossom-editor/src/renderer/src/assets/imgs/weather/yun.png new file mode 100644 index 0000000..e9983ed Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/yun.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu-s.png b/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu-s.png new file mode 100644 index 0000000..4346e86 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu-s.png differ diff --git a/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu.png b/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu.png new file mode 100644 index 0000000..e734dc2 Binary files /dev/null and b/blossom-editor/src/renderer/src/assets/imgs/weather/zhongyu.png differ diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-image.scss b/blossom-editor/src/renderer/src/assets/styles/bl-image.scss new file mode 100644 index 0000000..2107f91 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-image.scss @@ -0,0 +1,5 @@ +.el-image { + .el-image__inner { + pointer-events: all; + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-loading-spinner.scss b/blossom-editor/src/renderer/src/assets/styles/bl-loading-spinner.scss new file mode 100644 index 0000000..4234737 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-loading-spinner.scss @@ -0,0 +1,27 @@ +.bl-loading-spinner { + @include flex(column, flex-end); + height: 100px; + top: calc(50% - 50px); + + &::before { + background-image: url('@renderer/assets/imgs/blossom_logo_grey.png'); + background-position: center; + background-repeat: no-repeat; + background-size: 50px 50px; + height: 100%; + width: 100%; + content: ''; + position: absolute; + top: 0; + left: 0; + filter: blur(5px); + } + + .circular { + display: none; + } + + .el-loading-text { + @include font(15px, 300); + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-message.scss b/blossom-editor/src/renderer/src/assets/styles/bl-message.scss new file mode 100644 index 0000000..e292da2 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-message.scss @@ -0,0 +1,11 @@ +.el-message { + @include themeShadow(3px 3px 10px 1px rgb(191, 191, 191), 3px 3px 10px 1px #000); + padding: 10px 10px !important; + border: 3px; +} + +.el-message--success { + --el-message-bg-color: var(--xz-bg-color) !important; + border: 0 !important; + // border: 2px solid rgb(51, 217, 0) !important; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-notification.scss b/blossom-editor/src/renderer/src/assets/styles/bl-notification.scss new file mode 100644 index 0000000..e349d00 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-notification.scss @@ -0,0 +1,5 @@ +.el-notification { + word-wrap: break-word; + word-break: break-all; + // border-radius: 0px !important; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-preview-toc.scss b/blossom-editor/src/renderer/src/assets/styles/bl-preview-toc.scss new file mode 100644 index 0000000..13cf76d --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-preview-toc.scss @@ -0,0 +1,179 @@ +// 块的目录, 用在新窗口中 +.bl-preview-toc-block { + @include box(400px, 100%); + color: #5E5E5E; + border-right: 3px solid var(--bl-preview-toc-border-color); + + .toc-title { + @include box(100%, 50px); + @include font(30px, 700); + border-top: 3px solid var(--bl-preview-toc-border-color); + margin-top: 10px; + padding: 10px 0; + } + + .toc-subtitle { + @include box(100%, 20px); + @include font(12px); + color: #ABABAB; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + white-space: pre; + } + + .toc-content { + @include box(100%, calc(100% - 20px - 20px)); + @include font(14px); + + .toc-1 { + border-top: 2px solid var(--bl-preview-toc-border-color); + } + } +} + + +@media screen and (max-height: 800px) { + .bl-preview-toc-absolute { + max-height: 500px !important; + background-color: red; + + .toc-content { + max-height: calc(500px - 150px - 60px - 20px) !important; + } + } +} + +// 浮动的目录, 用在编辑器中 +.bl-preview-toc-absolute { + @include font(12px); + @include themeShadow(2px 4px 7px 2px rgba(49, 49, 49, 0.3), 2px 4px 7px 2px rgb(28, 28, 28)); + position: absolute; + top: 50px; + right: 50px; + background-color: #4D4D4DBD; + border-radius: 10px; + z-index: 1000; + max-height: 700px; + overflow: hidden; + + .toc-title { + @include box(100%, 20px); + @include font(15px, 700); + @include themeColor(#FFFFFF, #ffffff); + } + + .toc-content { + @include themeColor(#e2e2e2, #BCBCBC); + width: 100%; + max-height: calc(700px - 150px - 60px - 20px); + transition: 0.3s; + + .toc-1 { + @include themeBorder(2px, #a3a3a3, #a3a3a3, 'top'); + } + } +} + +.bl-preview-toc-block, +.bl-preview-toc-absolute { + padding: 10px; + transition: 0.3s; + + .toc-content { + overflow-y: scroll; + padding-top: 10px; + + .toc-1, + .toc-2, + .toc-3, + .toc-4, + .toc-5, + .toc-6 { + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + white-space: pre; + + &:hover { + font-weight: bold; + } + } + + .toc-1 { + margin-top: 5px; + padding-top: 5px; + + &:first-child { + margin: 0; + padding: 0; + border: 0; + } + } + + .toc-2 { + &::before { + content: ' '; + } + + } + + .toc-3 { + &::before { + content: ' '; + } + } + + .toc-4 { + &::before { + content: ' '; + } + } + + .toc-5 { + &::before { + content: ' '; + } + } + + .toc-6 { + &::before { + content: ' '; + } + } + } + + .img-title { + @extend .toc-title; + @include themeBorder(2px, #a3a3a3, #a3a3a3, 'top'); + margin-top: 20px; + height: 40px; + padding-top: 10px; + } + + .img-content { + @include box(100%, auto); + @include flex(row, flex-start, flex-start); + max-height: 150px; + align-content: flex-start; + flex-wrap: wrap; + overflow-y: scroll; + + .elimg { + @include flex(row, center, center); + @include box(50px, 50px); + border-radius: 4px; + object-fit: cover; + background-color: aliceblue; + margin: 3px; + filter: brightness(90%); + transition: 0.3s; + color: #a7a7a7; + + &:hover { + filter: brightness(100%); + } + } + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-preview.scss b/blossom-editor/src/renderer/src/assets/styles/bl-preview.scss new file mode 100644 index 0000000..6f076ec --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-preview.scss @@ -0,0 +1,356 @@ +/* + * 编辑器的预览页样式 + */ +.bl-preview { + color: var(--bl-preview-color); + background-color: var(--bl-preview-bg-color); + + a { + color: var(--el-color-primary); + font-weight: bold; + } + + a.inner-link { + // text-decoration: underline; + border-bottom: 2px dashed #858585; + box-sizing: border-box; + padding: 0 4px; + text-decoration: none; + } + + img { + border-radius: var(--bl-preview-border-radius); + max-width: 100%; + + [class="dark"] & { + filter: brightness(80%); + } + } + + // 列表 + h1 { + // padding: 10px; + margin-top: 70px; + padding: 10px 0; + border-bottom: 3px solid var(--bl-preview-h1-bg-color); + // border-radius: var(--bl-preview-border-radius); + // background-color: var(--bl-preview-h1-bg-color); + // box-shadow: var(--bl-preview-h1-box-shadow); + // text-shadow: var(--bl-preview-h1-text-shadow); + text-align: left; + position: relative; + } + + h2 { + font-size: 25px; + } + + h3 { + font-size: 22px; + } + + h4 { + font-size: 19px; + } + + h5, + h6 { + font-size: 16px; + } + + li::marker { + color: #989898; + } + + // 有序列表 + ol { + padding-left: 30px; + } + + // 无序列表 + ul { + padding-left: 20px; + + ul { + padding-left: 20px; + } + } + + // checkbox + ul:has(input) { + padding-left: 0px; + + ul { + padding-left: 15px; + + } + + li::marker { + content: none; + } + } + + hr { + border-color: var(--el-color-primary-light-7); + } + + // 表格 + table { + border: 1px solid var(--bl-preview-table-border-color); + box-sizing: border-box; + padding: 0; + border-spacing: 0; + margin: 10px 0; + max-width: 100%; + table-layout: fixed; + width: 100%; + + thead { + background-color: var(--bl-preview-table-thead-bg-color); + color: #d4d4d4; + + tr { + th { + font-size: 16px; + padding: 10px; + border-right: 1px solid var(--bl-preview-table-border-color); + } + + th:last-child { + border: 0; + } + } + } + + tbody { + tr { + td { + padding: 5px; + border-right: 1px solid var(--bl-preview-table-border-color); + border-bottom: 1px solid var(--bl-preview-table-border-color); + word-wrap: break-word; + width: auto; + // max-width: 200px; + } + + td:last-child { + border-right: 0; + } + } + + tr:last-child { + td { + border-bottom: 0; + } + } + } + } + + .bl-table-container { + border: 0; + + thead { + display: none; + } + + tbody { + td { + border: 0; + } + } + } + + // 引用 + blockquote { + padding: 1px 10px; + margin: 10px 0; + color: var(--bl-preview-blockquote-color); + border-left: 3px solid var(--bl-preview-blockquote-border-color); + border-radius: var(--bl-preview-border-radius); + background-color: var(--bl-preview-blockquote-bg-color); + + blockquote { + // border: 1px solid #dedede; + // border-left: 3px solid #bebebe; + } + } + + .bl-blockquote-green { + background-color: var(--bl-preview-blockquote-bg-green); + border-left: 3px solid var(--bl-preview-blockquote-border-green); + } + + .bl-blockquote-yellow { + background-color: var(--bl-preview-blockquote-bg-yellow); + border-left: 3px solid var(--bl-preview-blockquote-border-yellow); + } + + .bl-blockquote-red { + background-color: var(--bl-preview-blockquote-bg-red); + border-left: 3px solid var(--bl-preview-blockquote-border-red); + } + + .bl-blockquote-blue { + background-color: var(--bl-preview-blockquote-bg-blue); + border-left: 3px solid var(--bl-preview-blockquote-border-blue); + } + + .bl-blockquote-purple { + background-color: var(--bl-preview-blockquote-bg-purple); + border-left: 3px solid var(--bl-preview-blockquote-border-purple); + } + + .bl-blockquote-black { + background-color: var(--bl-preview-blockquote-bg-black); + border-left: 3px solid var(--bl-preview-blockquote-border-black); + } + + // 单行代码块 + code { + background-color: var(--bl-preview-code-bg-color); + padding: 0px 4px; + border-radius: 3px; + margin: 0 5px; + font-size: 13px; + } + + // 代码块 + pre { + padding: 10px 10px 10px 10px; + background-color: var(--bl-preview-pre-bg-color); + overflow: scroll; + border-radius: var(--bl-preview-border-radius); + box-shadow: inset 0 0 5px #000, 2px 2px 10px 1px #000; + font-size: 13px; + + code { + background-color: inherit; + padding: 0; + border-radius: 0; + margin: 0; + font-size: 13px; + } + + /*! + Theme: Darcula + Author: jetbrains + License: ~ MIT (or more permissive) [via base16-schemes-source] + Maintainer: @highlightjs/core-team + Version: 2021.09.0 + */ + pre code.hljs { + display: block; + overflow-x: auto; + // padding: 1em + } + + code.hljs { + text-shadow: none; + // padding: 3px 5px + } + + .hljs { + color: #a9b7c6; + background: var(--bl-preview-pre-bg-color); + } + + .hljs ::selection, + .hljs::selection { + background-color: #323232; + color: #a9b7c6 + } + + .hljs-comment { + color: #606366 + } + + .hljs-tag { + color: #a4a3a3 + } + + .hljs-operator, + .hljs-punctuation, + .hljs-subst { + color: #a9b7c6 + } + + .hljs-operator { + opacity: .7 + } + + .hljs-bullet, + .hljs-deletion, + .hljs-name, + .hljs-selector-tag, + .hljs-template-variable, + .hljs-variable { + color: #4eade5 + } + + .hljs-attr, + .hljs-link, + .hljs-literal, + .hljs-number, + .hljs-symbol, + .hljs-variable.constant_ { + color: #689757 + } + + .hljs-class .hljs-title, + .hljs-title, + .hljs-title.class_ { + color: #e4b568 + } + + .hljs-strong { + font-weight: 700; + color: #bbb529 + } + + .hljs-addition, + .hljs-code, + .hljs-string, + .hljs-title.class_.inherited__ { + color: #6a8759 + } + + .hljs-built_in, + .hljs-doctag, + .hljs-keyword.hljs-atrule, + .hljs-quote, + .hljs-regexp { + color: #629755 + } + + .hljs-attribute, + .hljs-function .hljs-title, + .hljs-section, + .hljs-title.function_, + .ruby .hljs-property { + color: #9876aa + } + + .diff .hljs-meta, + .hljs-keyword, + .hljs-template-tag, + .hljs-type { + color: #cc7832 + } + + .hljs-emphasis { + color: #cc7832; + font-style: italic + } + + .hljs-meta, + .hljs-meta .hljs-keyword, + .hljs-meta .hljs-string { + color: #b4b428; + } + + .hljs-meta .hljs-keyword, + .hljs-meta-keyword { + font-weight: 700 + } + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-tip.scss b/blossom-editor/src/renderer/src/assets/styles/bl-tip.scss new file mode 100644 index 0000000..76a547b --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-tip.scss @@ -0,0 +1,61 @@ +.bl-tip { + display: inline-block; + position: relative; + + &:before, + &:after { + opacity: 0; + visibility: hidden; + position: absolute; + z-index: 1000; + transition: 0.1s ease; + } + + &:before { + content: ''; + border: 6px solid transparent; + } + + &:after { + @include themeShadow(4px 4px 5px #00000080, 5px 5px 5px #000000); + content: attr(data-tip); + color: #fff; + background-color: var(--el-color-primary); + text-shadow: 3px 3px 5px #00000080; + padding: 5px; + white-space: nowrap; + border-radius: 4px; + } + + &:hover:before, + &:hover:after { + opacity: 1; + /*鼠标放上时透明度为完全显示*/ + visibility: visible; + z-index: 1000; + } + + /*bottom*/ + &-bottom:before { + top: 100%; + left: 25%; + margin-left: 10px; + margin-top: -9px; + border-bottom-color: var(--el-color-primary); + } + + &-bottom:after { + top: 100%; + left: 20%; + margin-left: -10px; + margin-top: 3px; + } + + &-bottom:hover:before { + margin-top: -3px; + } + + &-bottom:hover:after { + margin-top: 9px; + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/bl-tooltip.scss b/blossom-editor/src/renderer/src/assets/styles/bl-tooltip.scss new file mode 100644 index 0000000..e13f886 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/bl-tooltip.scss @@ -0,0 +1,45 @@ +// placement = top +.el-popper.is-blossomt { + @extend .is-blossomb; +} + +.el-popper.is-blossomt .el-popper__arrow::before { + @include themeBg(linear-gradient(0deg, #9fa5e4, #87abcf), linear-gradient(90deg, #899911, #a38c51)); + right: 0; +} + +// placement = bottom +.el-popper.is-blossomb { + @include themeBg(linear-gradient(90deg, #ad8cf2, #7fbeff), linear-gradient(90deg, #899911, #B98B17)); + @include themeText(2px 4px 5px rgba(107, 104, 104, 1), 2px 4px 5px rgb(62, 62, 62)); + @include themeShadow(2px 3px 7px 2px rgba(49, 49, 49, 0.3), 2px 3px 7px 2px rgb(0, 0, 0)); + color: #fff; + font-weight: bold; + padding: 6px 12px; +} + +.el-popper.is-blossomb .el-popper__arrow::before { + @include themeBg(linear-gradient(90deg, #9fa5e4, #87abcf), linear-gradient(90deg, #899911, #a38c51)); + right: 0; +} + +// placement = right + +.el-popper.is-blossomr { + @extend .is-blossomb; +} + +.el-popper.is-blossomr .el-popper__arrow::before { + @include themeBg(linear-gradient(0deg, #ad8cf2, #ad8cf2), linear-gradient(90deg, #899911, #899911)); + right: 0; +} + + +.el-popper.is-light { + color: var(--el-text-color-primary); + box-shadow: 3px 3px 10px 1px #848484; + + .keyboard { + margin-top: 5px; + } +} diff --git a/blossom-editor/src/renderer/src/assets/styles/common.scss b/blossom-editor/src/renderer/src/assets/styles/common.scss new file mode 100644 index 0000000..c4db8d4 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/common.scss @@ -0,0 +1,12 @@ +.keyboard { + display: inline-block; + padding: 0 5px; + margin-bottom: 10px; + color: #666; + border: 2px solid #e4e4e4; + border-radius: 4px; + background: #f1f1f1; + box-shadow: inset 0 0 20px white, 0 5px 0 #b1b1b1, 0 6px 0 1px #7e7e7e, 0 8px 5px #a5a5a5; + text-transform: uppercase; + text-align: center; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/config.scss b/blossom-editor/src/renderer/src/assets/styles/config.scss new file mode 100644 index 0000000..0050028 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/config.scss @@ -0,0 +1,16 @@ +@import '../fonts/config.scss'; + +@import './mixin-fuc.scss'; +@import './common.scss'; + +// bl 样式 +@import './bl-image.scss'; +@import './bl-loading-spinner.scss'; +@import './bl-message.scss'; +@import './bl-notification.scss'; +@import './bl-preview-toc.scss'; +@import './bl-preview.scss'; +@import './bl-tip.scss'; +@import './bl-tooltip.scss'; + +@import './el-tree.scss'; \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/el-tree.scss b/blossom-editor/src/renderer/src/assets/styles/el-tree.scss new file mode 100644 index 0000000..3fced93 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/el-tree.scss @@ -0,0 +1,9 @@ + + +.el-tree { + .el-tree-node { + .el-tree-node__content { + height: 20px; + } + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/styles/mixin-fuc.scss b/blossom-editor/src/renderer/src/assets/styles/mixin-fuc.scss new file mode 100644 index 0000000..a4504c1 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/styles/mixin-fuc.scss @@ -0,0 +1,193 @@ +// 盒子基础设置 +@mixin box($w: 100%, $h: 100%, $minW: null, $maxW: null, $minH: null, $maxH: null, ) { + @if $w { + width: $w; + } + + @if $h { + height: $h; + } + + @if $minW { + min-width: $minW; + } + + @if $maxW { + max-width: $maxW; + } + + @if $minH { + min-height: $minH; + } + + @if $maxH { + max-height: $maxH; + } +} + +/** + * 字体基础属性 + * fz : 字体大小 + * fw : 字体粗细 + */ +@mixin font($fz, $fw: null, $fm: null) { + font-size: $fz; + + @if $fw { + font-weight: $fw; + } + + @if $fm { + font-family: $fm; + } +} + +/** + * 边框 + */ +@mixin border($borderWidth, $borderColor, $radio: 0) { + border: $borderWidth solid; + border-radius: $radio; + + @if $borderColor { + border-color: $borderColor; + } +} + +/** + * flex布局属性 + * dir : 对齐方式 + * main : 主轴 + * cross : 交叉轴 + */ +@mixin flex($dir: row, $justifyContent: flex-start, $alignItem: stretch) { + display: flex; + flex-direction: $dir; + justify-content: $justifyContent; + align-items: $alignItem; +} + +@mixin absolute($top: null, $right: null, $bottom: null, $left: null) { + position: absolute; + + @if $top { + top: $top; + } + + @if $right { + right: $right; + } + + @if $bottom { + bottom: $bottom; + } + + @if $left { + left: $left; + } +} + +@mixin ellipsis() { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/** + + */ +@mixin color-logo ($colorA, $colorB) { + color: transparent; + font-family: current, sans-serif; + margin-top: 14px; + letter-spacing: 1px; + background: linear-gradient(90deg, + $colorA, + $colorB, + $colorA ); + -webkit-background-clip: text; + animation: glow 10s linear infinite; + transition: 1.5s; + background-size: 300%; + + @keyframes glow { + to { + background-position: -300%; + } + } +} + +/* + 主题控制 +*/ + +@mixin themeShadow ($light-color: 0 0 12px 0 #7c7c7c, $dark-color: 0 0 12px 0 #000000) { + box-shadow: $light-color; + + [class="dark"] & { + box-shadow: $dark-color; + } +} + +@mixin themeText ($light-color: 0 0 12px 0 #7c7c7c, $dark-color: 0 0 12px 0 #000000) { + text-shadow: $light-color; + + [class="dark"] & { + text-shadow: $dark-color; + } +} + +@mixin themeColor ($light-color: #ffffff, $dark-color: #000000) { + color: $light-color; + + [class="dark"] & { + color: $dark-color; + } +} + +@mixin themeBg ($light-color: #ffffff, $dark-color: #000000) { + background: $light-color; + + [class="dark"] & { + background: $dark-color; + } +} + +@mixin themeBorder ($w, $light-color: #ffffff, $dark-color: #000000, $location: 'around', $radius: '0', $style: solid) { + @if str_index($location, 'top') { + border-top: $w $style; + } + + @if str_index($location, 'right') { + border-right: $w $style; + } + + @if str_index($location, 'bottom') { + border-bottom: $w $style; + } + + @if str_index($location, 'left') { + border-left: $w $style; + } + + @if str_index($location, 'around') { + border: $w $style; + } + + border-color: $light-color; + + [class="dark"] & { + border-color: $dark-color; + } + + border-radius: $radius; + +} + +@mixin themeBrightness($light-brightness: 100%, $dark-brightness: 80%) { + filter: brightness($light-brightness); + + [class="dark"] & { + filter: brightness($dark-brightness); + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/ShortcutRegister.ts b/blossom-editor/src/renderer/src/assets/utils/ShortcutRegister.ts new file mode 100644 index 0000000..dcc4b70 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/ShortcutRegister.ts @@ -0,0 +1,124 @@ +import { isMap, isUndefined } from "lodash" + +export type shortcutFunc = () => void + +// 以下快捷键不会参与到监听中 +const ignoreCodes: string[] = ['Backspace', 'Enter', 'MetaLeft', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] + + +/** + * 局部快捷键注册 + */ +class ShortcutRegistrant { + shortcutMap: Map>> = new Map() + downIndex: number = 0 + downCodes: string[] = (['', '', '']) + debug: boolean = false + + public setDebug(debug: boolean): ShortcutRegistrant { + this.debug = debug + return this + } + + /** + * 注册快捷键 + * @param key 快捷键的开始前缀 + * @param value 快捷键对应的值, 可以是嵌套的快捷键key, 也可以是一个函数 {@link shortcutFunc}, 如果是函数, 则执行函数中的内容 + */ + public register(key: string, value: Map>): void { + this.shortcutMap.set(key, value) + } + + /** + * 清空按下的记录 + *

    例如在按下一个键后切出到其他应用(比如使用 Alt + Tab 键切换), 由于 Alt 键是在应用内下, 随后切换到了其他窗口, 此时 + * 无法获得 Alt 键的松开事件, 导致切回后按下 1 键, 会认为按下的是 alt + 1 键, 并且 alt 永远都不会被松开, 导致快捷键出 + * 现问题. + *

    所以需要在离开应用窗口后删除记录按下的快捷键, 可以使用: + *

    +   * window.onblur = () => {
    +   *   console.log('没有被激活')
    +   *   shortcutRegistrant.clearDownCodes()
    +   * }
    +   * 
    + */ + public clearDownCodes(): void { + this.downCodes[0] = '' + this.downCodes[1] = '' + this.downCodes[2] = '' + this.downIndex = 0 + } + + /** + * 键盘松开事件 + * @param event + */ + public keyup(event: KeyboardEvent): void { + if (this.debug) { + console.log(event) + let row1 = { col1: '动作', col2: '松开' } + let row2 = { col1: 'key', col2: event.key } + let row3 = { col1: 'code', col2: event.code } + console.table([row1, row2, row3]) + } + + let code: string = event.code + let upCodeIndex: number = this.downCodes.indexOf(code) + + if (upCodeIndex !== -1) { + this.downCodes[upCodeIndex] = '' + this.downIndex-- + } + } + + /** + * 键盘按下事件 + * @param event + */ + public keydown(event: KeyboardEvent): void { + if (ignoreCodes.indexOf(event.code) > -1) { + return + } + if (this.downIndex >= 3) { + return + } + let code: string = event.code + if (this.downCodes.indexOf(code) != -1) { + return + } + let lastdown = this.downCodes[this.downIndex] + if (lastdown === '') { + this.downCodes[this.downIndex] = code + this.downIndex++ + // 如果命中快捷键, 则阻止浏览器默认行为 + if (this.choiceShortcut(this.downCodes)) { + event.preventDefault() + } + } + } + + private choiceShortcut(codeArray: string[]): boolean { + let underfinedOrMap: undefined | Map> = this.shortcutMap.get(codeArray[0]) + if (isUndefined(underfinedOrMap)) { + return false + } + let underfinedOrMap2: undefined | shortcutFunc | Map = underfinedOrMap.get(codeArray[1]) + if (isUndefined(underfinedOrMap2)) { + return false + } + if (isMap(underfinedOrMap2)) { + let underfinedOrMap3: undefined | shortcutFunc = underfinedOrMap2.get(codeArray[2]) + if (isUndefined(underfinedOrMap3)) { + return false + } + underfinedOrMap3() + return true + } + + underfinedOrMap2() + return true + } + +} + +export default ShortcutRegistrant \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/electron.ts b/blossom-editor/src/renderer/src/assets/utils/electron.ts new file mode 100644 index 0000000..47f2220 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/electron.ts @@ -0,0 +1,66 @@ +// @ts-nocheck +/** 打开控制台, 会在打开和关闭之间切换 */ +export const openDevTools = () => { window.electronAPI.openDevTools() } + +export const windowMin = () => { window.electronAPI.windowMin() } +/** 会在最大化和还原之间切换 */ +export const windowMax = () => { window.electronAPI.windowMax() } +export const windowHide = () => { window.electronAPI.windowHide() } +export const windowClose = () => { window.electronAPI.windowClose() } + +/** + * 设置用户信息 + * @param userinfo 用户信息 + */ +export const setUserinfo = (userinfo: any) => { window.electronAPI.setUserinfo(userinfo) } + +/** + * 打开新图标窗口 + */ +export const openNewIconWindow = () => { window.electronAPI.openNewIconWindow() } + +/** + * 打开文章引用窗口 + */ +export const openNewArticleReferenceWindow = () => { window.electronAPI.openNewArticleReferenceWindow() } + +/** + * 使用默认浏览器打开链接 + * @param url 访问链接 + */ +export const openExtenal = (url: string): void => { window.electronAPI.openExtenal(url) } + +/** + * 截屏 + */ +export const printScreen = (): void => { window.electronAPI.printScreen() } + +/** + * 剪切板, 读取图片, 并转换成 base64 字符串 + * @returns base64 字符串 + */ +export const readImageToDataUrl = (): string => { return window.electronAPI.readImageToDataUrl('clipboard') } + +/** + * 剪切板, 读取图片, 并转换成 png 图片 buffer + * @returns png 图片 buffer + */ +export const readImageToPNG = (): string => { return window.electronAPI.readImageToPNG('clipboard') } + +/** + * 剪切板, 读取图片, 并转换成 jpeg 图片 buffer + * @returns jpeg 图片 buffer + */ +export const readImageToJPEG = (): string => { return window.electronAPI.readImageToJPEG('clipboard') } + +/** + * 向剪切板写入文本 + * @param text 文本内容 + */ +export const writeText = (text: string): void => { window.electronAPI.writeText(text, 'clipboard') } + +/** + * 下载 + * @param url 文件路径 + */ +export const download = (url: string): void => { window.electronAPI.download(url) } \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/formatTable.ts b/blossom-editor/src/renderer/src/assets/utils/formatTable.ts new file mode 100644 index 0000000..861a0a8 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/formatTable.ts @@ -0,0 +1,146 @@ + + + +export const formartMarkdownTable = (str: string) => { + var table = splitStringToTable(str), + alignments: string[], + max_length_per_column: any[]; + + table[1] = table[1].map(function (cell) { + return padHeaderSeparatorString(cell, 0); + }); + + fillInMissingColumns(table); + + alignments = table[1].map(getAlignment); + max_length_per_column = getMaxLengthPerColumn(table); + + return table.map(function (row, row_index) { + return '|' + row.map(function (cell, column_index) { + var column_length = max_length_per_column[column_index]; + if (row_index === 1) { + return padHeaderSeparatorString(cell, column_length + 2); + } + return ' ' + padStringWithAlignment(cell, column_length, alignments[column_index]) + ' '; + }).join('|') + '|'; + }).join('\n') + '\n'; +} + + + +function splitStringToTable(str: string) { + return trim(String(str)).split('\n').map(function (row) { + row = row.replace(/^\s*\|/, ''); + row = row.replace(/\|\s*$/, ''); + return row.split('|').map(trim); + }); +} + + +function getMaxLengthPerColumn(table: any[]) { + return table[0].map(function (_str: any, column_index: string | number) { + return getMaxLengthOfColumn(getColumn(table, column_index)); + }); +} + + +function getMaxLengthOfColumn(array: any[]) { + return array.reduce(function (max: number, item: any) { + return Math.max(max, getItemLength(item)); + }, 0); +} + + +function getItemLength(str: string) { + var len = 0, i: number; + for (i = 0; i < str.length; i++) { + len += (str.charCodeAt(i) >= 0 && str.charCodeAt(i) <= 128) ? 1 : 2; + } + return len; +} + + +function getMaxLength(array: any[]) { + return array.reduce(function (max: number, item: string | any[]) { + return Math.max(max, item.length); + }, 0); +} + + +function padHeaderSeparatorString(str: string, len: number) { + switch (getAlignment(str)) { + case 'l': return repeatStr('-', Math.max(3, len)); + case 'c': return ':' + repeatStr('-', Math.max(3, len - 2)) + ':'; + case 'r': return repeatStr('-', Math.max(3, len - 1)) + ':'; + } +} + + +function getAlignment(str: string | any[]) { + if (str[str.length - 1] === ':') { + return str[0] === ':' ? 'c' : 'r'; + } + return 'l'; +} + + +function fillInMissingColumns(table: any[]) { + var max = getMaxLength(table); + table.forEach(function (row: string[], i: number) { + while (row.length < max) { + row.push(i === 1 ? '---' : ''); + } + }); +} + + +function getColumn(table: any[], column_index: string | number) { + return table.map(function (row) { + return row[column_index]; + }); +} + + +function trim(str: string) { + return str.trim(); +} + + +function padStringWithAlignment(str: string, len: number, alignment: string): string { + switch (alignment) { + case 'l': return padRight(str, len); + case 'c': return padLeftAndRight(str, len); + case 'r': return padLeft(str, len); + } + return '' +} + + +function padLeft(str: string, len: number): string { + return getPadding(len - getItemLength(str)) + str; +} + + +function padRight(str: string, len: number): string { + return str + getPadding(len - getItemLength(str)); +} + + +function padLeftAndRight(str: string, len: number): string { + var l = (len - getItemLength(str)) / 2; + return getPadding(Math.ceil(l)) + str + getPadding(Math.floor(l)); +} + + +function getPadding(len: number): string { + return repeatStr(' ', len); +} + + +function repeatStr(str: string, count: number): string { + return count > 0 ? Array(count + 1).join(str) : ''; +} + +// console.log(formartMarkdownTable(`|张三|列22222| +// |---|---| +// |1|2|`)) \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/obj.ts b/blossom-editor/src/renderer/src/assets/utils/obj.ts new file mode 100644 index 0000000..2e2f8c8 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/obj.ts @@ -0,0 +1,51 @@ +export const isNull = (val: any): boolean => { + if (typeof val === 'boolean') { + return false + } + if (typeof val === 'number') { + return false + } + + // 控制 + if (val == null || val === 'undefined' || val === undefined || val === '') { + return true + } + + // 数组 + if (Array.isArray(val) && val.length === 0) { + return true + } + + // 对象, 但无字段 + if (val instanceof Object && JSON.stringify(val) === '{}') { + return true + } + return false +} + +export const isNotNull = (val: any): boolean => { + return !isNull(val) +} + +export const isBlank = (str: string | undefined | null): boolean => { + if (str === undefined) { + return true + } + if (str == null) { + return true + } + if (str == '') { + return true + } + if (str == ' ') { + return true + } + if (str.length == 0) { + return true + } + return false +} + +export const isNotBlank = (str: string | undefined | null): boolean => { + return !isBlank(str) +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/storage.ts b/blossom-editor/src/renderer/src/assets/utils/storage.ts new file mode 100644 index 0000000..312ab46 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/storage.ts @@ -0,0 +1,53 @@ +/** + * window.localStorage 浏览器永久缓存 + * @method set 设置永久缓存 + * @method get 获取永久缓存 + * @method remove 移除永久缓存 + * @method clear 移除全部永久缓存 + */ +export const Local = { + // 设置永久缓存 + set(key: string, val: any) { + window.localStorage.setItem(key, JSON.stringify(val)); + }, + // 获取永久缓存 + get(key: string): any { + let json = window.localStorage.getItem(key); + return JSON.parse(json); + }, + // 移除永久缓存 + remove(key: string) { + window.localStorage.removeItem(key); + }, + // 移除全部永久缓存 + clear() { + window.localStorage.clear(); + }, +}; + +/** + * window.sessionStorage 浏览器临时缓存 + * @method set 设置临时缓存 + * @method get 获取临时缓存 + * @method remove 移除临时缓存 + * @method clear 移除全部临时缓存 + */ +export const Session = { + // 设置临时缓存 + set(key: string, val: any) { + window.sessionStorage.setItem(key, JSON.stringify(val)); + }, + // 获取临时缓存 + get(key: string) { + let json = window.sessionStorage.getItem(key); + return JSON.parse(json); + }, + // 移除临时缓存 + remove(key: string) { + window.sessionStorage.removeItem(key); + }, + // 移除全部临时缓存 + clear() { + window.sessionStorage.clear(); + }, +}; \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/assets/utils/util.ts b/blossom-editor/src/renderer/src/assets/utils/util.ts new file mode 100644 index 0000000..45870f5 --- /dev/null +++ b/blossom-editor/src/renderer/src/assets/utils/util.ts @@ -0,0 +1,368 @@ +import { isNull } from "./obj" + +/** + * 跳转新开页面 + * @param url 地址 + */ +export const toView = (url: string): void => { + window.open(url) +} + +/** + * 获取当前时间的 yyyyMMdd_HHmmss + * @returns + */ +export const getNowTime = (): string => { + const now = new Date() + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + return `${y}${m}${d}_${h}${min}${s}` +} + +/** + * 获取当前时间的 yyyy-MM-dd HH:mm:ss + * @returns {string} + */ +export const getDateTimeFormat = (): string => { + const now = new Date() + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s; +} + +/** + * 获取下一天 + * @param date + * @param next + * @param format + * @returns + */ +export const getNextDay = (date: string, next: number = 1, format = '{y}-{m}-{d}'): string => { + if (!date) { + return '日期错误' + } + date = date.match(/\d+/g)!.join('-'); // 格式为2022年09月19日处理 + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + next); + + const formatObj = { + y: nextDay.getFullYear(), + m: nextDay.getMonth() + 1, + d: nextDay.getDate(), + }; + return format.replace(/{([ymd])+}/g, (_result, key) => { + const value = formatObj[key]; + return value.toString().padStart(2, '0'); + }); +} + +export const getDateZh = (): string => { + const now = new Date() + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + return `${y}年${m}月${d}日` +} + + +/** + * 获取当前处于何时 + */ +export const nowWhen = (): string => { + let nowTime: number = new Date().getHours(); + if (0 <= nowTime && nowTime < 7) { + return 'Dawning' + } + if (7 <= nowTime && nowTime < 12) { + return 'Morning' + } + if (12 <= nowTime && nowTime < 19) { + return 'Afternoon' + } + if (19 <= nowTime && nowTime < 24) { + return 'Evening' + } + return 'Night' +} + +/** + * 将 秒级时间戳 转为 yyyy-MM-dd HH:mm:ss + * @param seconds 秒级时间戳 + */ +export const secondsToDatetime = (seconds: number | string | Date): string => { + const now = new Date(Number(seconds) * 1000) + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s; +} + +/** + * 将 毫秒时间戳 转为 yyyy-MM-dd HH:mm:ss.sss + * @param timestamp 毫秒时间戳 + */ +export const timestampToDatetime = (timestamp: number | string | Date): string => { + const now = new Date(timestamp) + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + let SSS = formatNum(now.getMilliseconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s + '.' + SSS; +} + +const formatNum = (num: number) => { + if (num < 10) { + return '0' + num + } + return num +}; + +/** + * 格式化JSON字符串 + * @param msg + * @param customRetract 缩进 + * @returns {string} + */ +export const formatJson = (msg: string, customRetract?: string): string => { + // 格式化缩进为2个空格 + const retract = isNull(customRetract) ? ' ' : customRetract; + let rep = '~'; + let jsonStr = JSON.stringify(msg, null, rep); + let str = ''; + for (let i = 0; i < jsonStr.length; i++) { + let text2 = jsonStr.charAt(i); + if (i > 1) { + let text = jsonStr.charAt(i - 1); + if (rep !== text && rep === text2) { + // str += '\n' + } + } + str += text2 + } + jsonStr = ''; + for (let i = 0; i < str.length; i++) { + let text = str.charAt(i); + if (rep === text) { + jsonStr += retract; + } else { + jsonStr += text + } + if (i === str.length - 2) { + jsonStr += '\n' + } + } + return jsonStr; +}; + +/** + * 格式化文件大小 + * @param size + * @returns + */ +export const formatFileSize = (size: number): string => { + if (!size) + return ""; + + var num = 1024.00; //byte + + if (size < num) + return size + "B"; + if (size < Math.pow(num, 2)) + return (size / num).toFixed(2) + "KB"; //kb + if (size < Math.pow(num, 3)) + return (size / Math.pow(num, 2)).toFixed(2) + "MB"; //M + if (size < Math.pow(num, 4)) + return (size / Math.pow(num, 3)).toFixed(2) + "G"; //G + return (size / Math.pow(num, 4)).toFixed(2) + "T"; //T +} + +/** + * 将数字转为千分位的字符串 + * @param param + * @returns + */ +export const formartNumber = (param: number): string => { + let num: string = (param || 0).toString(), result: string = ''; + while (num.length > 3) { + result = ',' + num.slice(-3) + result; + num = num.slice(0, num.length - 3); + } + if (num) { result = num + result; } + return result; +} + +export const randomBoolean = (): boolean => { + return Math.random() >= 0.5 +} + +randomBoolean() + +export const getSolar = (): string => { + let today = new Date(); + let solar = getSolarTermByYearMonthDay(today.getMonth() + 1, today.getDate()); + // console.log('当前日期[%s-%s]的最后一次节气: %s', today.getMonth() + 1, today.getDate(), solar); + return solar; +} + +export const getSolarTermByYearMonthDay = (month: number, day: any): string => { + // return '冬至'; + if (month == 1) { + if (day >= 5) return "小寒"; + if (day >= 19) return "大寒" + } + if (month == 2) { + if (day < 3) return "大寒"; + if (day >= 3) return "立春"; + if (day >= 18) return "雨水" + } + if (month == 3) { + if (day < 5) return "雨水"; + if (day >= 5) return "惊蛰"; + if (day >= 20) return "春分" + } + if (month == 4) { + if (day < 4) return "春分"; + if (day >= 4) return "清明"; + if (day >= 19) return "谷雨" + } + if (month == 5) { + if (day < 5) return "谷雨"; + if (day >= 5) return "立夏"; + if (day >= 20) return "小满" + } + if (month == 6) { + if (day < 5) return "小满"; + if (day >= 5) return "芒种"; + if (day >= 21) return "夏至" + } + if (month == 7) { + if (day < 7) return "夏至"; + if (day >= 7) return "小暑"; + if (day >= 22) return "大暑" + } + if (month == 8) { + if (day < 6) return "大暑"; + if (day >= 6) return "立秋"; + if (day >= 22) return "处暑" + } + if (month == 9) { + if (day < 7) return "处暑"; + if (day >= 7) return "白露"; + if (day >= 22) return "秋分" + } + if (month == 10) { + if (day < 7) return "秋分"; + if (day >= 7) return "寒露"; + if (day >= 23) return "霜降" + } + if (month == 11) { + if (day < 7) return "霜降"; + if (day >= 7) return "立冬"; + if (day >= 22) return "小雪" + } + if (month == 12) { + if (day < 5) return "小雪"; + if (day >= 5) return "大雪"; + if (day >= 21) return "冬至" + } + return ''; +} + +export interface SolarColor { + firstColor: string; + firstColorFlip: string; + secondColor: string; + secondColorFlip: string, +} + +export const getSolorColor = (): SolarColor => { + const currentSolar = getSolar(); + + if (currentSolar == '小寒') { + return { firstColor: '#283233', secondColor: '#973131', firstColorFlip: '#42494a', secondColorFlip: '#a65050' } + } + if (currentSolar == '大寒') { + return { firstColor: '#c6c0ba', secondColor: '#576975', firstColorFlip: '#b5b0aa', secondColorFlip: '#4c5c66' } + } + if (currentSolar == '立春') { + return { firstColor: '#f6da8c', secondColor: '#c4beb9', firstColorFlip: '#e6cb83', secondColorFlip: '#ada8a4' } + } + if (currentSolar == '雨水') { + return { firstColor: '#ccaf9b', secondColor: '#463936', firstColorFlip: '#b59b8a', secondColorFlip: '#a68e7e' } + } + if (currentSolar == '惊蛰') { + return { firstColor: '#c9c1ba', secondColor: '#508e69', firstColorFlip: '#a9c2b3', secondColorFlip: '#c9c1ba' } + } + if (currentSolar == '春分') { + return { firstColor: '#c3bfba', secondColor: '#f5d791', firstColorFlip: '#d9c793', secondColorFlip: '#d6d2cc' } + } + if (currentSolar == '清明') { + return { firstColor: '#ded2c3', secondColor: '#b7bc4b', firstColorFlip: '#c1c2a1', secondColorFlip: '#f4e7d7' } + } + if (currentSolar == '谷雨') { + return { firstColor: '#d3e0ea', secondColor: '#529e43', firstColorFlip: '#b6d4b0', secondColorFlip: '#d3e0ea' } + } + if (currentSolar == '立夏') { + return { firstColor: '#efd58e', secondColor: '#6cc1ff', firstColorFlip: '#f7e7ba', secondColorFlip: '#83cbff' } + } + if (currentSolar == '小满') { + return { firstColor: '#d19c46', secondColor: '#53915a', firstColorFlip: '#edaf54', secondColorFlip: '#50a356' } + } + if (currentSolar == '芒种') { + return { firstColor: '#87c0f9', secondColor: '#b08738', firstColorFlip: '#7294ad', secondColorFlip: '#edaf54' } + } + if (currentSolar == '夏至') { + return { firstColor: '#dfeaf2', secondColor: '#8ed2b1', firstColorFlip: '#a2cfe5', secondColorFlip: '#b8d3da' } + } + if (currentSolar == '小暑') { + return { firstColor: '#60d7a0', secondColor: '#83bd6f', firstColorFlip: '#83bd6f', secondColorFlip: '#65ab63' } + } + if (currentSolar == '大暑') { + return { firstColor: '#05aae4', secondColor: '#9de7ed', firstColorFlip: '#3c9ee8', secondColorFlip: '#96e0e2' } + } + if (currentSolar == '立秋') { + return { firstColor: '#dbbc30', secondColor: '#a08373', firstColorFlip: '#fcd42b', secondColorFlip: '#885629' } + } + if (currentSolar == '处暑') { + return { firstColor: '#b6e2e3', secondColor: '#8ed2af', firstColorFlip: '#8dd0d1', secondColorFlip: '#35b578' } + } + if (currentSolar == '白露') { + return { firstColor: '#bdd8a5', secondColor: '#406b5e', firstColorFlip: '#9dd4bc', secondColorFlip: '#274238' } + } + if (currentSolar == '秋分') { + return { firstColor: '#f0ce8f', secondColor: '#c1ac9a', firstColorFlip: '#d9ba81', secondColorFlip: '#ab9888' } + } + if (currentSolar == '寒露') { + return { firstColor: '#edcbae', secondColor: '#c3beba', firstColorFlip: '#d6b79d', secondColorFlip: '#b0aba8' } + } + if (currentSolar == '霜降') { + return { firstColor: '#f2e6e2', secondColor: '#997b50', firstColorFlip: '#ded3cf', secondColorFlip: '#826944' } + } + if (currentSolar == '立冬') { + return { firstColor: '#3d6989', secondColor: '#abc9df', firstColorFlip: '#335873', secondColorFlip: '#8fa8ba' } + } + if (currentSolar == '小雪') { + return { firstColor: '#c3dcf4', secondColor: '#b0cced', firstColorFlip: '#b1c8de', secondColorFlip: '#9bb4d1' } + } + if (currentSolar == '大雪') { + return { firstColor: '#afc8e7', secondColor: '#84a8d2', firstColorFlip: '#a0b7d4', secondColorFlip: '#7797bd' } + } + if (currentSolar == '冬至') { + return { firstColor: '#ede1cc', secondColor: '#a3c1ce', firstColorFlip: '#d4c9b6', secondColorFlip: '#8da7b3' } + } + return { firstColor: '#ede1cc', secondColor: '#a3c1ce', firstColorFlip: '#d4c9b6', secondColorFlip: '#8da7b3' } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/AppHeader.vue b/blossom-editor/src/renderer/src/components/AppHeader.vue new file mode 100644 index 0000000..5c49891 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/AppHeader.vue @@ -0,0 +1,95 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/BLCol.vue b/blossom-editor/src/renderer/src/components/BLCol.vue new file mode 100644 index 0000000..afe9e32 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/BLCol.vue @@ -0,0 +1,42 @@ + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/BLRow.vue b/blossom-editor/src/renderer/src/components/BLRow.vue new file mode 100644 index 0000000..9904775 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/BLRow.vue @@ -0,0 +1,42 @@ + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/BLTag.vue b/blossom-editor/src/renderer/src/components/BLTag.vue new file mode 100644 index 0000000..d269a12 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/BLTag.vue @@ -0,0 +1,77 @@ + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/DateLine.vue b/blossom-editor/src/renderer/src/components/DateLine.vue new file mode 100644 index 0000000..2f37130 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/DateLine.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/IconList.vue b/blossom-editor/src/renderer/src/components/IconList.vue new file mode 100644 index 0000000..fb77686 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/IconList.vue @@ -0,0 +1,224 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Laptop.vue b/blossom-editor/src/renderer/src/components/Laptop.vue new file mode 100644 index 0000000..9fe2835 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Laptop.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Logo.vue b/blossom-editor/src/renderer/src/components/Logo.vue new file mode 100644 index 0000000..874b0fe --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Logo.vue @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Notify.ts b/blossom-editor/src/renderer/src/components/Notify.ts new file mode 100644 index 0000000..e9b717f --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Notify.ts @@ -0,0 +1,45 @@ +import { ElNotification } from 'element-plus' + +export class Notify { + + + public static info = (message: string, title?: string) => { + ElNotification.info({ + message: message, + title: title ? title : '(۶=°ω°=)۶', + offset: 30, + position: 'bottom-right' + }) + } + + public static success = (message: string, title?: string) => { + ElNotification.success({ + message: message, + title: title ? title : '(۶๑❛ᴗ❛๑)۶', + offset: 30, + position: 'bottom-right' + }) + } + + public static warning = (message: string, title?: string) => { + ElNotification.warning({ + message: message, + title: title ? title : '(ノ๑`^´๑)ノ', + offset: 30, + position: 'bottom-right' + }) + } + + public static error = (message: string, title?: string) => { + console.log('err'); + ElNotification.error({ + message: message, + title: title ? title : '(๑T^T๑) ', + offset: 30, + // duration: 0, + position: 'bottom-right' + }) + } +} + +export default Notify \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Template.vue b/blossom-editor/src/renderer/src/components/Template.vue new file mode 100644 index 0000000..3fb4bfa --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Template.vue @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/UserAvatar.vue b/blossom-editor/src/renderer/src/components/UserAvatar.vue new file mode 100644 index 0000000..cc8ead2 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/UserAvatar.vue @@ -0,0 +1,115 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Versions.vue b/blossom-editor/src/renderer/src/components/Versions.vue new file mode 100644 index 0000000..bd06132 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Versions.vue @@ -0,0 +1,26 @@ + + + + diff --git a/blossom-editor/src/renderer/src/components/WaveFooter.vue b/blossom-editor/src/renderer/src/components/WaveFooter.vue new file mode 100644 index 0000000..f050e9a --- /dev/null +++ b/blossom-editor/src/renderer/src/components/WaveFooter.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/components/Weather.vue b/blossom-editor/src/renderer/src/components/Weather.vue new file mode 100644 index 0000000..5cc9396 --- /dev/null +++ b/blossom-editor/src/renderer/src/components/Weather.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/blossom-editor/src/renderer/src/components/WebCollect.vue b/blossom-editor/src/renderer/src/components/WebCollect.vue new file mode 100644 index 0000000..c34fb8a --- /dev/null +++ b/blossom-editor/src/renderer/src/components/WebCollect.vue @@ -0,0 +1,254 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/declaration.d.ts b/blossom-editor/src/renderer/src/declaration.d.ts new file mode 100644 index 0000000..d5e7120 --- /dev/null +++ b/blossom-editor/src/renderer/src/declaration.d.ts @@ -0,0 +1,3 @@ +declare module 'prismjs'; +declare module '@renderer/assets/iconfont/iconfont.js' +declare module 'highlight.js' \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/env.d.ts b/blossom-editor/src/renderer/src/env.d.ts new file mode 100644 index 0000000..aafef95 --- /dev/null +++ b/blossom-editor/src/renderer/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/blossom-editor/src/renderer/src/main.ts b/blossom-editor/src/renderer/src/main.ts new file mode 100644 index 0000000..b7597fe --- /dev/null +++ b/blossom-editor/src/renderer/src/main.ts @@ -0,0 +1,37 @@ +import { createApp } from 'vue' +import App from './App.vue' + +import '@renderer/assets/iconfont/blossom/iconfont.css' +import '@renderer/assets/iconfont/blossom/iconfont.js' +import '@renderer/assets/iconfont/tech/iconfont.js' +import '@renderer/assets/iconfont/weblogo/iconfont.js' + +import pinia from './stores/storeConfig'; +import router from '@renderer/router/index' +import '@renderer/router/routeInit' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' + +import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' +import '@renderer/assets/css/main.css' // html 全局样式 +import '@renderer/assets/css/theme.css' // 修改框架等的全局样式 + +// BL Components +import BLTag from '@renderer/components/BLTag.vue' +import BLRow from '@renderer/components/BLRow.vue' +import BLCol from '@renderer/components/BLCol.vue' + +// doc config +import '@renderer/views/article/article' + +const app = createApp(App) +app.use(pinia) +app.use(router) +app.use(ElementPlus, { locale: zhCn, size: 'small', }) +app + .component('bl-tag', BLTag) + .component('bl-row', BLRow) + .component('bl-col', BLCol) + +app.mount('#app') diff --git a/blossom-editor/src/renderer/src/router/index.ts b/blossom-editor/src/renderer/src/router/index.ts new file mode 100644 index 0000000..c47df6c --- /dev/null +++ b/blossom-editor/src/renderer/src/router/index.ts @@ -0,0 +1,16 @@ +import { createRouter, createWebHashHistory } from 'vue-router' + +const router = createRouter({ + history: createWebHashHistory(import.meta.env.BASE_URL), + routes: [] +}) + +export const toRoute = (path: string) => { + router.push(path) +} + +export const toLogin = () => { + router.push('/settingIndex') +} + +export default router \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/router/routeInit.ts b/blossom-editor/src/renderer/src/router/routeInit.ts new file mode 100644 index 0000000..71af504 --- /dev/null +++ b/blossom-editor/src/renderer/src/router/routeInit.ts @@ -0,0 +1,40 @@ +import router from '@renderer/router' + +// 全局根页面 +import Index from '@renderer/views/Index.vue' +import Home from '@renderer/views/home/Home.vue' +import ArticleIndex from '@renderer/views/article/ArticleIndex.vue' +import PictureIndex from '@renderer/views/picture/PictureIndex.vue' +import NoteIndex from '@renderer/views/note/NoteIndex.vue' +import PlanIndex from '@renderer/views/plan/PlanIndex.vue' +import IconListIndex from '@renderer/components/IconList.vue' +import SettingIndex from '@renderer/views/index/SettingIndex.vue' + +// 新窗口页面 +import ArticleViewWindow from '@renderer/views/article/ArticleViewWindow.vue' +import ArticleReference from '@renderer/views/article/ChartGraphArticleReference.vue' + +router.addRoute({ + path: '/', redirect: '/settingIndex' +}) + +router.addRoute( + { + path: '/', name: 'Index', component: Index, meta: { keepAlive: true }, + children: [ + { path: '/home', name: 'Home', component: Home, meta: { keepAlive: true } }, + { path: '/settingIndex', name: 'SettingIndex', component: SettingIndex, meta: { keepAlive: false } }, + // 功能页面 + { path: '/articleIndex', name: 'ArticleIndex', component: ArticleIndex, meta: { keepAlive: true } }, + { path: '/pictureIndex', name: 'PictureIndex', component: PictureIndex, meta: { keepAlive: true } }, + { path: '/noteIndex', name: 'NoteIndex', component: NoteIndex, meta: { keepAlive: true } }, + { path: '/planIndex', name: 'PlanIndex', component: PlanIndex, meta: { keepAlive: true } }, + { path: '/iconListIndex', name: 'IconListIndex', component: IconListIndex, meta: { keepAlive: true } }, + + ] + } +) + +router.addRoute({ path: '/articleViewWindow', name: 'articleViewWindow', component: ArticleViewWindow, meta: { keepAlive: true } }) +router.addRoute({ path: '/iconListIndexWindow', name: 'IconListIndexWindow', component: IconListIndex, meta: { keepAlive: true } }) +router.addRoute({ path: '/articleReferenceWindow', name: 'ArticleReferenceWindow', component: ArticleReference, meta: { keepAlive: true } }) \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/stores/config.ts b/blossom-editor/src/renderer/src/stores/config.ts new file mode 100644 index 0000000..b7eb871 --- /dev/null +++ b/blossom-editor/src/renderer/src/stores/config.ts @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia'; +import { Local } from '@renderer/assets/utils/storage'; + +export const editorKey = 'editorConfig' +export const keymapKey = 'keymapConfig' + +export interface BlConfig { + // 编辑器配置 + editorConfig: { + fontFamily: string + }, + // 快捷键 + keymapConfig: { + + } +} + +export const useConfigStore = defineStore('configStore', { + state: (): BlConfig => ({ + // 编辑器配置 + editorConfig: Local.get(editorKey) || { + fontFamily: "'Jetbrains Mono', sans-serif" + }, + // 快捷键 + keymapConfig: { + + } + }), + actions: { + setEditorFontFamily(fontFamily: string) { + this.editorConfig.fontFamily = fontFamily + Local.set(editorKey, this.editorConfig) + } + } +}); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/stores/server.ts b/blossom-editor/src/renderer/src/stores/server.ts new file mode 100644 index 0000000..f57555f --- /dev/null +++ b/blossom-editor/src/renderer/src/stores/server.ts @@ -0,0 +1,29 @@ +import { defineStore } from 'pinia'; +import { Local } from '@renderer/assets/utils/storage'; + +export const storeKey = 'serverUrl'; +export const usernameKey = 'username'; + + +const initServerUrl = () => { + const defaultUrl = 'http://127.0.0.1:9999' + Local.set(storeKey, defaultUrl) + return defaultUrl +} + +export const useServerStore = defineStore('serverStore', { + state: () => ({ + serverUrl: Local.get(storeKey) || initServerUrl(), + serverUsername: Local.get(usernameKey) || '', + }), + actions: { + async setServerUrl(url: string) { + this.serverUrl = url + Local.set(storeKey, url) + }, + setServerUsername(username: string) { + this.serverUsername = username + Local.set(usernameKey, username) + } + } +}); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/stores/storeConfig.ts b/blossom-editor/src/renderer/src/stores/storeConfig.ts new file mode 100644 index 0000000..17b2c33 --- /dev/null +++ b/blossom-editor/src/renderer/src/stores/storeConfig.ts @@ -0,0 +1,3 @@ +import { createPinia } from 'pinia' +const pinia = createPinia() +export default pinia \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/stores/user.ts b/blossom-editor/src/renderer/src/stores/user.ts new file mode 100644 index 0000000..03c8dc9 --- /dev/null +++ b/blossom-editor/src/renderer/src/stores/user.ts @@ -0,0 +1,130 @@ +import { defineStore } from 'pinia' +import { Local } from '@renderer/assets/utils/storage' +import { loginApi, logoutApi, checkApi, userinfoApi } from '@renderer/api/auth' +import { setUserinfo } from '@renderer/assets/utils/electron' + +export const storeKey = 'token' +export const userinfoKey = 'userinfo' + +/* ====================================================================== +import { storeToRefs } from 'pinia' +import { useUserStore } from '@renderer/stores/user' + +const userStore = useUserStore() +const { userinfo } = storeToRefs(userStore) + * ====================================================================== */ + +/** + * 登录状态枚举 + */ +export const enum AuthStatus { + Wait = '请登录', + Succ = '登录成功', + Fail = '登录失败' +} + +const initAuth = () => { + let auth = { + token: '', + status: AuthStatus.Wait + } + Local.set(storeKey, auth) + return auth +} + +const initUserinfo = () => { + let userinfo = { + id: '', + username: '暂未登录', + nickName: '暂未登录', + avatar: '', + remark: '', + articleCount: 0, + articleWords: 0, + osRes: { + osType: "", + bucketName: "", + domain: "", + defaultPath: "" + }, + params: { + 'WEB_ARTICLE_URL': '' + } + } + Local.set(userinfoKey, userinfo) + return userinfo +} + +export const useUserStore = defineStore('userStore', { + state: () => ({ + auth: Local.get(storeKey) || initAuth(), + userinfo: Local.get(userinfoKey) || initUserinfo() + }), + actions: { + /** + * 根据用户名密码登录 + * @param username 用户名 + * @param password 密码 + */ + loginByPassword(username: string, password: string) { + /* + * 客户端ID, 见服务器配置 xzzz.auth.clients.clientid + * 登录模式, 见服务器配置 xzzz.auth.clients.grantType + */ + loginApi({ username: username, password: password, clientId: 'blossom', grantType: 'password' }) + .then((resp: any) => { + let auth = { token: resp.data.token, status: AuthStatus.Succ }; + this.auth = auth; + Local.set(storeKey, auth); + this.getUserinfo() + }).catch((_e) => { + this.reset() + // 登录失败的状态需要特别更改 + let auth = { token: '', status: AuthStatus.Fail } + this.auth = auth + }) + }, + logout() { + logoutApi().then(_ => { + this.reset(); + }) + }, + /** + * 检查登录状态 + */ + checkToken(succ: any, fail: any) { + checkApi().then(resp => { + let auth = { token: resp.data.token, status: AuthStatus.Succ } + this.auth = auth + Local.set(storeKey, auth) + this.getUserinfo() + succ() + }).catch(_error => { + this.reset() + // 登录失败的状态需要特别更改 + let auth = { token: '', status: AuthStatus.Wait } + this.auth = auth + fail() + }) + }, + /** + * 获取用户信息 + */ + getUserinfo() { + userinfoApi().then(resp => { + this.userinfo = resp.data + Local.set(userinfoKey, resp.data) + setUserinfo(resp.data) + }) + }, + /** + * 重置登录状态和用户信息 + */ + reset() { + Local.remove(storeKey); + Local.remove(userinfoKey); + this.auth = initAuth() + this.userinfo = initUserinfo() + } + } +}); \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/Index.vue b/blossom-editor/src/renderer/src/views/Index.vue new file mode 100644 index 0000000..32b63a9 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/Index.vue @@ -0,0 +1,75 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleIndex.scss b/blossom-editor/src/renderer/src/views/article/ArticleIndex.scss new file mode 100644 index 0000000..e2c82cd --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleIndex.scss @@ -0,0 +1,218 @@ +.index-article-root { + @include box(100%, 100%); + @include flex(row, flex-start, center); + ; + + // 文档 + .doc-container { + height: 100%; + @include flex(column, flex-start, center); + font-weight: 200; + transition: 0.1s; + left: 0; + top: 0; + + .doc-workbench { + @include box(100%, 90px); + } + + .doc-trees { + @include box(100%, calc(100% - 90px)); + font-weight: 200; + border: 0; + overflow-y: scroll; + padding-top: 5px; + padding-left: 10px; + padding-right: 6px; + // 基础的 padding + --el-menu-base-level-padding: 25px; + // 每级别的的缩进 + --el-menu-level-padding: 10px; + // ------------------- sub-item 的样式 + // sub-item 菜单的高度 + --el-menu-sub-item-height: 25px; + + // ------------------- item 的样式 + // 菜单每个 item 的高度 + --el-menu-item-height: 25px; + // item 的 字体大小 + --el-menu-item-font-size: 13px; + + :deep(.el-menu) { + transition: 0.1s !important; + } + + :deep(.el-sub-menu) { + .el-sub-menu__title { + height: auto; + min-height: 25px; + padding-right: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + .el-sub-menu__icon-arrow { + right: calc(215px - var(--el-menu-level) * 10px); + } + } + } + + :deep(.el-menu-item) { + --el-menu-hover-bg-color: #ffffff00 !important; + height: auto; + min-height: 25px; + padding-right: 0; + border-radius: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + transition: 0.1s; + } + + :deep(.el-menu-item.is-active) { + @include themeText(0px 4px 5px rgba(107, 104, 104, 1), 0px 4px 5px rgb(145, 145, 145)); + background: linear-gradient(90deg, var(--xz-html-color) 0%, var(--el-color-primary-light-5) 40%, var(--el-color-primary-light-5) 60%, var(--xz-html-color) 100%); + color: #ffffff; + font-weight: 700; + } + + :deep(.el-badge__content) { + top: 7px; + transform: translateY(-50%) translateX(100%) scale(0.8); + } + + } + + .doc-placeholder { + @include flex(row, center, center); + @include box(100%, calc(100% - 90px)); + color: var(--el-text-color-placeholder); + font-size: 12px; + } + + .doc-temp-textarea { + width: 100%; + + .doc-temp-textarea-workbench { + color: var(--bl-editor-color); + background-color: var(--bl-editor-gutters-bg-color); + border-top: 1px solid var(--el-border-color); + border-bottom: 1px solid var(--el-border-color); + padding: 0 10px; + } + + .doc-temp-textarea-input { + overflow: hidden; + + :deep(.el-textarea__inner) { + height: 130px; + } + } + } + } + + // 编辑器内容 + .editor-container { + @include box(100%, 100%); + position: relative; + border-left: 1px solid var(--el-border-color); + background-color: #FFFFFF00; + + $heightTools: 45px; + $heightStatus: 28px; + $heightEP: calc(100% - 5px - #{$heightStatus} - #{$heightTools}); + + .editor-tools { + @include box(100%, $heightTools); + } + + .editor-preview { + @include box(100%, $heightEP); + @include flex(row, center, center); + border-top: 1px solid var(--el-border-color); + position: relative; + font-family: inherit; + + :deep(*) { + font-family: inherit; + // font-family: 'Sarasa Fixed CL', sans-serif; + } + + .gutter-holder { + height: 100%; + width: 50px; + position: absolute; + background-color: var(--bl-editor-gutters-bg-color); + border-right: 1px solid var(--bl-editor-gutters-border-color); + top: 0; + left: 0; + z-index: 1; + } + + .editor-codemirror { + @include box(50%, 100%); + overflow: auto; + border-right: 1px solid var(--el-border-color); + z-index: 2; + + :deep(.cm-focused) { + outline: none; + } + + :deep(.cm-selectionLayer) { + z-index: 9999 !important; + } + + :deep(.cm-selectionBackground) { + background: var(--el-color-primary-light-6); + z-index: 9999; + } + } + + .preview-marked { + @include box(50%, 100%); + overflow: scroll; + font-size: 14px; + word-wrap: break-word; + padding: 10px 20px 0 20px; + } + + } + + .editor-status { + @include box(100%, $heightStatus); + border-top: 1px solid var(--el-border-color); + z-index: 2; + } + + .bl-preview-toc-absolute.is-expand-open { + @include box(310px, auto); + } + + .bl-preview-toc-absolute.is-expand-close { + @include box(150px, 40px); + } + } + + .editor-search-input { + @include box(300px, 60px); + position: absolute; + left: 40%; + top: 100px; + padding: 10px; + color: var(--el-text-color-placeholder); + background: var(--xz-bg-color); + border-radius: 10px; + box-shadow: 3px 3px 10px 1px #B7B7B7; + + &>div { + @include flex(row); + font-size: 12px; + } + } + + ::-webkit-scrollbar { + width: 5px; + height: 3px; + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleIndex.vue b/blossom-editor/src/renderer/src/views/article/ArticleIndex.vue new file mode 100644 index 0000000..bc02988 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleIndex.vue @@ -0,0 +1,1111 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleInfo.vue b/blossom-editor/src/renderer/src/views/article/ArticleInfo.vue new file mode 100644 index 0000000..047789a --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleInfo.vue @@ -0,0 +1,676 @@ + + + + + + +../doc \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleTreeTitle.vue b/blossom-editor/src/renderer/src/views/article/ArticleTreeTitle.vue new file mode 100644 index 0000000..f8c7274 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleTreeTitle.vue @@ -0,0 +1,395 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleTreeWorkbench.vue b/blossom-editor/src/renderer/src/views/article/ArticleTreeWorkbench.vue new file mode 100644 index 0000000..c0d9aaf --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleTreeWorkbench.vue @@ -0,0 +1,221 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ArticleViewWindow.vue b/blossom-editor/src/renderer/src/views/article/ArticleViewWindow.vue new file mode 100644 index 0000000..1dc9a97 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ArticleViewWindow.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/ChartGraphArticleReference.vue b/blossom-editor/src/renderer/src/views/article/ChartGraphArticleReference.vue new file mode 100644 index 0000000..3e7b078 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/ChartGraphArticleReference.vue @@ -0,0 +1,294 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/EditorStatus.vue b/blossom-editor/src/renderer/src/views/article/EditorStatus.vue new file mode 100644 index 0000000..c98e07f --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/EditorStatus.vue @@ -0,0 +1,82 @@ + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/EditorTools.vue b/blossom-editor/src/renderer/src/views/article/EditorTools.vue new file mode 100644 index 0000000..60184c7 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/EditorTools.vue @@ -0,0 +1,360 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/article.ts b/blossom-editor/src/renderer/src/views/article/article.ts new file mode 100644 index 0000000..7a0026e --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/article.ts @@ -0,0 +1,323 @@ +// codemirror +import { EditorView } from "codemirror" +import { EditorSelection, SelectionRange } from "@codemirror/state" + +// marked +import { marked } from 'marked' +import { markedHighlight } from "marked-highlight" +import hljs from 'highlight.js'; +// import 'highlight.js/styles/atom-one-light.css'; +// import 'highlight.js/styles/base16/darcula.css'; + +/** + * + */ +export const TempTextareaKey = 'editor_temp_textarea_value' + +//#region ----------------------------------------< codemirror >-------------------------------------- + +/** + * codemirror 配置 + */ +export const codemirrorTheme: any = { + "&": { + color: "var(--bl-editor-color)", + backgroundColor: "var(--bl-editor-bg-color)", + fontSize: '14px' + }, + ".cm-gutters": { + backgroundColor: 'var(--bl-editor-gutters-bg-color)', + borderColor: 'var(--bl-editor-gutters-border-color)', + fontSize: '12px' + }, + ".cm-activeLineGutter": { + backgroundColor: 'var(--bl-editor-active-line-gutter-bg-color)' + }, + ".cm-lineNumbers": { + width: '40px' + }, + ".cm-foldGutter": { + // paddingRight: '3px' + }, + ".cm-content": { + whiteSpace: "break-spaces", + wordWrap: "break-word", + width: "calc(100% - 55px)", + overflow: 'auto', + padding: '0' + }, + ".cm-line": { + // color: '#707070' + wordWrap: 'break-word', + wordBreak: 'break-all', + }, + ".cm-activeLine": { + backgroundColor: 'var(--bl-editor-active-line-gutter-bg-color)' + }, + ".cm-selectionMatch": { + backgroundColor: '#fa8072' + }, + ".ͼ1.cm-focused": { + outline: 'none' + }, + ".ͼ5": { + color: 'var(--bl-editor-c5-color)', + fontWeight: '700' + }, + ".ͼ6": { + color: '#707070', + fontWeight: '500' + }, + ".ͼ7": { + backgroundColor: 'var(--bl-editor-c7-bg-color)', + color: 'var(--bl-editor-c7-color)' + }, + ".ͼc": { + color: 'var(--bl-editor-cc-color)', + }, + // ͼm: 注释 #940 + ".ͼm": { + color: 'var(--bl-editor-cm-color)' + }, + // ͼb: 关键字 #708 + ".ͼb": { + color: 'var(--bl-editor-cb-color)' + }, + // ͼd: 数字 #708 + ".ͼd": { + color: 'var(--bl-editor-cd-color)' + }, + // ͼe: 字符串 #a11 + ".ͼe": { + color: 'var(--bl-editor-ce-color)' + }, + //ͼi: 类名: + ".ͼi": { + color: 'var(--bl-editor-ci-color)' + }, + //ͼg: 方法名和参数 + ".ͼg": { + color: 'var(--bl-editor-cg-color)' + } +} + +/** + * 行内格式的替换命令 + * @param editor 编辑器 + * @param range 范围 + * @param target 添加的前后缀字符, 如加粗是 **, 行内代码块是 ` + */ +const replaceInlineCommand = (editor: EditorView, range: SelectionRange, target: string): any => { + let targetLength = target.length + + const prefixFrom: number = range.from - targetLength + const prefixTo: number = range.from + const prefix = editor.state.sliceDoc(prefixFrom, prefixTo) + + const suffixFrom: number = range.to + const suffixTo: number = range.to + targetLength + const suffix = editor.state.sliceDoc(suffixFrom, suffixTo) + // 判断是取消还是添加, 如果被选中的文本前后已经是 target 字符, 则删除前后字符 + if (prefix == target && suffix == target) { + return { + changes: [ + { from: prefixFrom, to: prefixTo, insert: "" }, + { from: suffixFrom, to: suffixTo, insert: "" } + ], + range: EditorSelection.range(prefixFrom, suffixFrom - targetLength) + } + } else { + return { + changes: [ + { from: range.from, insert: target }, + { from: range.to, insert: target } + ], + range: EditorSelection.range(range.from + targetLength, range.to + targetLength) + } + } +} +const replaceDifInlineCommand = (editor: EditorView, range: SelectionRange, prefixTarget: string, suffixTarget: string): any => { + let prefixLength = prefixTarget.length + let suffixLength = suffixTarget.length + + const prefixFrom: number = range.from - prefixLength + const prefixTo: number = range.from + const prefix = editor.state.sliceDoc(prefixFrom, prefixTo) + + const suffixFrom: number = range.to + const suffixTo: number = range.to + suffixLength + const suffix = editor.state.sliceDoc(suffixFrom, suffixTo) + + console.log(prefix, suffix); + + + // 判断是取消还是添加, 如果被选中的文本前后已经是 target 字符, 则删除前后字符 + if (prefix == prefixTarget && suffix == suffixTarget) { + return { + changes: [ + { from: prefixFrom, to: prefixTo, insert: "" }, + { from: suffixFrom, to: suffixTo, insert: "" } + ], + range: EditorSelection.range(prefixFrom, range.to - prefixLength) + } + } else { + return { + changes: [ + { from: range.from, insert: prefixTarget }, + { from: range.to, insert: suffixTarget } + ], + range: EditorSelection.range(range.from + prefixLength, range.to + prefixLength) + } + } +} +/** + * 将选中内容替换为 content, 如果没有选中, 则在光标位置插入 + * @param editor 编辑器 + * @param content 插入的内容 + */ +export const insertBlockCommand = (editor: EditorView, content: string) => { + editor.dispatch(editor.state.replaceSelection(content)) +} +/** + * 选中内容加粗 + */ +export const commandInlineBold = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceInlineCommand(editor, range, '**'))) +} +/** + * 选中内容斜体 + */ +export const commandInlineItalic = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceInlineCommand(editor, range, '*'))) +} +/** + * 选中内容增加删除线 + */ +export const commandInlineStrike = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceInlineCommand(editor, range, '~~'))) +} +/** + * 选择内容设置为行内代码块 + */ +export const commandInlineCode = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceInlineCommand(editor, range, '`'))) +} +/** + * 选择内容设置为上标 + */ +export const commandInlineSup = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceDifInlineCommand(editor, range, '', ''))) +} +/** + * 选择内容设置为下标 + */ +export const commandInlineSub = (editor: EditorView) => { + editor.dispatch(editor.state.changeByRange((range: SelectionRange) => replaceDifInlineCommand(editor, range, '', ''))) +} +/** + * 在当前位置增加表格 + */ +export const commandBlockTable = (editor: EditorView) => { + insertBlockCommand(editor, `\n|||\n|---|---|\n|||\n`) +} +/** + * 在当前位置增加多行代码块 + */ +export const commandBlockPre = (editor: EditorView) => { + insertBlockCommand(editor, `\n\`\`\`java\n\n\`\`\`\n`) +} +/** + * 在当前位置增加单选框 + */ +export const commandBlockCheckBox = (editor: EditorView) => { + insertBlockCommand(editor, `\n- [ ] \n`) +} +/** + * 在当前位置增加分割线 + */ +export const commandBlockSeparator = (editor: EditorView) => { + insertBlockCommand(editor, `\n---\n`) +} +/** + * 在当前位置增加引用 + */ +export const commandBlockquote = (editor: EditorView) => { + insertBlockCommand(editor, `\n>\n>\n`) +} +export const commandBlockquoteBlack = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$black$$\n> ⚫\n`) +} +export const commandBlockquoteGreen = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$green$$\n> 🟢\n`) +} +export const commandBlockquoteYellow = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$yellow$$\n> 🟡\n`) +} +export const commandBlockquoteRed = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$red$$\n> 🔴\n`) +} +export const commandBlockquoteBlue = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$blue$$\n> 🔵\n`) +} +export const commandBlockquotePurple = (editor: EditorView) => { + insertBlockCommand(editor, `\n> $$purple$$\n> 🟣\n`) +} +/** + * 在当前位置增加无序列表 + */ +export const commandBlockUnordered = (editor: EditorView) => { + insertBlockCommand(editor, `\n- \n`) +} +/** + * 在当前位置增加有序列表 + */ +export const commandBlockOrdered = (editor: EditorView) => { + insertBlockCommand(editor, `\n1. \n`) +} +/** + * 在当前位置增加图片 + */ +export const commandBlockImg = (editor: EditorView) => { + insertBlockCommand(editor, `\n![]()\n`) +} +/** + * 在当前位置增加链接 + */ +export const commandBlockLink = (editor: EditorView) => { + insertBlockCommand(editor, `\n[]()\n`) +} +//#endregion + +//#region ----------------------------------------< marked >-------------------------------------- +marked.use({ + async: true, + pedantic: false, + gfm: true, + mangle: false, + headerIds: false +}) + +// 高亮拓展 +marked.use(markedHighlight({ + langPrefix: 'hljs language-', + highlight(code, lang) { + const language = hljs.getLanguage(lang) ? lang : 'shell'; + return hljs.highlight(code, { language }).value; + } +})) + +/* +1. tokenizer +2. walkTokens +3. renderer +*/ +// Override function +// const walkTokens = (token) => { +// if (token.type === 'heading') { +// token.depth += 1; +// } +// }; + +// marked.use({ walkTokens }); + +//#endregion \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/article/marked.d.ts b/blossom-editor/src/renderer/src/views/article/marked.d.ts new file mode 100644 index 0000000..42e07a6 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/article/marked.d.ts @@ -0,0 +1,88 @@ +import { + marked, + MarkedExtension, + MarkedOptions, + TokensList, + Token, +} from "marked"; + +/** marked 拓展 Marked 类, 用于创建新的实例 */ +declare module "marked" { + export class Marked { + /** + * Compiles markdown to HTML. + * + * @param src String of markdown source to be compiled + * @param callback Function called when the markdownString has been fully parsed when using async highlighting + * @return String of compiled HTML + */ + parse(src: string, callback: (error: any, parseResult: string) => void): void; + + /** + * Compiles markdown to HTML asynchronously. + * + * @param src String of markdown source to be compiled + * @param options Hash of options having async: true + * @return Promise of string of compiled HTML + */ + parse(src: string, options: MarkedOptions & { async: true }): Promise; + + /** + * Compiles markdown to HTML synchronously. + * + * @param src String of markdown source to be compiled + * @param options Optional hash of options + * @return String of compiled HTML + */ + parse(src: string, options?: MarkedOptions): string; + + /** + * Compiles markdown to HTML synchronously. + * + * @param src String of markdown source to be compiled + * @param options Optional hash of options + * @param callback Function called when the markdownString has been fully parsed when using async highlighting + * @return String of compiled HTML + */ + parse(src: string, options: MarkedOptions, callback: (error: any, parseResult: string) => void): void; + + /** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ + parseInline(src: string, options?: MarkedOptions): string; + + /** + * Gets the original marked default options. + */ + getDefaults(): MarkedOptions; + + walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => void): Marked; + + /** + * Sets the default options. + * + * @param options Hash of options + */ + setOptions(options: MarkedOptions): Marked; + + /** + * Use Extension + * @param MarkedExtension + */ + use(...extensions: MarkedExtension[]): void; + + walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => void): Marked; + + static Renderer: typeof marked.Renderer; + static Lexer: typeof marked.Lexer; + static Parser: typeof marked.Parser; + static Tokenizer: typeof marked.Tokenizer; + static Renderer: typeof marked.Renderer; + static TextRenderer: typeof marked.TextRenderer; + static Slugger: typeof marked.Slugger; + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/doc/doc.ts b/blossom-editor/src/renderer/src/views/doc/doc.ts new file mode 100644 index 0000000..8bc1a13 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/doc/doc.ts @@ -0,0 +1,27 @@ +import { Ref } from 'vue' +import type { InjectionKey } from 'vue' + +/** + * 依赖注入的 key, 用于限制类型 + */ +export const provideKeyDocTree = Symbol() as InjectionKey> +export const provideKeyDocInfo = Symbol() as InjectionKey> +export const provideKeyCurArticleInfo = Symbol() as InjectionKey> + +/** + * 将 docTree 对象转换为 docInfo + * @param tree DocTree + * @returns DocInfo + */ +export const treeToInfo = (tree: DocTree): DocInfo => { + return { + id: tree.i, + pid: 0, + name: tree.n, + tags: tree.t, + sort: 1, + openStatus: 0, + starStatus: 0, + type: tree.ty + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/doc/type.d.ts b/blossom-editor/src/renderer/src/views/doc/type.d.ts new file mode 100644 index 0000000..826bfc4 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/doc/type.d.ts @@ -0,0 +1,74 @@ +/** + * 文件夹和文档的树状结构, 简短的字段名有助于减少传输大小 + */ +declare interface DocTree { + /** id */ + i: number, + /** pid */ + p: number, + /** name */ + n: string, + /** open: 0:否;1:是; */ + o: number, + /** 版本有差异, 公开文章才会有此项 */ + vd?: number | null, + /** Tags */ + t: string[], + /** 排序 */ + s: number, + /** 图标 */ + icon: string, + /** 文档类型: 1:文章文件夹|2:图片文件夹|3:文章; */ + ty: DocType, + /** 是否 star */ + star: number, + /** 子集 */ + children?: DocTree[] +} + +/** + * 文章详情 + */ +declare interface DocInfo { + id: number, + pid: number, + name: string, + icon?: string, + tags: string[], + sort: number, + cover?: string, + color?: string, + describes?: string, + starStatus: number, + pv?: number, + uv?: number, + likes?: number, + words?: number, + version?: number, + storePath?: string, + subjectWords?: string, + subjectUpdTime?: string, + type: DocType, + creTime?: string, + updTime?: string, + toc?: string, + // 文章正文 + markdown?: string, + html?: string, + // 公开文章的信息 + openStatus: number, + openTime?: string, + openVersion?: number, + syncTime?: string, +} + +/** 文档类型: 1:文章文件夹|2:图片文件夹|3:文章; */ +declare type DocType = 1 | 2 | 3; + +/** 文档弹框类型: 增|删|查 */ +declare type DocDialogType = 'add' | 'upd' | 'info' + +/** 右键菜单对象的显示位置 */ +declare type RightMenu = { show: boolean, clientX: number, clientY: number } + + diff --git a/blossom-editor/src/renderer/src/views/home/ArticleStars.vue b/blossom-editor/src/renderer/src/views/home/ArticleStars.vue new file mode 100644 index 0000000..32c6830 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/ArticleStars.vue @@ -0,0 +1,189 @@ + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/ArticleSubjects.vue b/blossom-editor/src/renderer/src/views/home/ArticleSubjects.vue new file mode 100644 index 0000000..330dd70 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/ArticleSubjects.vue @@ -0,0 +1,187 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/ChartHeatmap.vue b/blossom-editor/src/renderer/src/views/home/ChartHeatmap.vue new file mode 100644 index 0000000..4ead7a8 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/ChartHeatmap.vue @@ -0,0 +1,208 @@ + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/ChartLineSentinel.vue b/blossom-editor/src/renderer/src/views/home/ChartLineSentinel.vue new file mode 100644 index 0000000..757564d --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/ChartLineSentinel.vue @@ -0,0 +1,259 @@ + + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/ChartLineWords.vue b/blossom-editor/src/renderer/src/views/home/ChartLineWords.vue new file mode 100644 index 0000000..14658d7 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/ChartLineWords.vue @@ -0,0 +1,176 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/Home.vue b/blossom-editor/src/renderer/src/views/home/Home.vue new file mode 100644 index 0000000..557c732 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/Home.vue @@ -0,0 +1,342 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/StatisticCard.vue b/blossom-editor/src/renderer/src/views/home/StatisticCard.vue new file mode 100644 index 0000000..e8cc91d --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/StatisticCard.vue @@ -0,0 +1,125 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/home/chartTheme.ts b/blossom-editor/src/renderer/src/views/home/chartTheme.ts new file mode 100644 index 0000000..584d818 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/home/chartTheme.ts @@ -0,0 +1,147 @@ +import { useDark } from '@vueuse/core'; +const isDark = useDark(); + +/** + * 公共样式 + */ +export const common = (): any => { + if (isDark.value) { + return { + fontFamily: 'Consolas, Monaco', + titleColor: '#8C8C8C', + textColor: '#a7a7a7', + legendColor: '#a7a7a7', + axisLineColor: '#272727', + // axisLineColor: '#000000', + itemShadowColor: 'rgba(255, 255, 255, 0.3)', + lineShadowColor: 'rgba(255, 255, 255, 0.5)', + backgroundColor: '#131313' + } + } else { + return { + fontFamily: 'Consolas, Monaco', + titleColor: '#8C8C8C', + textColor: '#494949', + legendColor: '#494949', + axisLineColor: '#ececec', + itemShadowColor: 'rgba(0, 0, 0, 0.7)', + lineShadowColor: 'rgba(0, 0, 0, 0.5)', + backgroundColor: '#ffffff' + } + } +} +/** + * 标题 + */ +export const title = (): any => { + return { + textStyle: { + fontSize: 14, + fontFamily: 'Jetbrains Mono', + color: isDark.value ? '#5B5B5B' : '#ABABAB' + } + } +} + +/** + * 图例样式 + */ +export const legend = (): any => { + return { + orient: 'vertical', + textStyle: { + fontFamily: 'Jetbrains Mono', + color: isDark.value ? '#5B5B5B' : '#ABABAB' + }, + itemStyle: { opacity: 0 } + } +} + +/** + * 提示框样式 + */ +export const tooltip = (): any => { + if (isDark.value) { + return { + show: true, + trigger: 'axis', + confine: true, + enterable: false, + // backgroundColor: '#2B2B2B', + // borderColor: '#252525', + // borderWidth: 2, + // textStyle: { + // color: '#A7A7A7' + // }, + // extraCssText: 'box-shadow: 5px 5px 10px 3px rgba(255, 255, 255, 0.1);' + } + } else { + return { + show: true, + trigger: 'axis', + // 是否将 tooltip 框限制在图表的区域内。 + confine: true, + // 鼠标是否可进入提示框浮层中,默认为false,如需详情内交互,如添加链接,按钮,可设置为 true。 + enterable: false, + // backgroundColor: + // borderColor: '#ffffff', + // borderWidth: 0, + // extraCssText: 'box-shadow: 5px 5px 10px 3px rgba(0, 0, 0, 0.1);' + } + } +} + +export const xAxis = (): any => { + return { + // X轴文字 + axisLabel: { + color: isDark.value ? '#474747' : '#E5E5E5' + }, + // X轴直通顶部的线 + splitLine: { + show: true, + lineStyle: { + color: isDark.value ? '#2B2B2B' : '#F7F7F7' + } + }, + // X轴的坐标轴的刻度 + axisTick: { + inside: true, + length: 0, + lineStyle: { + color: isDark.value ? '#2B2B2B' : '#F7F7F7' + } + }, + // X轴横线 + axisLine: { + show: true, + lineStyle: { + color: isDark.value ? '#2B2B2B' : '#CDCDCD', + shadowColor: isDark.value ? '#000000' : '#41434B', + shadowBlur: 10 + } + }, + axisPointer: { + lineStyle: { + color: isDark.value ? '#353535' : '#C8C8C8' + } + } + } +} + +export const yAxis = (): any => { + return { + show: true, + scale: true, + type: 'value', + splitLine: { + lineStyle: { + color: isDark.value ? '#2B2B2B' : '#F7F7F7' + } + }, + // Y轴文字 + axisLabel: { + color: isDark.value ? '#474747' : '#E5E5E5' + } + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/AsideSetting.vue b/blossom-editor/src/renderer/src/views/index/AsideSetting.vue new file mode 100644 index 0000000..9830b9c --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/AsideSetting.vue @@ -0,0 +1,295 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/AsideUpload.vue b/blossom-editor/src/renderer/src/views/index/AsideUpload.vue new file mode 100644 index 0000000..26e222a --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/AsideUpload.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/AsideUser.vue b/blossom-editor/src/renderer/src/views/index/AsideUser.vue new file mode 100644 index 0000000..d8e5042 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/AsideUser.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/ConfigAddUser.vue b/blossom-editor/src/renderer/src/views/index/ConfigAddUser.vue new file mode 100644 index 0000000..b1b6cd3 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/ConfigAddUser.vue @@ -0,0 +1,75 @@ + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/ConfigClient.vue b/blossom-editor/src/renderer/src/views/index/ConfigClient.vue new file mode 100644 index 0000000..c410e6a --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/ConfigClient.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/ConfigUpdPwd.vue b/blossom-editor/src/renderer/src/views/index/ConfigUpdPwd.vue new file mode 100644 index 0000000..1723d8f --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/ConfigUpdPwd.vue @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/ConfigUserinfo.vue b/blossom-editor/src/renderer/src/views/index/ConfigUserinfo.vue new file mode 100644 index 0000000..c3d7bb7 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/ConfigUserinfo.vue @@ -0,0 +1,101 @@ + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/IndexAside.vue b/blossom-editor/src/renderer/src/views/index/IndexAside.vue new file mode 100644 index 0000000..026a79f --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/IndexAside.vue @@ -0,0 +1,179 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/IndexHeader.vue b/blossom-editor/src/renderer/src/views/index/IndexHeader.vue new file mode 100644 index 0000000..0132e02 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/IndexHeader.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/SettingAbout.vue b/blossom-editor/src/renderer/src/views/index/SettingAbout.vue new file mode 100644 index 0000000..b5d89a2 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/SettingAbout.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/SettingConfig.vue b/blossom-editor/src/renderer/src/views/index/SettingConfig.vue new file mode 100644 index 0000000..078c6bb --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/SettingConfig.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/SettingIndex.vue b/blossom-editor/src/renderer/src/views/index/SettingIndex.vue new file mode 100644 index 0000000..23c5034 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/SettingIndex.vue @@ -0,0 +1,86 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/index/SettingLogin.vue b/blossom-editor/src/renderer/src/views/index/SettingLogin.vue new file mode 100644 index 0000000..4823069 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/index/SettingLogin.vue @@ -0,0 +1,344 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/note/NoteEditor.vue b/blossom-editor/src/renderer/src/views/note/NoteEditor.vue new file mode 100644 index 0000000..fcc3343 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/note/NoteEditor.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/note/NoteIndex.vue b/blossom-editor/src/renderer/src/views/note/NoteIndex.vue new file mode 100644 index 0000000..77a8583 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/note/NoteIndex.vue @@ -0,0 +1,228 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureIndex.scss b/blossom-editor/src/renderer/src/views/picture/PictureIndex.scss new file mode 100644 index 0000000..9d5ca0d --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureIndex.scss @@ -0,0 +1,293 @@ +.index-picture-root { + @include box(100%, 100%); + @include flex(row, flex-start, center); + + // 文档 + .doc-container { + @include box(250px, 100%); + @include flex(column, flex-start, center); + font-weight: 200; + transition: 0.1s; + left: 0; + top: 0; + + .doc-workbench { + @include box(100%, 90px); + border-bottom: 1px solid var(--el-border-color); + } + + .doc-trees { + @include box(100%, calc(100% - 90px - 239px)); + font-weight: 200; + border: 0; + overflow-y: scroll; + padding-top: 5px; + padding-left: 10px; + padding-right: 6px; + // 基础的 padding + --el-menu-base-level-padding: 25px; + // 每级别的的缩进 + --el-menu-level-padding: 10px; + // ------------------- sub-item 的样式 + // sub-item 菜单的高度 + --el-menu-sub-item-height: 25px; + + // ------------------- item 的样式 + // 菜单每个 item 的高度 + --el-menu-item-height: 25px; + // item 的 字体大小 + --el-menu-item-font-size: 13px; + + :deep(.el-menu) { + transition: 0.1s !important; + } + + :deep(.el-sub-menu) { + .el-sub-menu__title { + height: auto; + min-height: 25px; + padding-right: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + .el-sub-menu__icon-arrow { + right: calc(215px - var(--el-menu-level) * 10px); + } + } + } + + :deep(.el-menu-item) { + --el-menu-hover-bg-color: #ffffff00 !important; + height: auto; + min-height: 25px; + padding-right: 0; + border-radius: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + transition: 0.1s; + } + + :deep(.el-menu-item.is-active) { + @include themeText(0px 4px 5px rgba(107, 104, 104, 1), 0px 4px 5px rgb(145, 145, 145)); + background: linear-gradient(90deg, var(--xz-html-color) 0%, var(--el-color-primary-light-5) 40%, var(--el-color-primary-light-5) 60%, var(--xz-html-color) 100%); + color: #ffffff; + font-weight: 700; + } + + :deep(.el-badge__content) { + top: 7px; + transform: translateY(-50%) translateX(100%) scale(0.8); + } + + } + + .doc-upload { + @include box(100%, 239px); + border-top: 1px solid var(--el-border-color); + } + + } + + + + // 编辑器内容 + .picture-container { + @include box(calc(100% - 250px), 100%); + position: relative; + border-left: 1px solid var(--el-border-color); + background-color: #FFFFFF00; + + .star { + @include box(40px, 100%); + color: rgb(206, 175, 0); + padding: 0 5px; + + .iconbl { + padding: 5px; + font-size: 30px; + text-shadow: var(--xz-iconbl-text-shadow); + transition: 0.3s; + cursor: pointer; + } + } + + .bl-star-line { + &:hover { + color: rgb(255, 217, 0) !important; + } + } + + .bl-star-fill { + color: rgb(237, 204, 11); + + &:hover { + color: rgb(255, 217, 0) !important; + } + } + + // 图片列表的控制按钮 + .picutre-workbench { + @include flex(row, space-between, center); + @include box(calc(100% - 20px), 50px); + @include themeShadow(0 3px 10px 1px #d3d3d3, 0 3px 10px 1px #000); + margin: 5px 10px 10px 10px; + padding: 5px; + border-radius: 5px; + + .workbench-group { + height: 100%; + @include flex(row, flex-start, center); + } + + :deep(.el-upload) { + width: 150px; + // @include box(150px, 100%); + --el-upload-dragger-padding-horizontal: 20px; + --el-upload-dragger-padding-vertical: 3px; + --el-fill-color-blank: var(--el-color-primary-light-9); + --el-border-color: var(--el-color-primary-light-3); + color: var(--el-color-primary-light-5); + + .el-upload-dragger { + padding: 8px; + } + + :hover { + color: var(--el-color-primary); + } + } + + + .radio, + .statistic { + @include themeColor(#a7a7a7, #6d6d6d); + margin-left: 15px; + + &>div { + font-size: 11px; + text-align: center; + } + } + + .statistic { + @include themeBg(#f3f3f3, #2b2b2b); + height: 100%; + border-radius: 5px; + padding: 2px 0; + + div { + padding: 0 10px; + + &:first-child { + font-size: 14px; + @include themeBorder(1px, #cdcdcd, #6d6d6d, 'bottom'); + } + + &:last-child { + font-size: 13px; + } + } + } + } + + .picture-card-container { + @include box(100%, calc(100% - 50px)); + @include flex(row, flex-start, flex-start); + align-content: flex-start; + flex-wrap: wrap; + padding: 10px 20px; + overflow-y: scroll; + + + .picture-card { + @include themeShadow(3px 3px 10px 1px #d3d3d3, 3px 3px 10px 1px #000); + @include flex(column, flex-start, center); + position: relative; + border-radius: 5px; + transition: 0.3s; + overflow: hidden; + + &:hover { + @include themeShadow(3px 3px 10px 1px #b0b0b0, 3px 3px 10px 1px #000); + + .picuter-card-workbench { + opacity: 1; + + } + } + + img { + height: 100%; + object-fit: cover; + } + + .img-error { + @include flex(row, center, center); + color: var(--el-color-primary-light-7); + height: 100%; + } + + // 图片卡片的控制按钮 + .picuter-card-workbench { + @include flex(row, space-around, flex-start); + @include box(100%, 50px); + position: absolute; + color: #ffffff; + background-color: var(--el-color-primary-light-3); + bottom: 0; + opacity: 0; + transition: 0.3s; + + .item { + @include box(25%, 100%); + @include flex(row, center, center); + cursor: pointer; + transition: 0.3s; + + &:hover { + background-color: var(--el-color-primary); + width: 30%; + // @include themeColor(#904aff, rgb(255, 242, 0)); + // background-color: var(--el-color-primary); + } + } + + } + } + + .picutre-card-large { + @include box(250px, 300px); + margin: 20px; + + .picuter-card-workbench { + @include box(100%, 50px); + + .iconbl { + font-size: 27px; + } + } + + .iconbl { + font-size: 27px; + } + } + + .picutre-card-mini { + @include box(130px, 180px); + margin: 10px; + + .picuter-card-workbench { + @include box(100%, 30px); + } + } + + .picuter-card-next { + @include box(100%, 50px); + text-align: center; + } + } + + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureIndex.vue b/blossom-editor/src/renderer/src/views/picture/PictureIndex.vue new file mode 100644 index 0000000..d09a696 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureIndex.vue @@ -0,0 +1,403 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureInfo.vue b/blossom-editor/src/renderer/src/views/picture/PictureInfo.vue new file mode 100644 index 0000000..9385bfe --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureInfo.vue @@ -0,0 +1,448 @@ + + + + + + +../doc \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureTreeTitle.vue b/blossom-editor/src/renderer/src/views/picture/PictureTreeTitle.vue new file mode 100644 index 0000000..95b6e2a --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureTreeTitle.vue @@ -0,0 +1,287 @@ + + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureTreeWorkbench.vue b/blossom-editor/src/renderer/src/views/picture/PictureTreeWorkbench.vue new file mode 100644 index 0000000..bcd715a --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureTreeWorkbench.vue @@ -0,0 +1,102 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/PictureUpload.vue b/blossom-editor/src/renderer/src/views/picture/PictureUpload.vue new file mode 100644 index 0000000..8926a18 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/PictureUpload.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/picture.ts b/blossom-editor/src/renderer/src/views/picture/picture.ts new file mode 100644 index 0000000..54af8c4 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/picture.ts @@ -0,0 +1,46 @@ +import type { UploadProps } from 'element-plus' +import Notify from '@renderer/components/Notify' + +export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => { + if (rawFile.size / 1024 / 1024 > 10) { + Notify.error('文件大小不能超过 10MB!', '上传失败') + return false + } + return true +} + +export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file?) => { + handleUploadSeccess(resp) +} + +/** + * 上传文件结果处理 + * @param resp 接口响应 + * @returns 是否成功 + */ +export const handleUploadSeccess = (resp: any): boolean => { + if (resp.code === '20000') { + Notify.success('上传成功') + return true + } else { + Notify.error(resp.msg, '上传失败') + return false + } +} + +export const onError: UploadProps['onError'] = (error, _file, _files) => { + handleUploadError(error) +} + +export const handleUploadError = (error: Error) => { + if (error.message != undefined) { + try { + let resp = JSON.parse(error.message); + if (resp != undefined) { + Notify.error(resp.msg, '上传失败') + } + } catch (e) { + Notify.error(error.message, '上传失败') + } + } +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/picture/type.d.ts b/blossom-editor/src/renderer/src/views/picture/type.d.ts new file mode 100644 index 0000000..c4cf4a3 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/picture/type.d.ts @@ -0,0 +1,16 @@ +/** + * 图片对象 + */ +declare interface Picture { + creTime: string, + id: string | number, + name: string, + pathName: string, + pid: string | number, + size: number, + sourceName: string, + starStatus: number, + url: string, + articleNames: string, + delTime: number +} diff --git a/blossom-editor/src/renderer/src/views/plan/PlanColor.scss b/blossom-editor/src/renderer/src/views/plan/PlanColor.scss new file mode 100644 index 0000000..740dec5 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/plan/PlanColor.scss @@ -0,0 +1,47 @@ +.gray { + background-color: #858585; +} + +.gray.hl { + background-color: #646464; +} + +.red { + background-color: #fb0036; +} + +.red.hl { + background-color: #c9002c; +} + +.yellow { + background-color: #d8a600; +} + +.yellow.hl { + background-color: #ffc400; +} + +.blue { + background-color: #00a3cb; +} + +.blue.hl { + background-color: #00657e; +} + +.green { + background-color: #339a00; +} + +.green.hl { + background-color: #2d8700; +} + +.purple { + background-color: #ad8cf2; +} + +.purple.hl { + background-color: #9665ff; +} \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/plan/PlanDaily.vue b/blossom-editor/src/renderer/src/views/plan/PlanDaily.vue new file mode 100644 index 0000000..4f241ac --- /dev/null +++ b/blossom-editor/src/renderer/src/views/plan/PlanDaily.vue @@ -0,0 +1,202 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/plan/PlanDailyInfo.vue b/blossom-editor/src/renderer/src/views/plan/PlanDailyInfo.vue new file mode 100644 index 0000000..67974fb --- /dev/null +++ b/blossom-editor/src/renderer/src/views/plan/PlanDailyInfo.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/blossom-editor/src/renderer/src/views/plan/PlanDayInfo.vue b/blossom-editor/src/renderer/src/views/plan/PlanDayInfo.vue new file mode 100644 index 0000000..20f44d2 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/plan/PlanDayInfo.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/blossom-editor/src/renderer/src/views/plan/PlanIndex.vue b/blossom-editor/src/renderer/src/views/plan/PlanIndex.vue new file mode 100644 index 0000000..b272e40 --- /dev/null +++ b/blossom-editor/src/renderer/src/views/plan/PlanIndex.vue @@ -0,0 +1,377 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-editor/src/renderer/src/views/tool/IndexTool.vue b/blossom-editor/src/renderer/src/views/tool/IndexTool.vue new file mode 100644 index 0000000..1395def --- /dev/null +++ b/blossom-editor/src/renderer/src/views/tool/IndexTool.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/blossom-editor/test.md b/blossom-editor/test.md new file mode 100644 index 0000000..2146106 --- /dev/null +++ b/blossom-editor/test.md @@ -0,0 +1,554 @@ +# 系统预热 + +> 官方例子:[https://github.com/alibaba/Sentinel/blob/master/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java](https://github.com/alibaba/Sentinel/blob/master/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java) + +## 基本用法 + +预热的意思即为将流量的的控制逐步放宽,允许通过的流量逐步增大,直到所允许的最大流量。我是真的服了, 你看这个? 为啥么饿饿饿饿饿 饿饿饿饿饿饿饿饿饿饿 + +- 预热并不是从收到的第一个请求开始预热,而是当请求量达到允许最大值的 1/3 时才开始进入预热阶段。低于 1/3 时,系统会一直处于冷状态 +- 为什么是 1/3,而不是 1/4,1/5 呢?是通过`sentinel.properties`文件中配置`csp.sentinel.flow.cold.factor`中配置的,可以修改该值,该值越大,预热开始的初始值就越低,相应的 QPS 在预热阶段的变化范围就越大 + +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111 + +配置示例 + +```java +public String flowCheck () throws InterruptedException { + FlowRule rule = new FlowRule(); + rule.setLimitApp("default"); + // 资源名 + rule.setResource("getTest"); + // 系统预热限流方式 + rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); + // 阈值类型,QPS,系统预热其实只能针对QPS进行判断 + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + // 阈值个数,系统进入稳定期时最大的放行速度,QPS + rule.setCount(50); + // 系统进入稳定期需要的时长,单位秒,实际并不是精准的在这个时间进入稳定期的 + rule.setWarmUpPeriodSec(60); + try (Entry ignored = SphU.entry("getTest")) { + System.out.println("SUCC"); + } catch (BlockException e) { + System.out.println("> FAIL"); + } +} +``` + +系统预热的请求流量变化如下图: +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1635301000694-bc79f1ee-f6fe-43d9-8241-ce357d64e1b5.png#averageHue=%23ededed&clientId=u515bedae-5c42-4&from=paste&height=300&id=VctBl&originHeight=600&originWidth=1250&originalType=binary&ratio=1&rotation=0&showTitle=false&size=56651&status=done&style=shadow&taskId=uf75f5149-fb5c-4cfe-aab0-525995e6198&title=&width=525) + +## 源码解析 + +> 官方源码:[https://github.com/alibaba/Sentinel/blob/release-1.8/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java](https://github.com/alibaba/Sentinel/blob/release-1.8/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java) + +在阅读源码前,整理一些逻辑会有助于源码的阅读。 +在 Sentinel 中,对于系统预热的设计思想阐述的并不详尽,只是说主要想法来自[《Guava》](https://github.com/google/guava),所以阅读这部分源码前,可以先读懂[《Guava》](https://github.com/google/guava)的实现有助于我们使用 **Sentinel。** +[《Guava》](https://github.com/google/guava)将这种限流方式称为平滑限流`SmoothRateLimiter`,其中包括`SmoothWarmingUp`平滑预热,以及`SmoothBursty`平滑突发两种流控方式,其中`SmoothBursty`即我们所熟知的漏桶算法。我们的主要目的是阅读`SmoothRateLimiter`抽象类中的说明以及`SmoothWarmingUp`的实现方式。 + +> Guava 官方源码:[https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/SmoothRateLimiter.java](https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/SmoothRateLimiter.java) + +```java + + /* + * How is the RateLimiter designed, and why? + * RateLimiter 是如何设计的,为什么? + * + * The primary feature of a RateLimiter is its "stable rate", the maximum rate that it should + * allow in normal conditions. This is enforced by "throttling" incoming requests as needed. For + * example, we could compute the appropriate throttle time for an incoming request, and make the + * calling thread wait for that time. + * RateLimiter 的主要特点是它的“稳定速率 - stable rate”,即在正常情况下允许的最大速率。这是通过根据需要 "限制 - throttling" 传入请求来实现的。 + * 例如,我们可以计算传入请求的适当节流时间,并让调用线程等待该时间。 + * + * The simplest way to maintain a rate of QPS is to keep the timestamp of the last granted + * request, and ensure that (1/QPS) seconds have elapsed since then. For example, for a rate of + * QPS=5 (5 tokens per second), if we ensure that a request isn't granted earlier than 200ms after + * the last one, then we achieve the intended rate. If a request comes and the last request was + * granted only 100ms ago, then we wait for another 100ms. At this rate, serving 15 fresh permits + * (i.e. for an acquire(15) request) naturally takes 3 seconds. + * 维护 QPS 速率的最简单方法是保存最后一个被授予请求的时间戳,并确保从那时起已经经过了(1/QPS)秒。 + * 例如,对于QPS=5(每秒5个令牌)的速率,如果我们确保一个请求在最后一个请求之后200毫秒之前没有被授予,那么我们就达到了预期的速率。 + * 如果一个请求来了,而最后一个请求在100毫秒之前才被授予,那么我们再等待100毫秒。按照这个速度,服务15个新的许可(即获取15个)自然需要3秒。 + * ===================================================== 批注 ========================================================= + * 这里说的其实是漏桶的大致原理,即每个请求之间的间隔是相同的,每次保存最后一个请求的通过时间。下个请求的通过时间则需要向后递延一个令牌 + * 的下发周期。Guava 的实现与 Sentinel 略微不同,Guava 更注重调整每个请求之间的间隔,而 Sentinel 更注重 QPS,所以 Guava 的实现其 + * 实是基于漏桶的,而 Sentinel 与令牌桶更相似。 + * 这里记录几个名词,下面也会用到: + * ● rate:速率,相当于每秒允许通过的资源,当然也可以是QPS,线程数,网络带宽等等... + * ● permits:许可,可理解为1个令牌 + * ● fresh permits:新许可,新创建的令牌 + * =================================================================================================================== + * + * It is important to realize that such a RateLimiter has a very superficial memory of the past: + * it only remembers the last request. What if the RateLimiter was unused for a long period of + * time, then a request arrived and was immediately granted? This RateLimiter would immediately + * forget about that past underutilization. This may result in either underutilization or + * overflow, depending on the real world consequences of not using the expected rate. + * 重要的是要意识到这样的情况,一个 RateLimiter 对过去一段时间的系统情况的记忆是粗浅的: + * 这句话可以理解为,它只记得最后一个请求。如果限流器在很长一段时间内未使用,然后一个请求到达并立即被批准后,会怎么样? + * 这个限流器会立即忘记过去的这段时间系统是很长时间不被使用的。这可能导致资源未充分利用或溢出,具体取决于没有使用预期速率的实际结果。 + * ===================================================== 批注 ========================================================= + * 限流器并不了解系统的整体运行情况,系统在一段时间内未被使用,然后突然被访问,那对于限流器来说,它会认为记录这次请求为最后一次请求,并且认为系统过去是一直被使用的。 + * 名词: + * ● past underutilization:过去未充分利用,(用系统过去一段时间处于冷却阶段可能更好理解) + * =================================================================================================================== + * + * Past underutilization could mean that excess resources are available. Then, the RateLimiter + * should speed up for a while, to take advantage of these resources. This is important when the + * rate is applied to networking (limiting bandwidth), where past underutilization typically + * translates to "almost empty buffers", which can be filled immediately. + * 过去的资源未充分利用可能意味着有多余的资源可用。那么,限流器应该加速一段时间,以便利用这些资源。 + * 当速率应用于网络(限制带宽)时,这一点很重要,因为过去的未充分利用通常会导致“几乎为空的缓冲区”,这些缓冲区可以立即被填充。 + * ===================================================== 批注 ========================================================= + * 情况一:允许更大的流量通过,这是为了让这些闲置的资源更快的被利用起来,随后逐渐将流量降低。 + * =================================================================================================================== + * + * + * On the other hand, past underutilization could mean that "the server responsible for handling + * the request has become less ready for future requests", i.e. its caches become stale, and + * requests become more likely to trigger expensive operations (a more extreme case of this + * example is when a server has just booted, and it is mostly busy with getting itself up to + * speed). + * 另一方面,过去未充分利用可能意味着 “服务器负责处理请求已经变得不那么准备未来的请求”, 即其缓存变得陈旧, 请求变得更容易触发昂贵操作 + * (一个更极端的例子,这个例子是当一个服务器刚刚启动,而且它主要忙于让自己跟上速度)。 + * ===================================================== 批注 ========================================================= + * 情况二:允许较少的流量通过,随后逐渐将流量提高。因为这时一些功能的缓存可能已经失效,或者一些保持的连接已经过期需要重新连接等情况。 + * =================================================================================================================== + * + * To deal with such scenarios, we add an extra dimension, that of "past underutilization", + * modeled by "storedPermits" variable. This variable is zero when there is no underutilization, + * and it can grow up to maxStoredPermits, for sufficiently large underutilization. So, the + * requested permits, by an invocation acquire(permits), are served from: + * - stored permits (if available) + * - fresh permits (for any remaining permits) + * 为了处理这样的场景,我们添加了一个额外的维度,即由 “storedPermits” 变量建模的 “过去未充分利用” 维度。 + * 当过去没有未充分利用的资源时,该变量为零,当过去有未充分利用的资源时,它可以增长到 maxStoredPermits 令牌桶的最大容量。 + * 因此,请求的许可证,通过从以下两个来调用 acquire(permits) 获取: + * - 存储的令牌, 即令牌桶(如有) + * - 新发的令牌(任何剩余许可证) + * ===================================================== 批注 ========================================================= + * 为了处理这样的场景,Guava 设置了一个变量storedPermits,用来衡量过去未充分利用这一指标。当系统处于忙时:storedPermits为0;当系统 + * 冷却时,storedPermits可以逐渐增长到maxStoredPermits。 + * 名词: + * ● storedPermits:变量,意思为存储起来的令牌数,也可理解为令牌桶。 + * ● maxStoredPermits:变量,表示storedPermits可以存储的最大值,也可理解为令牌桶的最大容量。 + * =================================================================================================================== + * + * How this works is best explained with an example: + * 最好有一个例子解释它时如何工作的: + * + * For a RateLimiter that produces 1 token per second, every second that goes by with the + * RateLimiter being unused, we increase storedPermits by 1. Say we leave the RateLimiter unused + * for 10 seconds (i.e., we expected a request at time X, but we are at time X + 10 seconds before + * a request actually arrives; this is also related to the point made in the last paragraph), thus + * storedPermits becomes 10.0 (assuming maxStoredPermits >= 10.0). At that point, a request of + * acquire(3) arrives. We serve this request out of storedPermits, and reduce that to 7.0 (how + * this is translated to throttling time is discussed later). Immediately after, assume that an + * acquire(10) request arriving. We serve the request partly from storedPermits, using all the + * remaining 7.0 permits, and the remaining 3.0, we serve them by fresh permits produced by the + * rate limiter. + * 对于每秒产生1个令牌的 RateLimiter,在 RateLimiter 未使用的情况下每过一秒,我们将 storedPermit 增加1。 + * 假设我们让 RateLimiter 闲置10秒(即,我们期望在X时间有一个请求,但我们在X + 10秒的时间请求实际到达; + * 这也与上一段中提到的一点有关),因此 storedPermit 变成了10.0(假设 maxStoredPermits >= 10.0)。此时,获取请求(3)到达。 + * 我们从 storedPermit 提供此请求,并将其减少到7.0(稍后将讨论如何将其转换为节流时间)。 + * 紧接着,假设一个获取(10)请求到达。我们服务的请求部分来自 storedPermit,使用所有剩余的7.0许可证,和剩余的3.0许可证, + * 我们提供他们由速率限制器产生的新鲜许可证。 + * ===================================================== 批注 ========================================================= + * 此处举了一个例子来说明: + * 1. 限流器每秒产生1个令牌,最多存储10个令牌。 + * 2. 限流器闲置了10秒,也就是已经存储了10个令牌了。 + * 3. 这时有3个请求来了,那么就从令牌桶中取3个令牌给这三个请求。 + * 4. 紧接着又来了10个请求,那么从令牌桶中取剩余的7个令牌给其中的7个请求。 + * 5. 剩余的3个请求,就要等限流器重新生成了。 + * =================================================================================================================== + * + * We already know how much time it takes to serve 3 fresh permits: if the rate is + * "1 token per second", then this will take 3 seconds. But what does it mean to serve 7 stored + * permits? As explained above, there is no unique answer. If we are primarily interested to deal + * with underutilization, then we want stored permits to be given out /faster/ than fresh ones, + * because underutilization = free resources for the taking. If we are primarily interested to + * deal with overflow, then stored permits could be given out /slower/ than fresh ones. Thus, we + * require a (different in each case) function that translates storedPermits to throttling time. + * 我们已经知道提供 3 个 fresh permits 需要多少时间: 如果速率是 "每秒1个令牌" ,那么这将需要3秒。 + * 但是服务 7 个 stored permits 证意味着什么? + * 如上所述,没有唯一的答案。 + * - 如果我们主要关心的是未充分利用的的资源,那么我们希望 stored permits 比 fresh permits 发放得更快, 因为未充分利用的资源 = 免费资源。 + * - 如果我们主要想处理溢出的问题,那么 stored permits 的发放速度可能会比 fresh permits 慢。 + * 因此,我们需要一个(在每种情况下不同)函数将 storedPermit 属性转换为节流时间。 + * ===================================================== 批注 ========================================================= + * 需要一个函数, 能够控制存储的令牌的发放速度 + * =================================================================================================================== + * + * This role is played by storedPermitsToWaitTime(double storedPermits, double permitsToTake). The + * underlying model is a continuous function mapping storedPermits (from 0.0 to maxStoredPermits) + * onto the 1/rate (i.e. intervals) that is effective at the given storedPermits. "storedPermits" + * essentially measure unused time; we spend unused time buying/storing permits. Rate is + * "permits / time", thus "1 / rate = time / permits". Thus, "1/rate" (time / permits) times + * "permits" gives time, i.e., integrals on this function (which is what storedPermitsToWaitTime() + * computes) correspond to minimum intervals between subsequent requests, for the specified number + * of requested permits. + * 这个角色由 storedPermitsToWaitTime(double storedpermit, double permitsToTake)方法来扮演。 + * 底层模型是一个连续的函数,将 storedPermit (从 0.0 到 maxStoredPermits ) 映射到1/请求速率(即每个令牌的间隔),这在给定的 storedPermit 中是有效的。 + * "storedPermit" 本质上度量的时未使用的时间; 我们把未使用的时间花在 购买或储存 permits(令牌) 上。 + * Rate 是 [permits / time](令牌/时间, 例如令牌为5,时间为1s,则请求的速率就是0.2s) ,因此 [1 / Rate = time / permits]。 + * 因此,[1/rate](time/permits)乘以 permits 给出时间,即这个函数(storedPermitsToWaitTime()) 计算的结果就是在指定请求 + * 令牌 permits 的数量时, 后续请求之间的最小间隔. + * + * Here is an example of storedPermitsToWaitTime: If storedPermits == 10.0, and we want 3 permits, + * we take them from storedPermits, reducing them to 7.0, and compute the throttling for these as + * a call to storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0), which will + * evaluate the integral of the function from 7.0 to 10.0. + * 这里有一个storedPermitsToWaitTime的例子: + * 如果storedPermits == 10.0,我们需要3个 permits,我们从storedPermits取出,并将其减少至7.0,并计算这些作为调用的限流 + * storedPermitsToWaitTime (storedPermits = 10.0, permitsToTake = 3.0),它将计算该函数从7.0到10.0的积分。 + * + * Using integrals guarantees that the effect of a single acquire(3) is equivalent to { + * acquire(1); acquire(1); acquire(1); }, or { acquire(2); acquire(1); }, etc, since the integral + * of the function in [7.0, 10.0] is equivalent to the sum of the integrals of [7.0, 8.0], [8.0, + * 9.0], [9.0, 10.0] (and so on), no matter what the function is. This guarantees that we handle + * correctly requests of varying weight (permits), /no matter/ what the actual function is - so we + * can tweak the latter freely. (The only requirement, obviously, is that we can compute its + * integrals). + * 使用积分可以保证单个acquire(3)的效果等同于{acquire(1);acquire(1);acquire(1);},或{acquire(2);acquire(1);}等,因为函数 + * 在[7.0,10.0]中的积分等价于[7.0,8.0],[8.0,9.0],[9.0,10.0](等等)的积分之和,无论函数是什么。这保证了我们正确地处理不同权重的 + * 请求,而不管实际的功能是什么——所以我们可以自由地调整后者。(唯一的要求是,我们可以计算它的积分)。 + * + * Note well that if, for this function, we chose a horizontal line, at height of exactly (1/QPS), + * then the effect of the function is non-existent: we serve storedPermits at exactly the same + * cost as fresh ones (1/QPS is the cost for each). We use this trick later. + * 请注意,如果对于这个函数,我们选择了一条高度恰好为(1/QPS)的水平线,那么这个函数的效果是不存在的:我们以与 fresh permits 完全相同 + * 的成本提供storedPermits(1/QPS是每个 permits 的成本)。我们稍后会用到这个技巧。 + * + * ^ 令牌的生成时间,越小说明越快 + * | + * +---------- + * | + * | + * | + * +--------------→ + * + * If we pick a function that goes /below/ that horizontal line, it means that we reduce the area + * of the function, thus time. Thus, the RateLimiter becomes /faster/ after a period of + * underutilization. If, on the other hand, we pick a function that goes /above/ that horizontal + * line, then it means that the area (time) is increased, thus storedPermits are more costly than + * fresh permits, thus the RateLimiter becomes /slower/ after a period of underutilization. + * 如果我们选择一个在水平线以下的函数,这意味着我们减少了函数的面积,从而减少了时间。因此,RateLimiter在一段时间的未充分利用后变得/更 + * 快。也就是令牌的生成速度变低了 + * ^ 令牌的生成时间,越小说明越快 + * | + * +-----\ + * | \ + * | \ + * | \ + * +--------------→ + * 如果我们选择一个在水平线以上的函数,那么这意味着面积(时间)增加了,因此存储许可证比新的许可证更昂贵,因此RateLimiter在一段时间未充 + * 分利用后变得慢。也就是令牌的生成速度变慢了,也就让系统的通过数变低了。 + * ^ 令牌的生成时间,越小说明越快 + * | + * | / + * | / + * | / + * +-----/ + * +--------------→ + * + * + * Last, but not least: consider a RateLimiter with rate of 1 permit per second, currently + * completely unused, and an expensive acquire(100) request comes. It would be nonsensical to just + * wait for 100 seconds, and /then/ start the actual task. Why wait without doing anything? A much + * better approach is to /allow/ the request right away (as if it was an acquire(1) request + * instead), and postpone /subsequent/ requests as needed. In this version, we allow starting the + * task immediately, and postpone by 100 seconds future requests, thus we allow for work to get + * done in the meantime instead of waiting idly. + * 最后,但并非最不重要:考虑一个速率为每秒1许可证的RateLimiter,目前完全未使用,并且出现了一个昂贵的获取(100)请求。仅仅等待100秒, + * 然后/然后/开始实际的任务是没有意义的。为什么什么都不做而等待?一个更好的方法是立即/允许/请求(就好像它是一个acquire(1)请求),然后根 + * 据需要推迟/后续/请求。在这个版本中,我们允许立即启动任务,并将未来的请求推迟100秒,因此我们允许在此期间完成工作,而不是无所事事地等待。 + * + * This has important consequences: it means that the RateLimiter doesn't remember the time of the + * _last_ request, but it remembers the (expected) time of the _next_ request. This also enables + * us to tell immediately (see tryAcquire(timeout)) whether a particular timeout is enough to get + * us to the point of the next scheduling time, since we always maintain that. And what we mean by + * "an unused RateLimiter" is also defined by that notion: when we observe that the + * "expected arrival time of the next request" is actually in the past, then the difference (now - + * past) is the amount of time that the RateLimiter was formally unused, and it is that amount of + * time which we translate to storedPermits. (We increase storedPermits with the amount of permits + * that would have been produced in that idle time). So, if rate == 1 permit per second, and + * arrivals come exactly one second after the previous, then storedPermits is _never_ increased -- + * we would only increase it for arrivals _later_ than the expected one second. + * 这有重要的结果:它意味着RateLimiter不记得_last_请求的时间,但它记得_next_请求的(预期的)时间。这也使我们能够立即判断(参见tryAcquire(timeout)) + * 特定的超时是否足以让我们到达下一个调度时间点,因为我们总是保持这个时间点。和我们所说的“一个未使用的RateLimiter”概念也定义为:当我们 + * 观察到“预期下一个请求到达的时间”实际上是在过去,然后现在(过去)的差异RateLimiter正式未使用的时间,这是我们翻译的时间storedPermits。 + * (我们增加storedPermits的数量,在空闲时间将会生产的许可证)。因此,如果rate == 1 permit / s,并且到达时间恰好比前一个晚一秒,那么 + * storedpermit将永远不会增加——我们只会为比预期晚一秒到达的到达增加它。 + */ + + /** + * This implements the following function where coldInterval = coldFactor * stableInterval. + * 这实现了以下函数,其中coldInterval = coldFactor * stableInterval。 + * coldInterval :令牌生成的最慢速度,此时系统允许的QPS是最低的 + * stableInterval :令牌生成的最快速度,此时系统允许的QPS是最高的 + * thresholdPermits :令牌最少的个数 + * maxPermits :令牌最大的个数 + * warmupPeriod :系统预热的时间,也就是令牌数从 0 -> maxPermits 所需的时间 + *          也是令牌生成速 0 —> coldInterval 所需的时间 + * + *
    +   *          ^ throttling
    +   *          |
    +   *    cold  +                  /
    +   * interval |                 /.
    +   *          |                / .
    +   *          |               /  .   ← "warmup period" is the area of the trapezoid between
    +   *          |              /   .     thresholdPermits and maxPermits
    +   *          |             /    .
    +   *          |            /     .
    +   *          |           /      .
    +   *   stable +----------/  WARM .
    +   * interval |          .   UP  .
    +   *          |          . PERIOD.
    +   *          |          .       .
    +   *        0 +----------+-------+--------------→ storedPermits
    +   *          0 thresholdPermits maxPermits
    +   * 
    + * + * Before going into the details of this particular function, let's keep in mind the basics: + * 在深入了解这个特定函数的细节之前,让我们先记住一些基础知识: + *
      + *
    1. The state of the RateLimiter (storedPermits) is a vertical line in this figure. + * 存储的令牌个数在图中是一条垂直线。 + * + *
    2. When the RateLimiter is not used, this goes right (up to maxPermits) + * 当限流器不使用时,令牌数会一直向右增长(直到最大许可停止增长) + * + *
    3. When the RateLimiter is used, this goes left (down to zero), since if we have + * storedPermits, we serve from those first + * 当限流器在使用时,这将向左下降(最终下降到零),因为如果我们有存储的令牌,我们将首先这里获取令牌 + * + *
    4. When _unused_, we go right at a constant rate! The rate at which we move to the right is + * chosen as maxPermits / warmupPeriod. This ensures that the time it takes to go from 0 to + * maxPermits is equal to warmupPeriod. + * 当未被使用时,我们以恒定的速度前进, 向右移动的速率选择为 maxPermit / warmupPeriod。这确保从 0 到 maxPermit 所花费的时间 + * 等于 warmupPeriod(预热时间)。 + * + *
    5. When _used_, the time it takes, as explained in the introductory class note, is equal to + * the integral of our function, between X permits and X-K permits, assuming we want to + * spend K saved permits. + * 当使用时,它所花费的时间,正如在导论笔记中解释的,等于我们的函数的积分,在X许可和X-K许可之间,假设我们想花费K个保存的许可。 + * + *
    + * + *

    In summary, the time it takes to move to the left (spend K permits), is equal to the area of + * the function of width == K. + * 总之,向左移动的时间(也就是使用掉K个许可),等于宽度为K的函数的面积。 + * + *

    Assuming we have saturated demand, the time to go from maxPermits to thresholdPermits is + * equal to warmupPeriod. And the time to go from thresholdPermits to 0 is warmupPeriod/2. (The + * reason that this is warmupPeriod/2 is to maintain the behavior of the original implementation + * where coldFactor was hard coded as 3.) + * 假设我们有饱和的需求,从最大令牌数到稳定令牌数的时间等于预热时间。从稳定令牌数到0的时间是预热时间/2 。 + * (预热时间/2的原因是为了维护原始实现的行为,其中 coldFactor 被硬编码为3。) + * + *

    It remains to calculate thresholdsPermits and maxPermits. + * 接下来还需要计算 thresholdsPermits 和 maxPermit。 + * + *

      + *
    • The time to go from thresholdPermits to 0 is equal to the integral of the function + * between 0 and thresholdPermits. This is thresholdPermits * stableIntervals. By (5) it is + * also equal to warmupPeriod/2. Therefore + *
      + * thresholdPermits = 0.5 * warmupPeriod / stableInterval + *
      + * 从 thresholdsPermits 到 0 的时间等于函数在 0 和 thresholdsPermits 之间的积分。 + * 这是 thresholdsPermits * stableIntervals。(5)它也等于 warmupPeriod/2 。因此 + * + *
    • The time to go from maxPermits to thresholdPermits is equal to the integral of the + * function between thresholdPermits and maxPermits. This is the area of the pictured + * trapezoid, and it is equal to 0.5 * (stableInterval + coldInterval) * (maxPermits - + * thresholdPermits). It is also equal to warmupPeriod, so + *
      + * maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval) + *
      + * 从最大令牌数到稳定令牌数的时间等于稳定令牌数和最大令牌数之间函数的积分。 + * 这是图中梯形的面积,它等于0.5 * (stableInterval + coldInterval) * (maxpermit - thresholdpermit)。它也等于warmupPeriod,所以 + *
    + */ +``` + +上面是关于[《Guava》](https://github.com/google/guava)对限流的设计解释。下面笔者尝试用自己的理解来说明一下,相较于翻译过来的文字可能门槛会更容易理解一些。在这部分中,可能会涉及到一些较为基础的数学知识,包含:**斜率公式**,**斜截式,截距**,**定积分,梯形的面积计算。**如果熟悉这几种数学知识,对下面的说明会更好理解。 + +我们知道限流功能主要是控制请求的速率,一个很容易理解的做法是,系统记录下次允许通过的时间,当最新的请求到来时,如果最新请求小于下次允许通过的时间,那么新请求将被拒绝,或让新等待一段时间后再通过,等待的时间就是下次允许通过的时间`减去`新请求到来的时间。这里其实就是漏桶的大致原理(在前文中已经详细阐述过,这里不在重复解释了)。我们需要注意的是,限流器在使用时具有局限性,那就是**它对系统一段时间内的负载的记忆是粗浅的**,比如:系统已经很长一段时间未使用,这时突然来了请求,那么限流器就会记录这次请求的时间或下次允许通过的时间,这时对于限流器而言,是不知道系统已经很久未被使用了。那这意为着什么呢? + +这意为着限流器将继续按照我们设定的速率来放行请求,但这可能并不是我们期望发生的事情,比如: + +- **情况一**:系统已经很长一段时间未使用,这时突然来了请求,我们期望请求限流器能**按照比平时更大的速率来放行请求**,因为过去一段时间系统未使用可能说明,我们有很多资源被浪费了,比如在接收网络请求时,一段时间没有被使用,则说明系统有充足的缓冲区能允许更多的网络请求来使用这些未被使用的系统资源。 +- **情况二**:系统已经很长一段时间未使用,这时突然来了请求,我们期望请求限流器能**按照比平时更小的速率来放行请求**,因为过去一段时间系统未使用可能说明,我们有很多资源需要重新获取或创建,比如一些常用的缓存被释放、一些保持的连接已经断开需要重连。 + +为了处理这样的情景,[《Guava》](https://github.com/google/guava)设计了一个变量`storedPermits`(存储的令牌)**来衡量这一段未使用的系统资源,当系统繁忙时,`storedPermits`为零,当系统闲置时,`storedPermits`可以逐渐增长(这意为着系统闲置的时间越来越长了)。当然,`storedPermits`是不可以无限增长的,毕竟系统资源不是无限的。那么可增长的最大值,也可设定为`maxPermits`(最大令牌数)**。到这里可以看出,`storedPermits`的设计有些类似令牌桶,用令牌桶来存储令牌,让系统在有突发请求时可以有令牌使用,但其实[《Guava》](https://github.com/google/guava)的限流器从始至终就是漏桶的设计方式,而不是令牌桶。 + +下面先让我们用一个例子来模拟这种情况:假如限流器每秒允许一个请求通过,也就相当于一秒产生一个令牌,假设此时已经 10 秒钟没有请求到来了,那`storedPermits`存储的令牌数就为`10`,也相当于系统已经闲置了`10`秒了,举一个极端的例子,此时我们数据库链接已经超时并断开,所需要的缓存也已经过期,**那么这时如果我们从`storedPermits`中取出了一个令牌给新到来的\*\***请求 1**,平时一个请求需要 1 秒钟可以处理完业务,现在由于请求需要做更多的准备工作(如创建链接,刷新缓存等等)该请求需要更多的时间。**这时当请求 2 到来,由于`storedPermits`尚有`9`个令牌,所以我们又给一个新的\***\*请求 2\*\***下发了`storedPermits`的令牌\*\*,但前一个请求实际并未处理完成,且新的请求可能会与第一个请求做一些重复的操作(如创建链接,刷新缓存),以及占用一些共用资源,这时就意味着我们的系统超过了能承载的上限,而变得不那么稳定了(如频繁 GC,内存溢出,响应超时等等)。 + +从上述例子也可以看出,令牌桶只能保存令牌,但不能决定令牌的下发速度,如上面例子中,**请求 1**和**请求 2**由于都是使用的`storedPermits`存储起来的令牌,所以按理来说,这两个请求应该以区别于正常的速率通过。所以[《Guava》](https://github.com/google/guava)的设计是,根据`storedPermits`的大小,来计算每个请求允许通过时间,如果`storedPermits`越大,那么允许通过的时间就要越快(对应情况一)或越慢(对应情况二),也就是说,我们需要把`storedPermits`与系统的放行速率,即令牌的生成速度`interval`来建立映射关系。那么我们可以将这个映射关系放到坐标系中,如下: + +```java + ^ y: 令牌的生成时间,即每个请求之间的间隔 Interval + | + | + | + | + | + | +0 +------------------→ x:令牌的数量 storedPermits +``` + +用一张正规的坐标系表示,则如下: + +- **情况一**:表示随着令牌数变多,令牌的生成时间逐渐变快 +- **情况二**:表示随着令牌数变多,令牌的生成时间逐渐变慢 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639295022083-d4b14b09-f7dd-4013-9e40-bfa64e678e96.png#averageHue=%23faf4f2&clientId=u8fdf4a17-d0a2-4&from=paste&height=197&id=u1233f0e0&originHeight=197&originWidth=886&originalType=binary&ratio=1&rotation=0&showTitle=false&size=35881&status=done&style=shadow&taskId=u611a681d-5a5d-4224-bee3-799c07e00f5&title=&width=886) +下面将以以情况二举例(实际[《Guava》](https://github.com/google/guava)也只实现了情况二),由于`storedPermits`与`interval`的比值不同,那么二者的映射关系在坐标系中的呈现也是不同的,例如如下几种: +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639295087738-0bf0ea2f-abf9-4ce9-8250-aafa534bc874.png#averageHue=%23f9f8f7&clientId=u8fdf4a17-d0a2-4&from=paste&height=350&id=u76ed5d28&originHeight=350&originWidth=892&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65631&status=done&style=shadow&taskId=u1ec12994-0a99-4d8e-9753-8fc76dbd645&title=&width=892) +这三条线中,黑色线的**最慢速率**一定是最高的,黄色线的**最慢速率**一定是最低的,因为 Y 轴代表了每个**令牌生成的间隔,生成间隔越大,代表了单位时间内允许通过的请求数就越低。**另外需要补充的一点是,Y 轴是不可能为 0 的,如果令牌的生成间隔为 0,则说明单位时间内允许通过的请求时无限的,所以速率一定会有一个大于 0 的最小值,并且速率 = 1 秒/每秒通过的请求,这个值我们称之为`stableInterval`(稳定间隔)**,将稳定间隔的含义代入到这个坐标系中,结果如下:可以看到 Y 轴的最小值为`1`(仅考虑第一象限,其他象限的情况是不存在的。) +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639295184460-f468fc64-6060-45b3-a8d3-43f7666d2d6b.png#averageHue=%23f9f8f7&clientId=u8fdf4a17-d0a2-4&from=paste&height=352&id=ubbf51434&originHeight=352&originWidth=892&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65791&status=done&style=shadow&taskId=u79cb5daa-19f7-427a-b26f-1fdb1c34226&title=&width=892) +上图还可以体现出另一个现象,通俗讲就是斜线的倾斜程度越大,则这个限流器的最低速率就越小,比如下图中当令牌桶的个数为 10 时,三条线对应的**令牌生成的间隔\*\*是不同的 + +- 黄色为:4 +- 蓝色为:3 +- 黑色为:2 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639295322169-a95d7b85-2cd0-4c89-b116-34e01e38f381.png#averageHue=%23f9f8f7&clientId=u8fdf4a17-d0a2-4&from=paste&height=357&id=u0c923def&originHeight=357&originWidth=889&originalType=binary&ratio=1&rotation=0&showTitle=false&size=66573&status=done&style=shadow&taskId=u5adff63b-9d50-4601-a700-a60f387e384&title=&width=889) + +那么当令牌数为`10`,即`maxPermits`(最大令牌数)时,**一个令牌的生成速度应该是多少呢?很高兴这里不需要浪费过多心智了,因为[《Guava》](1)将此值设置为`stableInterval`的`3`倍,在此例中就是`3`秒,我们称之为`coldInterval`(冷却间隔)。**(虽然不理解为什么是 3,但这个值应该与`HashMap`的`_DEFAULT_LOAD_FACTOR_`属性一样,是一个多次测试得来的值,是一个基于性能和稳定性均和考虑的值)。也就是黑色线在`y`轴到达`3`的时候,将不会继续增长了。那么这条线将会被固定为如下: + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639295988650-e78c9e47-733c-4bd0-96f1-251241c7bdbc.png#averageHue=%23f9f9f9&clientId=u8fdf4a17-d0a2-4&from=paste&height=353&id=u14dafebb&originHeight=353&originWidth=893&originalType=binary&ratio=1&rotation=0&showTitle=false&size=45755&status=done&style=shadow&taskId=uf4580c46-31da-44b6-8079-526e434214b&title=&width=893) + +那么最大令牌数是如何确定的呢?[《Guava》](1)将此参数叫做`warmupPeriod`(预热时间),\*\*他表示限流器能存储的最大令牌数,以及从系统从冷却状态到稳定状态所需要的时间。现状我们来整理一下我们设定的各项条件: + +- ` stableInterval`:常量,稳定间隔,当前例子中为:`1`。 +- ` coldInterval`:常量,冷却间隔,当前例子中未:`3`。 +- `thresholdPermits`:常量,稳定间隔对应的令牌数,当前例子中为:`0`。 +- ` maxPermits`:常量,冷却间隔对应的令牌数,当前例子中为:`10`。该值 = `stableInterval` \* `warmupPeriod`。 +- ` storedPermits`:变量,当前存储的令牌数。该值 = `(当前时间 - 上次通过时间) * stableInterval` + +那么再次带入到这个例子中,假设此时`storedPermits`为`8`,那么如何确定此时该请求的下次通过时间呢?也就是如何计算对应 y 轴的值呢? + +通过斜率公式,我们可以计算该值,**斜率**的公式的,如下: + +![](https://cdn.nlark.com/yuque/__latex/3e4f3cab19eb730778906ba6492b4bc7.svg#card=math&code=k%3D%20%5Cfrac%7B%5CDelta%20y%7D%7B%5CDelta%20x%7D%20%3D%20%5Cfrac%7By_2%EF%BC%8Dy_1%7D%7Bx_2%EF%BC%8Dx_1%7D&id=XRt9n) + +```text +$k= \frac{\Delta y}{\Delta x} = \frac{y_2-y_1}{x_2-x_1}$ +``` + +其中`k`代表斜率,`(x1,y1)`和`(x2,y2)`代表了斜线上的任意两个点。其中(`y1`为`stableInterval`,`x1`为`0`),(`y2`为`coldInterval`,`x2`为`maxPermits`),,此时可以计算出斜率等于 + +![](https://cdn.nlark.com/yuque/__latex/44815c28ba5cc5aab68227919a5a895d.svg#card=math&code=k%3D%20%5Cfrac%7B%5CDelta%20y%7D%7B%5CDelta%20x%7D%20%3D%20%5Cfrac%7B3%EF%BC%8D1%7D%7B10%EF%BC%8D0%7D%3D0.2&id=srZMp) + +```text +$k= \frac{\Delta y}{\Delta x} = \frac{3-1}{10-0}=0.2$ +``` + +即`slope`(斜率) = (`coldInterval`-`stableInterval`) / (`maxPermits`-`thresholdPermits`)\*_ +那么带入公式,就能算出,当`storedPermits`为`8`时,对应的`y`轴等于 =`(8-0) _ 0.2 + 1` =`2.6`。也就是存储的令牌数为8时,可以允许的最大QPS是`2.6`。 + +**到此,好像我们已经完成了限流器的设计,但其中其实仍然有很大的问题。** + +由于`storedPermits`是个变化的值,所以每次请求到来时,我们都要计算当前时间与上次时间的间隔,来计算当前的存储的令牌数,按照当前例子,时隔`1`秒后有请求到来,这时需要先计算`storedPermits`的值,该值计算得`1`。带入公式算得该请求需要到`(1-0) * 0.2 + 1`=`1.2`秒后才能通过,但其实当前请求应该**立即通过,**所以为了避免这种请求的发生。[《Guava》](1)将稳定间隔对应的令牌数设置成了一段区间而不是固定值`0`,相当于为限流器设置了一段缓冲空间,在令牌数为这段区间内,系统仍然按照稳定速率来放行请求。如下图: + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639298424343-a94a7e58-ce6b-429f-a706-54c14d03ffa9.png#averageHue=%23faf8f8&clientId=u8fdf4a17-d0a2-4&from=paste&id=uaf5d1fa0&originHeight=350&originWidth=889&originalType=binary&ratio=1&rotation=0&showTitle=false&size=50473&status=done&style=shadow&taskId=u0d8fa858-3c47-4c31-a87f-bc99da0ba74&title=) + +这段缓冲空间为:`warmupPeriod / 2`,那么上述条件中`thresholdPermits`(稳定间隔的令牌数)**的就成了`0~5`,也就是当存储的令牌数大于`5`时,才开始进入冷却阶段,此时才需要将请求的间隔延长,也就是常量`thresholdPermits`的字面意思,即**触发许可数**,也可叫做**触发令牌数。\*\*那么上面的计算工式响应的也要进行修改,如下: + +![1](https://cdn.nlark.com/yuque/__latex/64df2ee8d4301c52c215cd67ffbfd17a.svg#card=math&code=k%3D%20%5Cfrac%7B%5CDelta%20y%7D%7B%5CDelta%20x%7D%20%3D%20%5Cfrac%7B3%EF%BC%8D1%7D%7B10%EF%BC%8D5%7D%3D0.4&id=XbH5J) + +``` +k= \frac{\Delta y}{\Delta x} = \frac{3-1}{10-5}=0.4 +``` + +接下来我们继续看,假设当前存储的令牌为`10`,此时同时`6`个请求到来,则需计算: +1. 请求 1:通过时间`0.0`,当前令牌数为`10-1`,算得请求间隔为`(9-5) * 0.4 + 1`=`2.6`,即下次请求需要在`2.6`秒后。 +2. 请求 2:通过时间`2.6`,当前令牌数为` 9-1`,算得请求间隔为`(8-5) * 0.4 + 1`=`2.2`,即下次请求需要在`2.2`秒后。 +3. 请求 3:通过时间`4.8`,当前令牌数为` 8-1`,算得请求间隔为`(7-5) * 0.4 + 1`=`1.8`,即下次请求需要在`1.8`秒后。 +4. 请求 4:通过时间`6.6`,当前令牌数为` 7-1`,算得请求间隔为`(6-5) * 0.4 + 1`=`1.4`,即下次请求需要在`1.4`秒后。 +5. 请求 5:通过时间`8.0`,当前令牌数为` 6-1`,算得请求间隔为`(5-5) * 0.4 + 1`=`1.0`,即下次请求需要在`1.0`秒后。 +6. 请求 6:通过时间`9.0`,当前令牌数为` 5-1`<触发令牌数,因此间隔=`stableInterval`,即下次请求需要在`1.0`秒后。 + > 这里需要拓展的一点,请求`1`其实并不需要等待`3`秒后才允许通过,因为此时限流器至少有`10`秒钟没有请求到来了,那么让请求 1 再等待 3 秒是无意义的。 + +可以看出:请求 5 时的令牌数为`thresholdPermits`(稳定间隔的令牌数),**所以请求六的间隔即为稳定间隔,也就是此时系统进入了稳定期,但是限流器是在第`9`秒的时候允许第一个稳定间隔的请求通过的,这不符合我们设定的`warmupPeriod`(预热时间):10 秒,**那么问题出在哪呢?其实是因为我们计算的并不那么准确,在[《Guava》](1)中,计算的并不是不同`storedPermits`对应的请求间隔。因为[《Guava》](1)的`SmoothWarmingUp`允许一次占用掉多个令牌,相当于并不是每次都执行`storedPermits-1`这样的操作,而是可能直接`storedPermits-3`,或`storedPermits-5`,那么就需要有一种方式直接计算`storedPermits`在`7-10`的时间,并且要求计算的结果与`7-8`,`8-9`,`9-10`的和是相同的,一种稳定的方式是计算这段的**积分(integrals)。** + +首先,计算 x 轴上`7-10`的积分,必须明确一个函数,那么这个函数是什么呢?上图可知我们计算的就是在斜线区间的`xa`与`xb`的积分区间的定积分,那么这段斜线用**斜截式**表达则为: +![斜截式](https://cdn.nlark.com/yuque/__latex/3949503bba9afc0a00ba9efa3e07fded.svg#card=math&code=y%3Dkx%2Bb%3D0.4x%2B1&id=sqVpD) + +``` +$y=kx+b=0.4x+1$ +``` + +其中`k`为斜率,`b`为与`Y**轴`的截距,图中斜线的截距当然不是`1`了,但若我们忽略掉横线部分,那么斜线的截距也就是`stableInterval`了,当然与其对应的,在计算令牌数时则为`storedPermit-thresholdPermits`,那么放在图中即为: +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639312291312-4344fe25-346a-444d-aa4e-653a6d55494c.png#averageHue=%23fbfafa&clientId=ue08adfb6-d545-4&from=paste&height=342&id=ue0b88bf4&originHeight=342&originWidth=873&originalType=binary&ratio=1&rotation=0&showTitle=false&size=41434&status=done&style=shadow&taskId=u6739a2d7-f654-4985-b266-7f76950e328&title=&width=873) +那么这段函数定积分表达即为: + +![111](https://cdn.nlark.com/yuque/__latex/385f3554544f9724cba53dd678681d7c.svg#card=math&code=%5Cint_%7Ba%7D%5E%7Bb%7D%200.4x%2B1%20dx&id=ZHjjD) + +``` +$\int_{a}^{b} 0.4x+1 dx$ +``` + +此时再来计算上述例子: + +1. 请求 1:通过时间`0.0`,当前令牌数为`10-5-1`,积分为`2.8`,即下次请求需要在`2.8`秒后。 +2. 请求 2:通过时间`2.8`,当前令牌数为` 9-5-1`,积分为`2.4`,即下次请求需要在`2.4`秒后。 +3. 请求 3:通过时间`5.2`,当前令牌数为` 8-5-1`,积分为`2.0`,即下次请求需要在`2.0`秒后。 +4. 请求 4:通过时间`7.2`,当前令牌数为` 7-5-1`,积分为`1.6`,即下次请求需要在`1.6`秒后。 +5. 请求 5:通过时间`8.8`,当前令牌数为` 6-5-1`,积分为`1.2`,即下次请求需要在`1.2`秒后。 +6. 请求 6:通过时间`10.`,当前令牌数为` 5-1`<触发令牌数,因此间隔=`stableInterval`,即下次请求需要在`1.0`秒后。 + +可以看到,第一个稳定速率的请求,是在`warmupPeriod`(预热时间):10 秒\*\*时被通过的。最后一个值得关注的是,[《Guava》](1)并没有使用复杂的积分计算公式,而是简单的计算了下图中不同梯形部分的面积,因为定积分实际就是在计算面积,这部分可见:[《定积分的意义为什么是面积?》](https://www.zhihu.com/question/351048683) + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639314749997-5985c70c-01f7-4b57-a855-5866f5c5d33c.png#averageHue=%23f9f8f8&clientId=ue08adfb6-d545-4&from=paste&height=347&id=ud9d4c948&originHeight=347&originWidth=802&originalType=binary&ratio=1&rotation=0&showTitle=false&size=50296&status=done&style=shadow&taskId=uee5e07de-572d-4675-a312-0c47b661460&title=&width=802) + +梯形的面积为`(上底 + 下底) * 高 / 2`,其中上底和下底可通过`storedPermit`(x 轴)**与`slope`(斜率**)得出,高为`1`,那么上面的例子继续用梯形面积公式计算则为: + +1. 请求 1:通过时间`0.0`,当前令牌数为`10-5-1`,即为绿色梯形面积`(2.6 + 3.0) * 1 / 2`=`2.8`,即下次请求需要在`2.8`秒后。 +2. 请求 2:通过时间`2.8`,当前令牌数为` 9-5-1`,即为绿色梯形面积`(2.2 + 2.6) * 1 / 2`=`2.4`,即下次请求需要在`2.4`秒后。 +3. 请求 3:通过时间`5.2`,当前令牌数为` 8-5-1`,即为绿色梯形面积`(1.8 + 2.2) * 1 / 2`=`2.0`,即下次请求需要在`2.0`秒后。 +4. 请求 4:通过时间`7.2`,当前令牌数为` 7-5-1`,即为绿色梯形面积`(1.4 + 1.8) * 1 / 2`=`1.6`,即下次请求需要在`1.6`秒后。 +5. 请求 5:通过时间`8.8`,当前令牌数为` 6-5-1`,即为绿色梯形面积`(1.0 + 1.4) * 1 / 2`=`1.2`,即下次请求需要在`1.2`秒后。 +6. 请求 6:通过时间`10.`,当前令牌数为` 5-1`<触发令牌数,因此间隔=`stableInterval`,即下次请求需要在`1.0`秒后。 + +结果与积分计算结果相同。[《Guava》](1)源码如下: + +```java +abstract class SmoothRateLimiter extends RateLimiter { + + static final class SmoothWarmingUp extends SmoothRateLimiter { + @Override + long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { + ... + /** + * length 即为上底 + 下底 + */ + double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); + micros = (long) (permitsAboveThresholdToTake * length / 2.0); + ... + } + } +} +``` + +预热功能的完整图形如下: +![image.png](https://cdn.nlark.com/yuque/0/2021/png/1875939/1639317780444-8a816a94-8acc-4e0a-92a4-2e921dff2474.png#averageHue=%23f5eaea&clientId=ue08adfb6-d545-4&from=paste&height=410&id=u462203c5&originHeight=410&originWidth=1013&originalType=binary&ratio=1&rotation=0&showTitle=false&size=89103&status=done&style=shadow&taskId=u055b760f-2d92-4382-9801-8498c368675&title=&width=1013) + +以上是**Guava**中对预热限流的实现,而 Sentinel 与之大同小异,唯一的区别是,**Sentinel 将稳定间隔与冷却间隔两个值替换成了 QPS。**其余计算方式等都相同。 + +> Sentinel 官方源码:[WarmUpController.java](https://github.com/alibaba/Sentinel/blob/master/sentinel-core/src/main/java/com/alibaba/csp/sen~~~~tinel/slots/block/flow/controller/WarmUpController.java) + +有趣的是,Guava 的开发者写有一行 **TODO**,也许`length`参数改叫梯形底之和,会更容易理解。 + +```java +// TODO(cpovirk): Figure out a good name for this variable. +double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); +``` + +> 本文画图工具:[https://www.geogebra.org](https://www.geogebra.org/) +> 定积分计算器:[https://zh.numberempire.com/definiteintegralcalculator.php](https://zh.numberempire.com/definiteintegralcalculator.php) diff --git a/blossom-editor/tsconfig.json b/blossom-editor/tsconfig.json new file mode 100644 index 0000000..31bac6e --- /dev/null +++ b/blossom-editor/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] +} diff --git a/blossom-editor/tsconfig.node.json b/blossom-editor/tsconfig.node.json new file mode 100644 index 0000000..2178c60 --- /dev/null +++ b/blossom-editor/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", + "include": ["electron.vite.config.*", "src/main/*", "src/preload/*"], + "compilerOptions": { + "composite": true, + "types": ["electron-vite/node"] + } +} diff --git a/blossom-editor/tsconfig.web.json b/blossom-editor/tsconfig.web.json new file mode 100644 index 0000000..33dbebd --- /dev/null +++ b/blossom-editor/tsconfig.web.json @@ -0,0 +1,22 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "include": [ + "src/renderer/src/declaration.d.ts", + "src/renderer/src/env.d.ts", + "src/renderer/src/**/*", + "src/renderer/src/**/*.vue", + "src/renderer/src/**/*.d.ts", + "src/renderer/src/assets/iconfont/**/*.json", + "src/preload/*.d.ts", + "src/main/*.d.ts", + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@renderer/*": [ + "src/renderer/src/*" + ] + } + } +} \ No newline at end of file diff --git a/blossom-web/.gitignore b/blossom-web/.gitignore new file mode 100644 index 0000000..38adffa --- /dev/null +++ b/blossom-web/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/blossom-web/README.md b/blossom-web/README.md new file mode 100644 index 0000000..1a7b925 --- /dev/null +++ b/blossom-web/README.md @@ -0,0 +1,70 @@ +# Guardcat 后台监控 + +## 依赖版本 +1. node: 18.12.1 +2. npm: 8.19.2 +3. cnpm: 9.0.1 + +# 相关官网 +## Vue +- https://cn.vuejs.org/guide/introduction.html + +## Vite +- https://cn.vitejs.dev/guide/ + +## tui-editor +- https://ui.toast.com/tui-editor +- https://www.npmjs.com/package/@toast-ui/editor-plugin-color-syntax +- https://www.npmjs.com/package/@toast-ui/editor-plugin-code-syntax-highlight + + +# package.json +```json +{ + "name": "guardcat", + "version": "1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit --skipLibCheck" + }, + "dependencies": { + "@codemirror/lang-java": "^6.0.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-sql": "^6.5.0", + "@codemirror/theme-one-dark": "^6.1.2", + "@toast-ui/editor": "^3.2.2", + "@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0", + "@toast-ui/editor-plugin-color-syntax": "^3.1.0", + "axios": "^1.2.2", + "echarts": "^5.4.1", + "element-plus": "^2.2.28", + "pinia": "^2.0.28", + "svg-sprite-loader": "^2.0.3", + "vue": "^3.2.45", + "vue-codemirror": "^6.1.1", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@types/codemirror": "^5.60.7", + "@types/node": "^18.11.12", + "@vitejs/plugin-vue": "^4.0.0", + "@vitejs/plugin-vue-jsx": "^3.0.0", + "@vue/tsconfig": "^0.1.3", + "npm-run-all": "^4.1.5", + "rollup-plugin-visualizer": "^5.9.0", + "sass": "^1.57.1", + "typescript": "~4.7.4", + "unplugin-auto-import": "^0.16.4", + "unplugin-element-plus": "^0.7.1", + "unplugin-vue-components": "^0.25.0", + "vite": "^4.0.0", + "vue-tsc": "^1.0.12" + } +} + + +``` \ No newline at end of file diff --git a/blossom-web/auto-imports.d.ts b/blossom-web/auto-imports.d.ts new file mode 100644 index 0000000..430addd --- /dev/null +++ b/blossom-web/auto-imports.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-auto-import +export {} +declare global { + const ElNotification: typeof import('element-plus/es')['ElNotification'] +} diff --git a/blossom-web/components.d.ts b/blossom-web/components.d.ts new file mode 100644 index 0000000..dcb6326 --- /dev/null +++ b/blossom-web/components.d.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +import '@vue/runtime-core' + +export {} + +declare module '@vue/runtime-core' { + export interface GlobalComponents { + BLTag: typeof import('./src/components/BLTag.vue')['default'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + NotFound: typeof import('./src/components/NotFound.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Template: typeof import('./src/components/Template.vue')['default'] + Test: typeof import('./src/components/Test.vue')['default'] + } +} diff --git a/blossom-web/declaration.d.ts b/blossom-web/declaration.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/blossom-web/env.d.ts b/blossom-web/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/blossom-web/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/blossom-web/index.html b/blossom-web/index.html new file mode 100644 index 0000000..a657871 --- /dev/null +++ b/blossom-web/index.html @@ -0,0 +1,116 @@ + + + + + + + + + Blossom + + + + +
    +
    +
    +
    + 正在加载 Blossom... +
    + V1.0.0 +
    +
    + +
    + + + \ No newline at end of file diff --git a/blossom-web/package-lock.json b/blossom-web/package-lock.json new file mode 100644 index 0000000..a974f71 --- /dev/null +++ b/blossom-web/package-lock.json @@ -0,0 +1,7002 @@ +{ + "name": "guardcat", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "guardcat", + "version": "1.0.0", + "dependencies": { + "axios": "^1.4.0", + "echarts": "^5.4.2", + "element-plus": "^2.3.7", + "pinia": "^2.1.4", + "vue": "^3.3.4", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^18.11.12", + "@vitejs/plugin-vue": "^4.2.3", + "@vitejs/plugin-vue-jsx": "^3.0.1", + "@vue/tsconfig": "^0.1.3", + "npm-run-all": "^4.1.5", + "rollup-plugin-visualizer": "^5.9.0", + "sass": "^1.57.1", + "typescript": "~4.7.4", + "unplugin-auto-import": "^0.16.4", + "unplugin-element-plus": "^0.7.1", + "unplugin-vue-components": "^0.25.0", + "vite": "^4.3.9", + "vue-tsc": "^1.8.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz", + "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.7.tgz", + "integrity": "sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.7.tgz", + "integrity": "sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", + "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz", + "integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.0.10.tgz", + "integrity": "sha512-ygEZ1mwPjcPo/OulhzLE7mtDrQBWI8vZzEWSNB2W/RNCRjoQGwbaK4N8lV4rid7Ts4qvySU3njMN7YCiSlSaTQ==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.0.tgz", + "integrity": "sha512-zbsLwtnHo84w1Kc8rScAo5GMk1GdecSlrflIbfnEBJwvTSj1SL6kkOYV+nHraMCPEy+RNZZUaZyL8JosDGCtGQ==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.0.tgz", + "integrity": "sha512-TSogMPVxbRe77QCj1dt8NmRiJasPvuc+eT5jnJ6YpLqgOD2zXc5UA3S1qwybN+GVCDNdKfpKy1oj8RpzLJvh6A==", + "dependencies": { + "@floating-ui/core": "^1.0.5" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz", + "integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", + "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.0.1.tgz", + "integrity": "sha512-+Jb7ggL48FSPS1uhPnJbJwWa9Sr90vQ+d0InW+AhBM22n+cfuYqJZDckBc+W3QSHe1WDvewMZfa4wZOtk5pRgw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.7", + "@babel/plugin-transform-typescript": "^7.20.7", + "@vue/babel-plugin-jsx": "^1.1.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.7.8.tgz", + "integrity": "sha512-TPklg4c2e/f1xB/MGZEiQc3AWG+dH64ZfBlYjFB8nNaWJt4Z4k+IHBhmaP52APG+5PHFerwiWI9oF002RrRTPA==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.7.8" + } + }, + "node_modules/@volar/source-map": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.7.8.tgz", + "integrity": "sha512-g2dtC2kOghvfzMDWeODIo4HO1Ml4hxzPTZyAFDz+YhRF9HjZYJSCaWaVuPZ+z0kY+T2daOHYA10GdrWQ5q0teA==", + "dev": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.7.8.tgz", + "integrity": "sha512-NDcI5ZQcdr8kgxzMQrhSSWIM8Tl0MbMFrkvJPTjfm2rdAQZPFT8zv3LrEW9Fqh0e9z2YbCry7jr4a/GShBqeDA==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.7.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", + "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", + "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "@vue/babel-helper-vue-transform-on": "^1.0.2", + "camelcase": "^6.0.0", + "html-tags": "^3.1.0", + "svg-tags": "^1.0.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/language-core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.1.tgz", + "integrity": "sha512-pumv3k4J7P58hVh4YGRM9Qz3HaAr4TlFWM9bnVOkZ/2K9o2CK1lAP2y9Jw+Z0+mNL4F2uWQqnAPzj3seLyfpDA==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.7.8", + "@volar/source-map": "1.7.8", + "@vue/compiler-dom": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.3.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", + "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + }, + "node_modules/@vue/tsconfig": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.1.3.tgz", + "integrity": "sha512-kQVsh8yyWPvHpb8gIc9l/HIDiiVUy1amynLNpCy8p+FoCiZXCo6fQos5/097MmnNZc9AtseDsCrfkhqCrJ8Olg==", + "dev": true, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@vue/typescript": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@vue/typescript/-/typescript-1.8.1.tgz", + "integrity": "sha512-nQpo55j/roie8heCfqyXHnyayqD5+p4/0fzfxH4ZuHf7NSBQS791PNv7ztp2CCOjnGAiaiCMdtC9rc6oriyPUg==", + "dev": true, + "dependencies": { + "@volar/typescript": "1.7.8", + "@vue/language-core": "1.8.1" + } + }, + "node_modules/@vueuse/core": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz", + "integrity": "sha512-CxMewME07qeuzuT/AOIQGv0EhhDoojniqU6pC3F8m5VC76L47UT18DcX88kWlP3I7d3qMJ4u/PD8iSRsy3bmNA==", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.10.0", + "@vueuse/shared": "9.10.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.10.0.tgz", + "integrity": "sha512-G5VZhgTCapzU9rv0Iq2HBrVOSGzOKb+OE668NxhXNcTjUjwYxULkEhAw70FtRLMZc+hxcFAzDZlKYA0xcwNMuw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.10.0.tgz", + "integrity": "sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.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 + }, + "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/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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 + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "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/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "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/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": 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==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.4.3" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true + }, + "node_modules/element-plus": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.7.tgz", + "integrity": "sha512-h6TxclbaLUJxg/Bv5j/ZKsK+K5yadQliw5+R30HWyE69pXlqXTX24oYx+yw3pA4Dy+lqEDi5501FQ0CORk3OSA==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.0.6", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.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 + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.0.tgz", + "integrity": "sha512-GUGtW7eXQay0c+PRq0sGIKSdaBorfVqsCMhGHo4elP7YVqZu9nCZS4UkK4gv71gOWNMra/PaSKD3ao1oWExO0g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.0", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.0", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.0.tgz", + "integrity": "sha512-vZVAIWss0FcR/+a08s6e2/GjGjjYBCZJXDrOnj6l5kJCKhQvJs4cnVqUxkVepIhqHbKHm3uwOvPb8lRcqA3DSg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "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/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "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==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.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, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": 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-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "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, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", + "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/immutable": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", + "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", + "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-TI2hnvT6dPUnn/jARFCJBKL1eeabAfLnKZ2lmW5Uh317s1Ii2IMroL1yMciEk/G+OETykVzlsH6x/L4q/avhgw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": 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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", + "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" + } + }, + "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/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "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/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", + "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "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-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "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/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pinia": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz", + "integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.1.tgz", + "integrity": "sha512-REHe5dx30ERBRFS0iENPHy+t6wtSEYkjrhwNsLyh3qpRaZ1+aylvMUdMBUHWUD/RjjLmLzEvY8Z9XRlpcdIkHA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz", + "integrity": "sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==", + "dev": true, + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/scule": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.0.0.tgz", + "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true + }, + "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, + "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/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", + "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unimport": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.0.7.tgz", + "integrity": "sha512-2dVQUxJEGcrSZ0U4qtwJVODrlfyGcwmIOoHVqbAFFUx7kPoEN5JWr1cZFhLwoAwTmZOvqAm3YIkzv1engIQocg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.2.12", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "mlly": "^1.2.1", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "scule": "^1.0.0", + "strip-literal": "^1.0.1", + "unplugin": "^1.3.1" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unplugin": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz", + "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.16.4.tgz", + "integrity": "sha512-xdgBa9NAS3JG8HjkAZHSbGSMlrjKpaWKXGUzaF6RzEtr980RCl1t0Zsu0skUInNYrEQfqaHc7aGWPv41DLTK/w==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.2", + "@rollup/pluginutils": "^5.0.2", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "minimatch": "^9.0.1", + "unimport": "^3.0.7", + "unplugin": "^1.3.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-auto-import/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-auto-import/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/unplugin-element-plus": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/unplugin-element-plus/-/unplugin-element-plus-0.7.1.tgz", + "integrity": "sha512-qzvPyVUdwVMkWGYHFhVm6hTqmP/juQa8tR+RzY/T1YLEk9o69FGQD8G9fOPzXLeYXJi6MjkIrBye9YUYvLMiKg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "es-module-lexer": "^1.2.1", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + }, + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.25.0.tgz", + "integrity": "sha512-HxrQ4GMSS1RwVww2av3a42cABo/v5AmTRN9iARv6e/xwkrfTyHhLh84kFwXxKkXK61vxDHxaryn694mQmkiVBg==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.3", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "minimatch": "^9.0.1", + "resolve": "^1.22.2", + "unplugin": "^1.3.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-vue-components/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/vue-router": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz", + "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.1.tgz", + "integrity": "sha512-GxBQrcb0Qvyrj1uZqnTXQyWbXdNDRY2MTa+r7ESgjhf+WzBSdxZfkS3KD/C3WhKYG+aN8hf44Hp5Gqzb6PehAA==", + "dev": true, + "dependencies": { + "@vue/language-core": "1.8.1", + "@vue/typescript": "1.8.1", + "semver": "^7.3.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/vue-tsc/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/vue-tsc/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-tsc/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/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "dev": true + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", + "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", + "dev": true, + "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, + "engines": { + "node": ">=12" + } + }, + "node_modules/zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "dependencies": { + "tslib": "2.3.0" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@antfu/utils": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz", + "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==", + "dev": true + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true + }, + "@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.7.tgz", + "integrity": "sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==" + }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.7.tgz", + "integrity": "sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", + "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@ctrl/tinycolor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz", + "integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==" + }, + "@element-plus/icons-vue": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.0.10.tgz", + "integrity": "sha512-ygEZ1mwPjcPo/OulhzLE7mtDrQBWI8vZzEWSNB2W/RNCRjoQGwbaK4N8lV4rid7Ts4qvySU3njMN7YCiSlSaTQ==", + "requires": {} + }, + "@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "dev": true, + "optional": true + }, + "@floating-ui/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.0.tgz", + "integrity": "sha512-zbsLwtnHo84w1Kc8rScAo5GMk1GdecSlrflIbfnEBJwvTSj1SL6kkOYV+nHraMCPEy+RNZZUaZyL8JosDGCtGQ==" + }, + "@floating-ui/dom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.0.tgz", + "integrity": "sha512-TSogMPVxbRe77QCj1dt8NmRiJasPvuc+eT5jnJ6YpLqgOD2zXc5UA3S1qwybN+GVCDNdKfpKy1oj8RpzLJvh6A==", + "requires": { + "@floating-ui/core": "^1.0.5" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@popperjs/core": { + "version": "npm:@sxzz/popperjs-es@2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" + }, + "@types/lodash-es": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz", + "integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "dev": true + }, + "@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "@vitejs/plugin-vue": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", + "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", + "dev": true, + "requires": {} + }, + "@vitejs/plugin-vue-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.0.1.tgz", + "integrity": "sha512-+Jb7ggL48FSPS1uhPnJbJwWa9Sr90vQ+d0InW+AhBM22n+cfuYqJZDckBc+W3QSHe1WDvewMZfa4wZOtk5pRgw==", + "dev": true, + "requires": { + "@babel/core": "^7.20.7", + "@babel/plugin-transform-typescript": "^7.20.7", + "@vue/babel-plugin-jsx": "^1.1.1" + } + }, + "@volar/language-core": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.7.8.tgz", + "integrity": "sha512-TPklg4c2e/f1xB/MGZEiQc3AWG+dH64ZfBlYjFB8nNaWJt4Z4k+IHBhmaP52APG+5PHFerwiWI9oF002RrRTPA==", + "dev": true, + "requires": { + "@volar/source-map": "1.7.8" + } + }, + "@volar/source-map": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.7.8.tgz", + "integrity": "sha512-g2dtC2kOghvfzMDWeODIo4HO1Ml4hxzPTZyAFDz+YhRF9HjZYJSCaWaVuPZ+z0kY+T2daOHYA10GdrWQ5q0teA==", + "dev": true, + "requires": { + "muggle-string": "^0.3.1" + } + }, + "@volar/typescript": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.7.8.tgz", + "integrity": "sha512-NDcI5ZQcdr8kgxzMQrhSSWIM8Tl0MbMFrkvJPTjfm2rdAQZPFT8zv3LrEW9Fqh0e9z2YbCry7jr4a/GShBqeDA==", + "dev": true, + "requires": { + "@volar/language-core": "1.7.8" + } + }, + "@vue/babel-helper-vue-transform-on": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", + "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", + "dev": true + }, + "@vue/babel-plugin-jsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", + "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "@vue/babel-helper-vue-transform-on": "^1.0.2", + "camelcase": "^6.0.0", + "html-tags": "^3.1.0", + "svg-tags": "^1.0.0" + } + }, + "@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "requires": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "requires": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "requires": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "requires": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "@vue/language-core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.1.tgz", + "integrity": "sha512-pumv3k4J7P58hVh4YGRM9Qz3HaAr4TlFWM9bnVOkZ/2K9o2CK1lAP2y9Jw+Z0+mNL4F2uWQqnAPzj3seLyfpDA==", + "dev": true, + "requires": { + "@volar/language-core": "1.7.8", + "@volar/source-map": "1.7.8", + "@vue/compiler-dom": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.3.1", + "vue-template-compiler": "^2.7.14" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", + "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "requires": { + "@vue/shared": "3.3.4" + } + }, + "@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "requires": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "requires": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "requires": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "requires": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + }, + "@vue/tsconfig": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.1.3.tgz", + "integrity": "sha512-kQVsh8yyWPvHpb8gIc9l/HIDiiVUy1amynLNpCy8p+FoCiZXCo6fQos5/097MmnNZc9AtseDsCrfkhqCrJ8Olg==", + "dev": true, + "requires": {} + }, + "@vue/typescript": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@vue/typescript/-/typescript-1.8.1.tgz", + "integrity": "sha512-nQpo55j/roie8heCfqyXHnyayqD5+p4/0fzfxH4ZuHf7NSBQS791PNv7ztp2CCOjnGAiaiCMdtC9rc6oriyPUg==", + "dev": true, + "requires": { + "@volar/typescript": "1.7.8", + "@vue/language-core": "1.8.1" + } + }, + "@vueuse/core": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz", + "integrity": "sha512-CxMewME07qeuzuT/AOIQGv0EhhDoojniqU6pC3F8m5VC76L47UT18DcX88kWlP3I7d3qMJ4u/PD8iSRsy3bmNA==", + "requires": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.10.0", + "@vueuse/shared": "9.10.0", + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "requires": {} + } + } + }, + "@vueuse/metadata": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.10.0.tgz", + "integrity": "sha512-G5VZhgTCapzU9rv0Iq2HBrVOSGzOKb+OE668NxhXNcTjUjwYxULkEhAw70FtRLMZc+hxcFAzDZlKYA0xcwNMuw==" + }, + "@vueuse/shared": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.10.0.tgz", + "integrity": "sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==", + "requires": { + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "requires": {} + } + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "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 + }, + "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 + }, + "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, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.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" + } + }, + "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, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "requires": { + "tslib": "2.3.0", + "zrender": "5.4.3" + } + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true + }, + "element-plus": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.7.tgz", + "integrity": "sha512-h6TxclbaLUJxg/Bv5j/ZKsK+K5yadQliw5+R30HWyE69pXlqXTX24oYx+yw3pA4Dy+lqEDi5501FQ0CORk3OSA==", + "requires": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.0.6", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.3", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + } + }, + "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 + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.0.tgz", + "integrity": "sha512-GUGtW7eXQay0c+PRq0sGIKSdaBorfVqsCMhGHo4elP7YVqZu9nCZS4UkK4gv71gOWNMra/PaSKD3ao1oWExO0g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.0", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.0", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "es-set-tostringtag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.0.tgz", + "integrity": "sha512-vZVAIWss0FcR/+a08s6e2/GjGjjYBCZJXDrOnj6l5kJCKhQvJs4cnVqUxkVepIhqHbKHm3uwOvPb8lRcqA3DSg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "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==" + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + } + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "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==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "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 + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "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, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "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, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "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 + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "html-tags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", + "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "dev": true + }, + "immutable": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", + "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", + "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-TI2hnvT6dPUnn/jARFCJBKL1eeabAfLnKZ2lmW5Uh317s1Ii2IMroL1yMciEk/G+OETykVzlsH6x/L4q/avhgw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "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, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "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 + }, + "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, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "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 + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "requires": {} + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mlly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", + "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "dev": true, + "requires": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" + } + }, + "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 + }, + "muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-releases": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", + "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "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 + }, + "normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "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 + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "pinia": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz", + "integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==", + "requires": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", + "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "requires": {} + } + } + }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "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 + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rollup": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.1.tgz", + "integrity": "sha512-REHe5dx30ERBRFS0iENPHy+t6wtSEYkjrhwNsLyh3qpRaZ1+aylvMUdMBUHWUD/RjjLmLzEvY8Z9XRlpcdIkHA==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-visualizer": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz", + "integrity": "sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==", + "dev": true, + "requires": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "sass": { + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "scule": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.0.0.tgz", + "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "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==" + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true + }, + "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, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string.prototype.padend": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", + "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "requires": { + "acorn": "^8.8.2" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "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, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "devOptional": true + }, + "ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unimport": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.0.7.tgz", + "integrity": "sha512-2dVQUxJEGcrSZ0U4qtwJVODrlfyGcwmIOoHVqbAFFUx7kPoEN5JWr1cZFhLwoAwTmZOvqAm3YIkzv1engIQocg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2", + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.2.12", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "mlly": "^1.2.1", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "scule": "^1.0.0", + "strip-literal": "^1.0.1", + "unplugin": "^1.3.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + } + } + }, + "unplugin": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz", + "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==", + "dev": true, + "requires": { + "acorn": "^8.8.2", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "unplugin-auto-import": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.16.4.tgz", + "integrity": "sha512-xdgBa9NAS3JG8HjkAZHSbGSMlrjKpaWKXGUzaF6RzEtr980RCl1t0Zsu0skUInNYrEQfqaHc7aGWPv41DLTK/w==", + "dev": true, + "requires": { + "@antfu/utils": "^0.7.2", + "@rollup/pluginutils": "^5.0.2", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "minimatch": "^9.0.1", + "unimport": "^3.0.7", + "unplugin": "^1.3.1" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "unplugin-element-plus": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/unplugin-element-plus/-/unplugin-element-plus-0.7.1.tgz", + "integrity": "sha512-qzvPyVUdwVMkWGYHFhVm6hTqmP/juQa8tR+RzY/T1YLEk9o69FGQD8G9fOPzXLeYXJi6MjkIrBye9YUYvLMiKg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2", + "es-module-lexer": "^1.2.1", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + } + }, + "unplugin-vue-components": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.25.0.tgz", + "integrity": "sha512-HxrQ4GMSS1RwVww2av3a42cABo/v5AmTRN9iARv6e/xwkrfTyHhLh84kFwXxKkXK61vxDHxaryn694mQmkiVBg==", + "dev": true, + "requires": { + "@antfu/utils": "^0.7.3", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "minimatch": "^9.0.1", + "resolve": "^1.22.2", + "unplugin": "^1.3.1" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + } + }, + "vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "requires": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "vue-router": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz", + "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==", + "requires": { + "@vue/devtools-api": "^6.5.0" + } + }, + "vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "vue-tsc": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.1.tgz", + "integrity": "sha512-GxBQrcb0Qvyrj1uZqnTXQyWbXdNDRY2MTa+r7ESgjhf+WzBSdxZfkS3KD/C3WhKYG+aN8hf44Hp5Gqzb6PehAA==", + "dev": true, + "requires": { + "@vue/language-core": "1.8.1", + "@vue/typescript": "1.8.1", + "semver": "^7.3.8" + }, + "dependencies": { + "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, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "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, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "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 + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", + "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", + "dev": true, + "requires": { + "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" + } + }, + "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 + }, + "zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "requires": { + "tslib": "2.3.0" + } + } + } +} diff --git a/blossom-web/package.json b/blossom-web/package.json new file mode 100644 index 0000000..a686f05 --- /dev/null +++ b/blossom-web/package.json @@ -0,0 +1,35 @@ +{ + "name": "guardcat", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit --skipLibCheck" + }, + "dependencies": { + "axios": "^1.4.0", + "echarts": "^5.4.2", + "element-plus": "^2.3.7", + "pinia": "^2.1.4", + "vue": "^3.3.4", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^18.11.12", + "@vitejs/plugin-vue": "^4.2.3", + "@vitejs/plugin-vue-jsx": "^3.0.1", + "@vue/tsconfig": "^0.1.3", + "npm-run-all": "^4.1.5", + "rollup-plugin-visualizer": "^5.9.0", + "sass": "^1.57.1", + "typescript": "~4.7.4", + "unplugin-auto-import": "^0.16.4", + "unplugin-element-plus": "^0.7.1", + "unplugin-vue-components": "^0.25.0", + "vite": "^4.3.9", + "vue-tsc": "^1.8.1" + } +} diff --git a/blossom-web/public/favicon.png b/blossom-web/public/favicon.png new file mode 100644 index 0000000..9258146 Binary files /dev/null and b/blossom-web/public/favicon.png differ diff --git a/blossom-web/src/App.vue b/blossom-web/src/App.vue new file mode 100644 index 0000000..8f3f4e9 --- /dev/null +++ b/blossom-web/src/App.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/blossom-web/src/api/blossom.ts b/blossom-web/src/api/blossom.ts new file mode 100644 index 0000000..2963659 --- /dev/null +++ b/blossom-web/src/api/blossom.ts @@ -0,0 +1,68 @@ +import { defaultRequest as rq } from "./request"; +import type { R } from "./request"; + +/** + * 用户信息 + * @param params + * @returns + */ +export const userinfoApi = (params?: object): Promise> => { + return rq.get>("/user/info/open", { params }); +} + +/** + * 文档树状列表 + * @param params + * @returns + */ +export const docTreeApi = (params?: object): Promise> => { + return rq.get>("/doc/trees/open", { params }); +} + +/** + * 专题列表 + * @param params + * @returns + */ +export const subjectsApi = (params?: object): Promise> => { + return rq.get>("/folder/subjects/open", { params }); +} + +// --------------------------------------------------< 文章 >-------------------------------------------------- + +/** + * 文章热力图 + * @param params + * @returns + */ +export const articleHeatmapApi = (params?: object): Promise> => { + return rq.get>("/article/stat/heatmap/open", { params }); +} + +/** + * 字数折线图 + * @param params + * @returns + */ +export const articleWordLineApi = (params?: object): Promise> => { + return rq.get>("/article/stat/line/open", { params }); +} + +/** + * 文章详情 + * @param params + * @returns + */ +export const articleInfoOpenApi = (params?: object): Promise> => { + return rq.get>("/article/open/info", { params }); +} + +// --------------------------------------------------< 三方接口 >-------------------------------------------------- +/** + * gitee 动态热力图 + * @param params + * @returns + */ +export const giteeHeatmapApi = (params?: object): Promise> => { + return rq.get>("/gitee/heatmap/open", { params }); +} \ No newline at end of file diff --git a/blossom-web/src/api/request.ts b/blossom-web/src/api/request.ts new file mode 100644 index 0000000..54b0e35 --- /dev/null +++ b/blossom-web/src/api/request.ts @@ -0,0 +1,133 @@ +import axios from "axios" +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios" +import SYSTEM from '@/assets/constants/blossom' +import { ElNotification } from "element-plus" +import { isNotNull } from "@/assets/utils/obj" + +export class Request { + /** axios 实例 */ + instance: AxiosInstance + /** 基础配置,url和超时时间 */ + baseConfig: AxiosRequestConfig = { + baseURL: SYSTEM.DOMAIN.PRD, + timeout: 60000 + }; + /** + * 构造方法中会设置拦截器逻辑,配置文件为可选项 + * @param config 配置文件内容,为可选项 + */ + constructor(config?: AxiosRequestConfig) { + this.instance = axios.create(Object.assign(this.baseConfig, config)); + /** + * 请求拦截 + */ + this.instance.interceptors.request.use( + //@ts-ignore + (config: AxiosRequestConfig) => { + return { + ...config, + headers: { + "Blossom-User-Id": SYSTEM.DOMAIN.USER_ID + } + } + }, + (err: any) => Promise.reject(err) + ); + /** + * 响应拦截 + */ + this.instance.interceptors.response.use( + (res: AxiosResponse) => { + const status = res.status; + if (status !== 200) { + Promise.reject(res); + } + // 本次响应是否正确 + let isSuccess = false; + // 返回文件流的, 可能没有JSON返回体, 但如果接口报错还是有返回体的 + if (res.config.responseType === 'blob' || res.headers['content-type'] === 'application/force-download') { + if (isNotNull(res.data.code)) { + if (!isSuccessRCode(res.data.code)) { + isSuccess = false; + } + } else { + isSuccess = true; + } + } + // 响应码为正确的直接返回 + if (isSuccessRCode(res.data.code)) { + isSuccess = true; + } + + if (isSuccess) { + return res.data; + } + /* + * 授权被拦截, 则需要退回登录页请求 + */ + else if (res.data.code === 'AUTH-40101') { + ElNotification({ message: res.data.msg, type: "error", position: 'bottom-right', title: '请求错误' }); + return Promise.reject(res); + } + /* + * 其他接口报错, 直接拒绝并提示错误信息 + */ + else { + let errorResponse = res.data; + errorResponse['url'] = res.config.url; + // 其他情况拒绝 + ElNotification({ message: res.data.msg, type: "error", position: 'bottom-right', title: '请求错误' }); + return Promise.reject(res); + } + // 直接返回res,当然你也可以只返回res.data + return res; + }, + (err: any) => { + let errorMsg = err.message; + let code = err.code; + if (code === "ERR_NETWORK") { + errorMsg = "网络错误,请检查您的网络是否通畅"; + } + // 字符串和变量拼接:请求失败(${err.response.status}) + ElNotification({ message: errorMsg, type: "error", position: 'bottom-right', title: '请求错误' }); + return Promise.reject(err); + } + ); + } + + // 定义请求方法 + public request(config: AxiosRequestConfig): Promise { + return this.instance.request(config); + } + + public get(url: string, params?: object): Promise> { + return this.instance.get(url, params); + } + + public post(url: string, data?: object, config?: object): Promise> { + return this.instance.post(url, data, config); + } + + public delete(url: string, params?: object): Promise> { + return this.instance.delete(url, params); + } +} + +export interface R { + ok: boolean; + code: number; + msg: string; + traceId: string, + data?: T; +}; + +/** + * 判断接口响应码是否正确 + * @param code 接口响应码 + * @returns + */ +const isSuccessRCode = (code: string | number): boolean => { + return code === 0 || code === '0' || code === 20000 || code === '20000'; +} + +export const defaultRequest = new Request(); \ No newline at end of file diff --git a/blossom-web/src/assets/constants/blossom.ts b/blossom-web/src/assets/constants/blossom.ts new file mode 100644 index 0000000..9b0ae74 --- /dev/null +++ b/blossom-web/src/assets/constants/blossom.ts @@ -0,0 +1,51 @@ +const blossom: any = { + /** + * 基础配置 + */ + SYS: { + NAME: 'Blossom', + SHORT_NAME: 'BLOSSOM-WEB', + // 版本 + VERSION: 'v1.0', + // 公网安备号 + GONG_WANG_AN_BEI: "X公网安备 XXXXXXXXXX号", + // ICP 备案号 + ICP_BEI_AN_HAO: '京ICP备123123123号', + // 邮箱 + EMAIL: '491548320@qq.com' + }, + /** + * 填写服务器的地址 + */ + DOMAIN: { + LOC: 'http://127.0.0.1:9999/', + PRD: 'https://www.wangyunf.com/bl/', + // 填写你开放为博客的用户ID + USER_ID: 1 + }, + /** + * 可以填写你自己的网站 + * NAME: 网站名称 + * URL: 网站地址 + * LOGO: 网站LOGO, 放在 src/assets/imgs/linklogo/ 路径下 + */ + LINKS: [ + { + NAME: 'Blossom 博客', + URL: 'https://www.wangyunf.com/blossom', + LOGO: 'blossom_logo.png' + }, + { + NAME: 'GuardCat 服务监控', + URL: 'https://www.wangyunf.com/guardcat', + LOGO: 'guardcat_logo.jpg' + }, + { + NAME: 'Danci/e 编程单词速查', + URL: 'https://www.wangyunf.com/danci', + LOGO: 'danci_logo.png' + } + ] +} + +export default blossom diff --git a/blossom-web/src/assets/imgs/blossom/blossom_fill.png b/blossom-web/src/assets/imgs/blossom/blossom_fill.png new file mode 100644 index 0000000..9258146 Binary files /dev/null and b/blossom-web/src/assets/imgs/blossom/blossom_fill.png differ diff --git a/blossom-web/src/assets/imgs/blossom/blossom_logo.png b/blossom-web/src/assets/imgs/blossom/blossom_logo.png new file mode 100644 index 0000000..7c103fe Binary files /dev/null and b/blossom-web/src/assets/imgs/blossom/blossom_logo.png differ diff --git a/blossom-web/src/assets/imgs/common/gitee.png b/blossom-web/src/assets/imgs/common/gitee.png new file mode 100644 index 0000000..0a203c9 Binary files /dev/null and b/blossom-web/src/assets/imgs/common/gitee.png differ diff --git a/blossom-web/src/assets/imgs/common/gong_wang_an_bei_img.png b/blossom-web/src/assets/imgs/common/gong_wang_an_bei_img.png new file mode 100644 index 0000000..9f76394 Binary files /dev/null and b/blossom-web/src/assets/imgs/common/gong_wang_an_bei_img.png differ diff --git a/blossom-web/src/assets/imgs/linklogo/blossom_logo.png b/blossom-web/src/assets/imgs/linklogo/blossom_logo.png new file mode 100644 index 0000000..7c103fe Binary files /dev/null and b/blossom-web/src/assets/imgs/linklogo/blossom_logo.png differ diff --git a/blossom-web/src/assets/imgs/linklogo/danci_logo.png b/blossom-web/src/assets/imgs/linklogo/danci_logo.png new file mode 100644 index 0000000..79cd2d5 Binary files /dev/null and b/blossom-web/src/assets/imgs/linklogo/danci_logo.png differ diff --git a/blossom-web/src/assets/imgs/linklogo/dancidance_logo.png b/blossom-web/src/assets/imgs/linklogo/dancidance_logo.png new file mode 100644 index 0000000..197cbd6 Binary files /dev/null and b/blossom-web/src/assets/imgs/linklogo/dancidance_logo.png differ diff --git a/blossom-web/src/assets/imgs/linklogo/guardcat_logo.jpg b/blossom-web/src/assets/imgs/linklogo/guardcat_logo.jpg new file mode 100644 index 0000000..72f390d Binary files /dev/null and b/blossom-web/src/assets/imgs/linklogo/guardcat_logo.jpg differ diff --git a/blossom-web/src/assets/styles/blossom/bl-preview.scss b/blossom-web/src/assets/styles/blossom/bl-preview.scss new file mode 100644 index 0000000..569e1b9 --- /dev/null +++ b/blossom-web/src/assets/styles/blossom/bl-preview.scss @@ -0,0 +1,339 @@ +// .bl-preview { +// $borderRadius: 4px; +// color: #4b4b4b; +// font-size: 14px; +// line-height: 1.4; + +// img { +// border-radius: $borderRadius; +// max-width: 100%; +// } + +// // 列表 +// h1 { +// background-color: #eaeaea; +// padding: 10px; +// margin-top: 70px; +// border-bottom: 3px solid #bebebe; +// border-radius: $borderRadius; +// box-shadow: 3px 3px 10px 3px #e5e5e5; +// text-align: left; +// text-shadow: 3px 3px 3px rgb(148, 148, 148); +// position: relative; +// } + +// h2 { +// font-size: 25px; +// } + +// h3 { +// font-size: 22px; +// } + +// h4 { +// font-size: 19px; +// } + +// h5, +// h6 { +// font-size: 16px; +// } + +// h1:first-child { +// margin-top: 20px; +// } + +// li::marker { +// color: #989898; +// } + +// // 有序列表 +// ol { +// padding-left: 30px; +// } + +// // 无序列表 +// ul { +// padding-left: 20px; + +// ul { +// padding-left: 20px; +// } +// } + +// // checkbox +// ul:has(input) { +// padding-left: 0px; + +// ul { +// padding-left: 15px; + +// } + +// li::marker { +// content: none; +// } +// } + +// hr { +// border-color: var(--el-color-primary-light-7); +// } + +// // 表格 +// table { +// border: 1px solid #939393; +// box-sizing: border-box; +// padding: 0; +// border-spacing: 0; +// margin: 10px 0; +// max-width: 100%; +// table-layout: fixed; +// width: 100%; + +// thead { +// background-color: #2b2b2b; +// color: #d4d4d4; + +// tr { +// th { +// font-size: 16px; +// padding: 10px; +// border-right: 1px solid #6e6e6e; +// } + +// th:last-child { +// border: 0; +// } +// } +// } + +// tbody { +// tr { +// td { +// padding: 5px; +// border-right: 1px solid #939393; +// border-bottom: 1px solid #939393; +// word-wrap: break-word; +// width: auto; +// } + +// td:last-child { +// border-right: 0; +// } +// } + +// tr:last-child { +// td { +// border-bottom: 0; +// } +// } +// } +// } + +// .bl-table-container { +// border: 0; + +// thead { +// display: none; +// } + +// tbody { +// td { +// border: 0; +// } +// } +// } + +// // 引用 +// blockquote { +// padding: 1px 10px; +// margin: 10px 0; +// color: #7d7d7d; +// background-color: #f0f0f0; +// border-left: 3px solid #bebebe; +// border-radius: $borderRadius; + +// blockquote { +// border: 1px solid #dedede; +// border-left: 3px solid #bebebe; +// } +// } + +// .bl-blockquote-green { +// background-color: #edf8db; +// border-left: 3px solid #bed609; +// } + +// .bl-blockquote-yellow { +// background-color: #faf0d5; +// border-left: 3px solid #efc75e; +// } + +// .bl-blockquote-red { +// background-color: #fbe6e9; +// border-left: 3px solid #ff9090; +// } + +// .bl-blockquote-blue { +// background-color: #dfeefd; +// border-left: 3px solid #81bbf8; +// } + +// .bl-blockquote-purple { +// background-color: #ece4fb; +// border-left: 3px solid #ba9bf2; +// } + +// .bl-blockquote-black { +// background-color: rgba(0, 0, 0, 0.7); +// border-left: 3px solid #000000; +// } + +// // 单行代码块 +// code { +// background-color: #62975536; +// padding: 0px 4px; +// border-radius: $borderRadius; +// margin: 0 5px; +// font-size: 13px; +// } + +// // 代码块 +// pre { +// padding: 10px 10px 10px 10px; +// background-color: #2b2b2b; +// overflow: scroll; +// border-radius: $borderRadius; +// box-shadow: inset 0 0 5px #000, 2px 2px 10px 1px #000; +// font-size: 13px; +// max-height: 1100px; + +// code { +// background-color: inherit; +// padding: 0; +// border-radius: 0; +// margin: 0; +// font-size: 13px; +// } + +// /*! +// Theme: Darcula +// Author: jetbrains +// License: ~ MIT (or more permissive) [via base16-schemes-source] +// Maintainer: @highlightjs/core-team +// Version: 2021.09.0 +// */ +// pre code.hljs { +// display: block; +// overflow-x: auto; +// } + +// code.hljs { +// text-shadow: none; +// } + +// .hljs { +// color: #a9b7c6; +// background: #2b2b2b +// } + +// .hljs ::selection, +// .hljs::selection { +// background-color: #323232; +// color: #a9b7c6 +// } + +// .hljs-comment { +// color: #606366 +// } + +// .hljs-tag { +// color: #a4a3a3 +// } + +// .hljs-operator, +// .hljs-punctuation, +// .hljs-subst { +// color: #a9b7c6 +// } + +// .hljs-operator { +// opacity: .7 +// } + +// .hljs-bullet, +// .hljs-deletion, +// .hljs-name, +// .hljs-selector-tag, +// .hljs-template-variable, +// .hljs-variable { +// color: #4eade5 +// } + +// .hljs-attr, +// .hljs-link, +// .hljs-literal, +// .hljs-number, +// .hljs-symbol, +// .hljs-variable.constant_ { +// color: #689757 +// } + +// .hljs-class .hljs-title, +// .hljs-title, +// .hljs-title.class_ { +// color: #e4b568 +// } + +// .hljs-strong { +// font-weight: 700; +// color: #bbb529 +// } + +// .hljs-addition, +// .hljs-code, +// .hljs-string, +// .hljs-title.class_.inherited__ { +// color: #6a8759 +// } + +// .hljs-built_in, +// .hljs-doctag, +// .hljs-keyword.hljs-atrule, +// .hljs-quote, +// .hljs-regexp { +// color: #629755 +// } + +// .hljs-attribute, +// .hljs-function .hljs-title, +// .hljs-section, +// .hljs-title.function_, +// .ruby .hljs-property { +// color: #9876aa +// } + +// .diff .hljs-meta, +// .hljs-keyword, +// .hljs-template-tag, +// .hljs-type { +// color: #cc7832 +// } + +// .hljs-emphasis { +// color: #cc7832; +// font-style: italic +// } + +// .hljs-meta, +// .hljs-meta .hljs-keyword, +// .hljs-meta .hljs-string { +// color: #b4b428; +// } + +// .hljs-meta .hljs-keyword, +// .hljs-meta-keyword { +// font-weight: 700 +// } +// } +// } \ No newline at end of file diff --git a/blossom-web/src/assets/styles/config.scss b/blossom-web/src/assets/styles/config.scss new file mode 100644 index 0000000..c65258f --- /dev/null +++ b/blossom-web/src/assets/styles/config.scss @@ -0,0 +1,8 @@ +@import "./element/menu"; + +// @import "./blossom/bl-preview"; + +@import "./mixin-fuc"; + +@import "./iconfont"; + diff --git a/blossom-web/src/assets/styles/css/main.css b/blossom-web/src/assets/styles/css/main.css new file mode 100644 index 0000000..a47df0c --- /dev/null +++ b/blossom-web/src/assets/styles/css/main.css @@ -0,0 +1,48 @@ +* { + font-family: 'Consolas', 'Monaco', 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + box-sizing: border-box; + background-color: var(--xz-html-color); +} + +body { + margin: 0; +} + +#app { + width: 100%; + height: 100%; +} + +/** ================================== 滚动条 ================================== */ +/* 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸 */ +::-webkit-scrollbar { + /* 滚动条宽度 */ + width: 5px; + /* 滚动条高度 */ + height: 5px; + /* 滚动条里槽的背景色 */ + /* background-color: #eeeeee; */ +} + +::-webkit-scrollbar:hover { + /* width: 8px; */ + /* height: 8px; */ +} + +/* 定义滑块 内阴影+圆角 */ +::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: #c7c7c7; +} + +/* 解决滚动条右下角出现白色方块 */ +::-webkit-scrollbar-corner { + background: transparent; +} \ No newline at end of file diff --git a/blossom-web/src/assets/styles/css/theme.css b/blossom-web/src/assets/styles/css/theme.css new file mode 100644 index 0000000..e4c3c3e --- /dev/null +++ b/blossom-web/src/assets/styles/css/theme.css @@ -0,0 +1,102 @@ +:root { + + /* 数字越大, 颜色越淡 */ + --el-color-primary: #c1992f; + --el-color-primary-dark-2: rgba(250, 195, 56, 0.8); + --el-color-primary-light-1: rgba(193, 153, 47, 0.9); + --el-color-primary-light-2: rgba(193, 153, 47, 0.8); + --el-color-primary-light-3: rgba(193, 153, 47, 0.7); + --el-color-primary-light-5: rgba(193, 153, 47, 0.5); + --el-color-primary-light-6: rgba(193, 153, 47, 0.4); + --el-color-primary-light-7: rgba(193, 153, 47, 0.3); + --el-color-primary-light-8: rgba(193, 153, 47, 0.2); + --el-color-primary-light-9: rgba(232, 219, 190, 0.505); + + /* 文本颜色 */ + /* --el-text-color-primary: #303133; */ + --el-text-color-primary: #606266; + --el-text-color-regular: #606266; + --el-text-color-secondary: #909399; + --el-text-color-placeholder: #a8abb2; + --el-text-color-disabled: #c0c4cc; + + --el-menu-item-height: 40px; +} + +/* ================================= 自定义颜色 ================================== */ + +:root { + --xz-html-color: #ffffff; +} + +html.dark { + --xz-html-color: #1e1e1e; + +} + +:root { + /* */ + --bl-editor-color: #707070; + --bl-editor-bg-color: #FFFFFF; + + /* gutter */ + --bl-editor-gutters-bg-color: #f5f5f5; + --bl-editor-gutters-border-color: #DDDDDD00; + --bl-editor-active-line-gutter-bg-color: #f7f3fe; + + /* 各类关键字颜色 */ + --bl-editor-cc-color: #219; + --bl-editor-cm-color: #b6b6b6; + --bl-editor-cb-color: #ce5d00; + --bl-editor-cd-color: #689757; + --bl-editor-ce-color: #47ade5; + --bl-editor-ci-color: #ba7300; + --bl-editor-cg-color: #9876aa; + + --bl-editor-c5-color: #7843E9; + --bl-editor-c7-color: #7843E9; + --bl-editor-c7-bg-color: #00000000; + /* ============================================================= */ + + /* 预览的字体颜色 */ + --bl-preview-color: #2b2b2b; + --bl-preview-bg-color: #FFFFFF; + --bl-preview-border-radius: 4px; + + /* 目录 */ + --bl-preview-toc-border-color: #e0e0e0; + + /* 标题 */ + --bl-preview-h1-bg-color: #eaeaea; + --bl-preview-h1-box-shadow: 3px 3px 5px 1px #a8a8a8; + --bl-preview-h1-text-shadow: 3px 3px 3px rgb(148, 148, 148); + + /* 表格 */ + --bl-preview-table-border-color: #939393; + --bl-preview-table-thead-bg-color: #2b2b2b; + + /* 引用 */ + --bl-preview-blockquote-color: #7d7d7d; + --bl-preview-blockquote-bg-color: #f0f0f0; + --bl-preview-blockquote-border-color: #bebebe; + --bl-preview-blockquote-bg-green: #edf8db; + --bl-preview-blockquote-border-green: #bed609; + --bl-preview-blockquote-bg-yellow: #faf0d5; + --bl-preview-blockquote-border-yellow: #efc75e; + --bl-preview-blockquote-bg-red: #fbe6e9; + --bl-preview-blockquote-border-red: #ff9090; + --bl-preview-blockquote-bg-blue: #dfeefd; + --bl-preview-blockquote-border-blue: #81bbf8; + --bl-preview-blockquote-bg-purple: #ece4fb; + --bl-preview-blockquote-border-purple: #ba9bf2; + --bl-preview-blockquote-bg-black: rgba(0, 0, 0, 0.7); + --bl-preview-blockquote-border-black: #000000; + + /* 图片 */ + --bl-preview-img-box-shadow: 2px 2px 10px 3px #c6c6c6; + + /* 单行代码块 */ + --bl-preview-code-bg-color: #eaeaea; + /* 多行代码块 */ + --bl-preview-pre-bg-color: #2b2b2b; +} \ No newline at end of file diff --git a/blossom-web/src/assets/styles/element/menu.scss b/blossom-web/src/assets/styles/element/menu.scss new file mode 100644 index 0000000..dc28d02 --- /dev/null +++ b/blossom-web/src/assets/styles/element/menu.scss @@ -0,0 +1,10 @@ +.el-menu { +} + +:deep(.el-sub-menu__title) { + transition: background-color 0s; +} + +:deep(.el-menu-item:hover) { + background-color: var(--el-color-primary-light-9); +} \ No newline at end of file diff --git a/blossom-web/src/assets/styles/iconfont.scss b/blossom-web/src/assets/styles/iconfont.scss new file mode 100644 index 0000000..50a65ca --- /dev/null +++ b/blossom-web/src/assets/styles/iconfont.scss @@ -0,0 +1,209 @@ +/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ +/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ +@font-face { + font-family: 'iconfont'; /* Project id 1823192 */ + src: url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.eot?t=1686157411988'); /* IE9 */ + src: url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.eot?t=1686157411988#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.woff2?t=1686157411988') format('woff2'), + url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.woff?t=1686157411988') format('woff'), + url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.ttf?t=1686157411988') format('truetype'), + url('https://at.alicdn.com/t/c/font_1823192_gdhg6vgz0r9.svg?t=1686157411988#iconfont') format('svg'); +} + +.icon { + width: 2em; height: 2em; + vertical-align: -0.15em; + fill: currentColor; + overflow: hidden; +} + +.iconfont { + font-family: "iconfont", serif !important; + font-size: 16px; + /*margin-right: 1px;*/ + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + + +.icon-userkey::before {content: "\e631";} +.icon-user3::before {content: "\e634";} +.icon-roles::before {content: "\e637";} +.icon-key1::before {content: "\e649";} +.icon-devil::before {content: "\e6df";} +.icon-setting::before {content: "\e627";} +.icon-home:before {content: "\e867";} +.icon-toggle-left:before {content: "\e681";} +.icon-toggle-right:before {content: "\e682";} +.icon-sql:before {content: "\e63e";} +.icon-b:before {content: "\e6ec";} +.icon-f:before {content: "\e6eb";} +.icon-node-4:before {content: "\e865";} +.icon-node-3:before {content: "\e64d";} +.icon-face-smile:before {content: "\e6cd";} +.icon-node-2:before {content: "\e61e";} +.icon-bug:before {content: "\e61b";} +.icon-nodata:before {content: "\e630";} +.icon-nodata1:before {content: "\e618";} +.icon-nodata1-mini:before {content: "\e618";} +.icon-depend:before {content: "\e642";} +.icon-monitor:before {content: "\e62a";} +.icon-thread:before {content: "\e612";} +.icon-memory:before {content: "\e6ca";} +.icon-flow:before {content: "\e61f";} +.icon-node:before {content: "\e7c1";} +.icon-monitor1:before {content: "\e616";} +.icon-server:before {content: "\e6a4";} +.icon-elasticsearch:before {content: "\e69b";} +.icon-jvm:before {content: "\e717";} +.icon-heartbeat:before {content: "\e68d";} +.icon-user:before {content: "\e653";} +.icon-user-level:before {content: "\e69c";} +.icon-user-search:before {content: "\e66d";} +.icon-user-line:before {content: "\e66b";} +.icon-warn:before {content: "\e622";} +.icon-warn1:before {content: "\e6b6";} +.icon-refresh:before {content: "\ec08";} +.icon-download:before {content: "\e646";} +.icon-execute:before {content: "\e61a";} +.icon-route:before {content: "\e689";} +.icon-interface:before {content: "\e652";} +.icon-redis1:before {content: "\e6a2";} +.icon-redis2:before {content: "\e63a";} +.icon-chart-bar:before {content: "\e628";} +.icon-trace:before {content: "\e68e";} +.icon-bucket:before {content: "\e73f";} +.icon-article-menu:before {content: "\e608";} +.icon-operator:before {content: "\e664";} +.icon-wunai:before {content: "\e600";} +.icon-read2:before {content: "\e665";} +.icon-username:before {content: "\e653";} +.icon-secret:before {content: "\e7c0";} +.icon-glasses:before {content: "\e8ab";} /*眼镜*/ + + + +/*码云*/ +.icon-gitee:before { content: "\e686"; } +/*url*/ +.icon-url:before { font-size: 17px; content: "\e6e8"; } +/*markdown*/ +.icon-markdown1:before { font-size: 25px; content: "\e624"; } +.icon-markdown2:before { font-size: 25px; content: "\e688"; } +/*截图*/ +.icon-picture1:before { font-size: 17px; content: "\e63c"; } +.icon-picture2:before { font-size: 25px; content: "\e69d"; } +/*云端*/ +.icon-cloud1:before { font-size: 14px; content: "\eaaf"; } +.icon-cloud1-20:before { font-size: 20px; content: "\eaaf"; } +.icon-cloud2:before { font-size: 14px; content: "\ee77"; } +.icon-cloud2-20:before { font-size: 20px; content: "\ee77"; } +/*供应商*/ +.icon-gys:before { font-size: 20px; content: "\e6ae"; } +/*key*/ +.icon-key:before { font-size: 20px; content: "\e6bc"; } +/*区域*/ +.icon-endpoint:before { font-size: 20px; content: "\e79f"; } +/*施工*/ +.icon-construction1:before { font-size: 20px; content: "\e66e"; } +.icon-construction2:before { font-size: 20px; content: "\e63d"; } +.icon-construction2-60:before { font-size: 60px; content: "\e63d";} +/*无奈*/ +.icon-jujue:before { font-size: 80px; content: "\e6af";} +/*喜欢*/ +.icon-like:before { font-size: 16px; content: "\ee78"; color: #e33d0e;} +.icon-dontlike:before { font-size: 16px; content: "\e63b"; color: #858585} +/*复制*/ +.icon-copy:before { font-size: 17px; content: "\e690"; } +/*修改*/ +.icon-upd:before { font-size: 17px; content: "\e601"; } +.icon-upd1:before { font-size: 17px; content: "\e604"; } +.icon-upd2:before { font-size: 17px; content: "\e62f"; } +/* 修改 */ +.icon-default:before { font-size: 17px; content: "\e76e"; } +/* 文章 */ +.icon-article:before { font-size: 80px; content: "\e608";} +/** 编辑 */ +.icon-editor-menu:before { font-size: 18px; content: "\e6f7";padding-left: 3px;margin-right: 9px; } +.icon-editor1:before { font-size: 20px; content: "\e604";} +.icon-editor2:before { font-size: 15px; content: "\e62f";} +/** 点击量 */ +.icon-hits1:before { font-size: 15px; content: "\e659";} +.icon-hits2:before { font-size: 15px; content: "\e61c";} +.icon-hits3:before { font-size: 15px; content: "\e6a3";} +.icon-hits4:before { font-size: 15px; content: "\e60c";} +.icon-hits5:before { font-size: 13px; content: "\e660";} +.icon-hits6:before { font-size: 15px; content: "\e669";} +/** 阅读量 */ +.icon-read1:before { font-size: 15px; content: "\e60f";} +/** 点赞 */ +.icon-applaud1:before { font-size: 20px; content: "\e617";} +.icon-applaud2:before { font-size: 15px; content: "\e60d";} +.icon-applaud3:before { font-size: 13px; content: "\e69d";} +/**文档*/ +.icon-doctask:before { font-size: 90px; content: "\e605";} +.icon-docadd:before { font-size: 80px; content: "\e607";} +.icon-docedit:before { font-size: 90px; content: "\e60b";} + +.icon-none:before { font-size: 20px; content: "\e610";} +.icon-find:before { font-size: 20px; content: "\e603";} + +/**撤销*/ +.icon-repeal:before { font-size: 15px; content: "\e636";} +/**下载*/ +.icon-download1:before { font-size: 16px; content: "\e620";} +/**二维码*/ +.icon-qrcode:before { font-size: 15px; content: "\e683";} +/**WEB*/ +.icon-web:before { font-size: 15px; content: "\e680";} + +/** +截图 + */ +.icon-capture1:before { font-size: 25px; content: "\e62e"; } +.icon-capture14:before { font-size: 14px; content: "\e62e"; } +.icon-capture2:before { font-size: 25px; content: "\e6d9"; } + +/** +用户 + */ +.icon-login1:before { font-size: 12px; content: "\e635"; } +.icon-login2:before { font-size: 20px; content: "\e6a9"; } +.icon-password:before { font-size: 20px; content: "\e679"; } + +/** +APP图标 + */ +.icon-tiaose:before { font-size: 29px; content: "\e6f2"; } +.icon-sheji1:before { font-size: 20px; content: "\e685"; } +.icon-sheji2:before { font-size: 20px; content: "\e60d"; } +.icon-element:before { font-size: 20px; content: "\e605"; } +.icon-txcloud:before { font-size: 28px; content: "\e747"; } +.icon-gamersky:before { font-size: 25px; content: "\e60c"; } +.icon-qncloud:before { font-size: 20px; content: "\e661"; } +.icon-baidu:before { font-size: 29px; content: "\e602"; } +.icon-aliyun:before { font-size: 20px; content: "\e6b0"; } +.icon-bilibili:before { font-size: 25px; content: "\e617"; } +.icon-github:before { font-size: 20px; content: "\e67c"; } +.icon-electron:before { font-size: 20px; content: "\eb86"; } + +.icon-yuque:before { font-size: 20px; content: "\e6"; } +.icon-chandan:before { font-size: 20px; content: "\e7fa"; } +.icon-m:before { font-size: 20px; content: "\e613"; } +.icon-p:before { font-size: 20px; content: "\e657"; } + +.icon-wechat:before {font-size: 16px; content: "\e621"; margin-left: 5px;margin-right: 5px;} +.icon-wechat-mp-menu-1:before {font-size: 16px; content: "\e615"; margin-left: 5px;margin-right: 5px;} +.icon-wechat-mp-menu-2:before {font-size: 16px; content: "\e668"; margin-left: 5px;margin-right: 5px;} +.icon-wechat-mp-menu-3:before {font-size: 16px; content: "\e640"; margin-left: 5px;margin-right: 5px;} + + + + + + + + + + diff --git a/blossom-web/src/assets/styles/iconfont/blossom/iconfont.css b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.css new file mode 100644 index 0000000..ed1d151 --- /dev/null +++ b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.css @@ -0,0 +1,423 @@ +@font-face { + font-family: "iconbl"; /* Project id 4118609 */ + src: url('iconfont.woff2?t=1688457329856') format('woff2'), + url('iconfont.woff?t=1688457329856') format('woff'), + url('iconfont.ttf?t=1688457329856') format('truetype'); +} + +.iconbl { + font-family: "iconbl" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bl-a-online-line:before { + content: "\ea0b"; +} + +.bl-problem-line:before { + content: "\ea0c"; +} + +.bl-a-radiochoose-line:before { + content: "\ea0d"; +} + +.bl-a-rightto-line:before { + content: "\ea11"; +} + +.bl-switch-line:before { + content: "\ea40"; +} + +.bl-a-underline-line:before { + content: "\ea41"; +} + +.bl-a-doubleunderline-line:before { + content: "\ea09"; +} + +.bl-a-doubleonline-line:before { + content: "\ea0a"; +} + +.bl-a-texteditorback-line:before { + content: "\ea3a"; +} + +.bl-a-texteditorsuperscript-line:before { + content: "\ea3b"; +} + +.bl-a-texteditorsubscript-line:before { + content: "\ea3c"; +} + +.bl-a-texteditorforward-line:before { + content: "\ea3e"; +} + +.bl-a-underbox-line:before { + content: "\ea3f"; +} + +.bl-a-icon_jiandaojianqie:before { + content: "\e650"; +} + +.bl-jianpan-xianxing:before { + content: "\e8ce"; +} + +.bl-admonish-line:before { + content: "\e9d2"; +} + +.bl-a-eyeclose-line:before { + content: "\ea01"; +} + +.bl-a-fileadd-fill:before { + content: "\ea02"; +} + +.bl-a-listview-line:before { + content: "\ea08"; +} + +.bl-login-circle-line:before { + content: "\e70e"; +} + +.bl-logout-circle-line:before { + content: "\e71a"; +} + +.bl-a-Securitypermissions-line:before { + content: "\ea1e"; +} + +.bl-a-servercloud-line:before { + content: "\ea1f"; +} + +.bl-user-line:before { + content: "\ea3d"; +} + +.bl-effect-line:before { + content: "\e9e3"; +} + +.bl-expansion-line:before { + content: "\e9ec"; +} + +.bl-eye-line:before { + content: "\e9ee"; +} + +.bl-a-Informationquestions-line:before { + content: "\e9ff"; +} + +.bl-like-line:before { + content: "\e9fe"; +} + +.bl-read-line:before { + content: "\ea07"; +} + +.bl-search-line:before { + content: "\ea1c"; +} + +.bl-a-texteditorpastetext-line:before { + content: "\ea37"; +} + +.bl-transcript-line:before { + content: "\ea38"; +} + +.bl-transcript-fill:before { + content: "\ea39"; +} + +.bl-a-computerend-line:before { + content: "\e9fd"; +} + +.bl-cloud-fill:before { + content: "\e9e4"; +} + +.bl-a-lowerrightpage-line:before { + content: "\e9f9"; +} + +.bl-a-lowerrightpage-fill:before { + content: "\e9fa"; +} + +.bl-model-fill:before { + content: "\e9fb"; +} + +.bl-model-line:before { + content: "\e9fc"; +} + +.bl-a-filetext-line:before { + content: "\e9f7"; +} + +.bl-a-folderon-line:before { + content: "\e9f8"; +} + +.bl-a-linkspread-line:before { + content: "\e9f6"; +} + +.bl-planet-line:before { + content: "\ea05"; +} + +.bl-a-qrcode1-line:before { + content: "\ea06"; +} + +.bl-a-filechoose-line:before { + content: "\e9f1"; +} + +.bl-a-filehistory-line:before { + content: "\e9f2"; +} + +.bl-a-filelink-line:before { + content: "\e9f3"; +} + +.bl-a-fileswitch-line13:before { + content: "\e9f4"; +} + +.bl-a-fileseal-line:before { + content: "\e9f5"; +} + +.bl-a-clock3-line:before { + content: "\e9e5"; +} + +.bl-a-fileadd-line:before { + content: "\e9e6"; +} + +.bl-a-filecycle-line:before { + content: "\e9e7"; +} + +.bl-a-fileedit-line:before { + content: "\e9e8"; +} + +.bl-a-filemedal-line:before { + content: "\e9e9"; +} + +.bl-a-fileprohibit-line:before { + content: "\e9ea"; +} + +.bl-a-filesend-line:before { + content: "\e9eb"; +} + +.bl-a-fileminus-line:before { + content: "\e9ef"; +} + +.bl-a-filestar-line:before { + content: "\e9f0"; +} + +.bl-pen-line:before { + content: "\ea03"; +} + +.bl-sendmail-fill:before { + content: "\ea1d"; +} + +.bl-star-fill:before { + content: "\ea29"; +} + +.bl-star-line:before { + content: "\ea2a"; +} + +.bl-a-switchoff-line:before { + content: "\ea35"; +} + +.bl-a-switchon-line:before { + content: "\ea36"; +} + +.bl-a-rightsmallline-line:before { + content: "\ea99"; +} + +.bl-a-cloudrefresh-line:before { + content: "\e9e2"; +} + +.bl-tally-line:before { + content: "\ea31"; +} + +.bl-a-texteditorsave-line:before { + content: "\ea34"; +} + +.bl-adhibition-line:before { + content: "\e9d1"; +} + +.bl-wifi-line:before { + content: "\ea42"; +} + +.bl-a-clouddownload-line:before { + content: "\e9dc"; +} + +.bl-a-cloudchoose-line:before { + content: "\e9dd"; +} + +.bl-cloud-line:before { + content: "\e9de"; +} + +.bl-a-cloudupload-line:before { + content: "\e9e0"; +} + +.bl-task-line:before { + content: "\e675"; +} + +.bl-layout-column-line:before { + content: "\e638"; +} + +.bl-bold:before { + content: "\e678"; +} + +.bl-italic:before { + content: "\e679"; +} + +.bl-heading:before { + content: "\e67a"; +} + +.bl-indent-increase:before { + content: "\e67b"; +} + +.bl-double-quotes-r:before { + content: "\e67c"; +} + +.bl-list-unordered:before { + content: "\e67d"; +} + +.bl-link-m:before { + content: "\e67e"; +} + +.bl-indent-decrease:before { + content: "\e67f"; +} + +.bl-separator:before { + content: "\e680"; +} + +.bl-list-ordered:before { + content: "\e681"; +} + +.bl-code-view:before { + content: "\e682"; +} + +.bl-strikethrough:before { + content: "\e683"; +} + +.bl-single-quotes-r:before { + content: "\e684"; +} + +.bl-table-:before { + content: "\e685"; +} + +.bl-image--line:before { + content: "\e6d3"; +} + +.bl-apps-line:before { + content: "\e6ed"; +} + +.bl-refresh-line:before { + content: "\e713"; +} + +.bl-copy-line:before { + content: "\e9df"; +} + +.bl-bug-line:before { + content: "\e9da"; +} + +.bl-shrink-line:before { + content: "\ea27"; +} + +.bl-a-home1-line:before { + content: "\e9ed"; +} + +.bl-a-package2-line:before { + content: "\ea00"; +} + +.bl-picture-line:before { + content: "\ea04"; +} + +.bl-a-texteditorhighlightcolor-line:before { + content: "\ea32"; +} + +.bl-tier-line:before { + content: "\ea33"; +} + diff --git a/blossom-web/src/assets/styles/iconfont/blossom/iconfont.ttf b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.ttf new file mode 100644 index 0000000..71b5dee Binary files /dev/null and b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.ttf differ diff --git a/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff new file mode 100644 index 0000000..63f8ffb Binary files /dev/null and b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff differ diff --git a/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff2 b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff2 new file mode 100644 index 0000000..445c40c Binary files /dev/null and b/blossom-web/src/assets/styles/iconfont/blossom/iconfont.woff2 differ diff --git a/blossom-web/src/assets/styles/iconfont/weblogo/iconfont.js b/blossom-web/src/assets/styles/iconfont/weblogo/iconfont.js new file mode 100644 index 0000000..95296ea --- /dev/null +++ b/blossom-web/src/assets/styles/iconfont/weblogo/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_4163717='',function(h){var l=(l=document.getElementsByTagName("script"))[l.length-1],a=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var c,t,z,p,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}c=function(){var l,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_4163717,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(z=c,p=h.document,v=!1,M(),p.onreadystatechange=function(){"complete"==p.readyState&&(p.onreadystatechange=null,m())})}function m(){v||(v=!0,z())}function M(){try{p.documentElement.doScroll("left")}catch(l){return void setTimeout(M,50)}m()}}(window); \ No newline at end of file diff --git a/blossom-web/src/assets/styles/mixin-fuc.scss b/blossom-web/src/assets/styles/mixin-fuc.scss new file mode 100644 index 0000000..435d971 --- /dev/null +++ b/blossom-web/src/assets/styles/mixin-fuc.scss @@ -0,0 +1,68 @@ +// 盒子基础设置 +@mixin box($w: 100%, $h: 100%, $minW: null, $maxW: null, $minH: null, $maxH: null, ) { + @if $w { + width: $w; + } + + @if $h { + height: $h; + } + + @if $minW { + min-width: $minW; + } + + @if $maxW { + max-width: $maxW; + } + + @if $minH { + min-height: $minH; + } + + @if $maxH { + max-height: $maxH; + } +} + +/** + * 字体基础属性 + * fz : 字体大小 + * fw : 字体粗细 + */ +@mixin font($fz, $fw: null, $fm: null) { + font-size: $fz; + + @if $fw { + font-weight: $fw; + } + + @if $fm { + font-family: $fm; + } +} + +/** + * 边框 + */ +@mixin border($borderWidth, $borderColor, $radio: 0) { + border: $borderWidth solid; + border-radius: $radio; + + @if $borderColor { + border-color: $borderColor; + } +} + +/** + * flex布局属性 + * dir : 对齐方式 + * main : 主轴 + * cross : 交叉轴 + */ +@mixin flex($dir: row, $main: flex-start, $cross: stretch) { + display: flex; + flex-direction: $dir; + justify-content: $main; + align-items: $cross; +} \ No newline at end of file diff --git a/blossom-web/src/assets/utils/obj.ts b/blossom-web/src/assets/utils/obj.ts new file mode 100644 index 0000000..ec3d3f9 --- /dev/null +++ b/blossom-web/src/assets/utils/obj.ts @@ -0,0 +1,64 @@ +export const isNull = (val: any): boolean => { + if (typeof val === 'boolean') { + return false + } + if (typeof val === 'number') { + return false + } + + // 控制 + if (val == null || val === 'undefined' || val === undefined || val === '') { + return true; + } + + // 数组 + if (Array.isArray(val) && val.length === 0) { + return true; + } + + // 对象, 但无字段 + if (val instanceof Object && JSON.stringify(val) === '{}') { + return true; + } + return false; +} + +export const isNotNull = (val: any): boolean => { + return !isNull(val); +} + + +export const isBlank = (str: string | undefined | null): boolean => { + if (str === undefined) { + return true + } + if (str == null) { + return true + } + if (str == '') { + return true + } + if (str == ' ') { + return true + } + if (str.length == 0) { + return true + } + return false +} + +export const isNotBlank = (str: string | undefined | null): boolean => { + return !isBlank(str) +} + +export const isEmpty = (val: any): boolean => { + if (val == undefined || val == null) { + return true + } + + if (val.length == 0) { + return true; + } + + return false; +} \ No newline at end of file diff --git a/blossom-web/src/assets/utils/storage.ts b/blossom-web/src/assets/utils/storage.ts new file mode 100644 index 0000000..7282a27 --- /dev/null +++ b/blossom-web/src/assets/utils/storage.ts @@ -0,0 +1,57 @@ +import SYSTEM from '@/assets/constants/blossom'; + +const prefix = SYSTEM.SHORT_NAME + '_'; + +/** + * window.localStorage 浏览器永久缓存 + * @method set 设置永久缓存 + * @method get 获取永久缓存 + * @method remove 移除永久缓存 + * @method clear 移除全部永久缓存 + */ +export const Local = { + // 设置永久缓存 + set(key: string, val: any) { + window.localStorage.setItem(key, JSON.stringify(val)); + }, + // 获取永久缓存 + get(key: string): any { + let json = window.localStorage.getItem(key); + return JSON.parse(json); + }, + // 移除永久缓存 + remove(key: string) { + window.localStorage.removeItem(key); + }, + // 移除全部永久缓存 + clear() { + window.localStorage.clear(); + }, +}; + +/** + * window.sessionStorage 浏览器临时缓存 + * @method set 设置临时缓存 + * @method get 获取临时缓存 + * @method remove 移除临时缓存 + * @method clear 移除全部临时缓存 + */ +export const Session = { + // 设置临时缓存 + set(key: string, val: any) { + window.sessionStorage.setItem(key, JSON.stringify(val)); + }, + // 获取临时缓存 + get(key: string) { + let json = window.sessionStorage.getItem(key); + return JSON.parse(json); + }, + // 移除临时缓存 + remove(key: string) { + window.sessionStorage.removeItem(key); + }, + // 移除全部临时缓存 + clear() { + window.sessionStorage.clear(); + }, +}; \ No newline at end of file diff --git a/blossom-web/src/assets/utils/util.ts b/blossom-web/src/assets/utils/util.ts new file mode 100644 index 0000000..a2ec9bd --- /dev/null +++ b/blossom-web/src/assets/utils/util.ts @@ -0,0 +1,255 @@ +import { isNull } from "./obj" + +/** + * 跳转新开页面 + * @param url 地址 + */ +export const toView = (url:string):void => { + window.open(url) +} + +/** + * 获取当前时间的 yyyy-MM-dd HH:mm:ss + * @returns {string} + */ +export const getNowTimeFormat = (): string => { + const now = new Date() + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s; +} + +/** + * 将 秒级时间戳 转为 yyyy-MM-dd HH:mm:ss + * @param seconds 秒级时间戳 + */ +export const secondsToDatetime = (seconds: number | string | Date): string => { + const now = new Date(Number(seconds) * 1000) + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s; +} + +/** + * 将 毫秒时间戳 转为 yyyy-MM-dd HH:mm:ss.sss + * @param timestamp 毫秒时间戳 + */ +export const timestampToDatetime = (timestamp: number | string | Date): string => { + const now = new Date(timestamp) + let y = now.getFullYear() + let m = formatNum(now.getMonth() + 1) + let d = formatNum(now.getDate()) + let h = formatNum(now.getHours()) + let min = formatNum(now.getMinutes()) + let s = formatNum(now.getSeconds()) + let SSS = formatNum(now.getMilliseconds()) + return '' + y + '-' + m + '-' + d + ' ' + h + ':' + min + ':' + s + '.' + SSS; +} + +const formatNum = (num: string | number) => { + if (num < 10) { + return '0' + num + } + return num +}; + +/** + * 格式化JSON字符串 + * @param msg + * @param customRetract 缩进 + * @returns {string} + */ +export const formatJson = (msg: string, customRetract?: string): string => { + // 格式化缩进为2个空格 + const retract = isNull(customRetract) ? ' ' : customRetract; + let rep = '~'; + let jsonStr = JSON.stringify(msg, null, rep); + let str = ''; + for (let i = 0; i < jsonStr.length; i++) { + let text2 = jsonStr.charAt(i); + if (i > 1) { + let text = jsonStr.charAt(i - 1); + if (rep !== text && rep === text2) { + // str += '\n' + } + } + str += text2 + } + jsonStr = ''; + for (let i = 0; i < str.length; i++) { + let text = str.charAt(i); + if (rep === text) { + jsonStr += retract; + } else { + jsonStr += text + } + if (i === str.length - 2) { + jsonStr += '\n' + } + } + return jsonStr; +}; + +export const getSolar = (): string => { + let today = new Date(); + let solar = getSolarTermByYearMonthDay(today.getMonth() + 1, today.getDate()); + // console.log('当前日期[%s-%s]的最后一次节气: %s', today.getMonth() + 1, today.getDate(), solar); + return solar; +} + +export const getSolarTermByYearMonthDay = (month: number, day: any): string => { + // return '冬至'; + if (month == 1) { + if (day >= 5) return "小寒"; + if (day >= 19) return "大寒" + } + if (month == 2) { + if (day < 3) return "大寒"; + if (day >= 3) return "立春"; + if (day >= 18) return "雨水" + } + if (month == 3) { + if (day < 5) return "雨水"; + if (day >= 5) return "惊蛰"; + if (day >= 20) return "春分" + } + if (month == 4) { + if (day < 4) return "春分"; + if (day >= 4) return "清明"; + if (day >= 19) return "谷雨" + } + if (month == 5) { + if (day < 5) return "谷雨"; + if (day >= 5) return "立夏"; + if (day >= 20) return "小满" + } + if (month == 6) { + if (day < 5) return "小满"; + if (day >= 5) return "芒种"; + if (day >= 21) return "夏至" + } + if (month == 7) { + if (day < 7) return "夏至"; + if (day >= 7) return "小暑"; + if (day >= 22) return "大暑" + } + if (month == 8) { + if (day < 6) return "大暑"; + if (day >= 6) return "立秋"; + if (day >= 22) return "处暑" + } + if (month == 9) { + if (day < 7) return "处暑"; + if (day >= 7) return "白露"; + if (day >= 22) return "秋分" + } + if (month == 10) { + if (day < 7) return "秋分"; + if (day >= 7) return "寒露"; + if (day >= 23) return "霜降" + } + if (month == 11) { + if (day < 7) return "霜降"; + if (day >= 7) return "立冬"; + if (day >= 22) return "小雪" + } + if (month == 12) { + if (day < 5) return "小雪"; + if (day >= 5) return "大雪"; + if (day >= 21) return "冬至" + } + return ''; +} + +export interface SolarColor { + firstColor: string; + firstColorFlip: string; + secondColor: string; + secondColorFlip: string, +} + +export const getSolorColor = (): SolarColor => { + const currentSolar = getSolar(); + + if (currentSolar == '小寒') { + return { firstColor: '#283233', secondColor: '#973131', firstColorFlip: '#42494a', secondColorFlip: '#a65050' } + } + if (currentSolar == '大寒') { + return { firstColor: '#c6c0ba', secondColor: '#576975', firstColorFlip: '#b5b0aa', secondColorFlip: '#4c5c66' } + } + if (currentSolar == '立春') { + return { firstColor: '#f6da8c', secondColor: '#c4beb9', firstColorFlip: '#e6cb83', secondColorFlip: '#ada8a4' } + } + if (currentSolar == '雨水') { + return { firstColor: '#ccaf9b', secondColor: '#463936', firstColorFlip: '#b59b8a', secondColorFlip: '#a68e7e' } + } + if (currentSolar == '惊蛰') { + return { firstColor: '#c9c1ba', secondColor: '#508e69', firstColorFlip: '#a9c2b3', secondColorFlip: '#c9c1ba' } + } + if (currentSolar == '春分') { + return { firstColor: '#c3bfba', secondColor: '#f5d791', firstColorFlip: '#d9c793', secondColorFlip: '#d6d2cc' } + } + if (currentSolar == '清明') { + return { firstColor: '#ded2c3', secondColor: '#b7bc4b', firstColorFlip: '#c1c2a1', secondColorFlip: '#f4e7d7' } + } + if (currentSolar == '谷雨') { + return { firstColor: '#d3e0ea', secondColor: '#529e43', firstColorFlip: '#b6d4b0', secondColorFlip: '#d3e0ea' } + } + if (currentSolar == '立夏') { + return { firstColor: '#efd58e', secondColor: '#6cc1ff', firstColorFlip: '#f7e7ba', secondColorFlip: '#83cbff' } + } + if (currentSolar == '小满') { + return { firstColor: '#d19c46', secondColor: '#53915a', firstColorFlip: '#edaf54', secondColorFlip: '#50a356' } + } + if (currentSolar == '芒种') { + return { firstColor: '#87c0f9', secondColor: '#b08738', firstColorFlip: '#7294ad', secondColorFlip: '#edaf54' } + } + if (currentSolar == '夏至') { + return { firstColor: '#dfeaf2', secondColor: '#8ed2b1', firstColorFlip: '#a2cfe5', secondColorFlip: '#b8d3da' } + } + if (currentSolar == '小暑') { + return { firstColor: '#60d7a0', secondColor: '#83bd6f', firstColorFlip: '#83bd6f', secondColorFlip: '#65ab63' } + } + if (currentSolar == '大暑') { + return { firstColor: '#05aae4', secondColor: '#9de7ed', firstColorFlip: '#3c9ee8', secondColorFlip: '#96e0e2' } + } + if (currentSolar == '立秋') { + return { firstColor: '#dbbc30', secondColor: '#a08373', firstColorFlip: '#fcd42b', secondColorFlip: '#885629' } + } + if (currentSolar == '处暑') { + return { firstColor: '#b6e2e3', secondColor: '#8ed2af', firstColorFlip: '#8dd0d1', secondColorFlip: '#35b578' } + } + if (currentSolar == '白露') { + return { firstColor: '#bdd8a5', secondColor: '#406b5e', firstColorFlip: '#9dd4bc', secondColorFlip: '#274238' } + } + if (currentSolar == '秋分') { + return { firstColor: '#f0ce8f', secondColor: '#c1ac9a', firstColorFlip: '#d9ba81', secondColorFlip: '#ab9888' } + } + if (currentSolar == '寒露') { + return { firstColor: '#edcbae', secondColor: '#c3beba', firstColorFlip: '#d6b79d', secondColorFlip: '#b0aba8' } + } + if (currentSolar == '霜降') { + return { firstColor: '#f2e6e2', secondColor: '#997b50', firstColorFlip: '#ded3cf', secondColorFlip: '#826944' } + } + if (currentSolar == '立冬') { + return { firstColor: '#3d6989', secondColor: '#abc9df', firstColorFlip: '#335873', secondColorFlip: '#8fa8ba' } + } + if (currentSolar == '小雪') { + return { firstColor: '#c3dcf4', secondColor: '#b0cced', firstColorFlip: '#b1c8de', secondColorFlip: '#9bb4d1' } + } + if (currentSolar == '大雪') { + return { firstColor: '#afc8e7', secondColor: '#84a8d2', firstColorFlip: '#a0b7d4', secondColorFlip: '#7797bd' } + } + if (currentSolar == '冬至') { + return { firstColor: '#ede1cc', secondColor: '#a3c1ce', firstColorFlip: '#d4c9b6', secondColorFlip: '#8da7b3' } + } + return { firstColor: '#ede1cc', secondColor: '#a3c1ce', firstColorFlip: '#d4c9b6', secondColorFlip: '#8da7b3' } +} \ No newline at end of file diff --git a/blossom-web/src/components/BLTag.vue b/blossom-web/src/components/BLTag.vue new file mode 100644 index 0000000..69c7b8a --- /dev/null +++ b/blossom-web/src/components/BLTag.vue @@ -0,0 +1,78 @@ + + + + \ No newline at end of file diff --git a/blossom-web/src/components/NotFound.vue b/blossom-web/src/components/NotFound.vue new file mode 100644 index 0000000..7eadaff --- /dev/null +++ b/blossom-web/src/components/NotFound.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/blossom-web/src/components/Template.vue b/blossom-web/src/components/Template.vue new file mode 100644 index 0000000..eaf1156 --- /dev/null +++ b/blossom-web/src/components/Template.vue @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/components/Test.vue b/blossom-web/src/components/Test.vue new file mode 100644 index 0000000..6935766 --- /dev/null +++ b/blossom-web/src/components/Test.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/main.ts b/blossom-web/src/main.ts new file mode 100644 index 0000000..205f650 --- /dev/null +++ b/blossom-web/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from 'vue' +import pinia from './stores/storeConfig' +import App from './App.vue' +import router from './router' +import './router/routeInit' + +import 'element-plus/theme-chalk/dark/css-vars.css' +import '@/assets/styles/css/theme.css' +import '@/assets/styles/css/main.css' +import '@/assets/styles/iconfont/blossom/iconfont.css' +import '@/assets/styles/iconfont/weblogo/iconfont.js' + +const app = createApp(App); + +app.use(pinia) +app.use(router) +app.mount('#app'); diff --git a/blossom-web/src/router/index.ts b/blossom-web/src/router/index.ts new file mode 100644 index 0000000..c464bbe --- /dev/null +++ b/blossom-web/src/router/index.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHashHistory } from 'vue-router'; + +const router = createRouter({ + history: createWebHashHistory(import.meta.env.BASE_URL), + routes: [] +}) + +export const toRoute = (path: string): void => { + router.push(path) +} + +export default router diff --git a/blossom-web/src/router/routeInit.ts b/blossom-web/src/router/routeInit.ts new file mode 100644 index 0000000..9282b60 --- /dev/null +++ b/blossom-web/src/router/routeInit.ts @@ -0,0 +1,27 @@ +import router from '@/router' + +// components +const NotFound = () => import("../components/NotFound.vue") + +// blossom +const IndexBlossom = () => import('../views/Index.vue') +const BlossomHome = () => import('../views/index/Home.vue') +const Articles = () => import('../views/article/Articles.vue') +const Test = () => import('../components/Test.vue') + + +router.addRoute({ path: '/404', component: NotFound, }) +router.addRoute({ path: '/:pathMatch(.*)', redirect: '/404', }) +router.addRoute({ + path: '/', redirect: '/home' +}) + +// blossom +router.addRoute({ + path: '/', name: 'IndexBlossom', component: IndexBlossom, meta: { keepAlive: false }, + children: [ + { path: '/home', name: 'BlossomHome', component: BlossomHome, meta: { keepAlive: false } }, + { path: '/articles', name: 'Articles', component: Articles, meta: { keepAlive: false } }, + { path: '/test', name: 'Test', component: Test, meta: { keepAlive: false } } + ] +}) \ No newline at end of file diff --git a/blossom-web/src/stores/storeConfig.ts b/blossom-web/src/stores/storeConfig.ts new file mode 100644 index 0000000..bd56c3b --- /dev/null +++ b/blossom-web/src/stores/storeConfig.ts @@ -0,0 +1,3 @@ +import { createPinia } from 'pinia'; +const pinia = createPinia(); +export default pinia; \ No newline at end of file diff --git a/blossom-web/src/views/Index.vue b/blossom-web/src/views/Index.vue new file mode 100644 index 0000000..382431e --- /dev/null +++ b/blossom-web/src/views/Index.vue @@ -0,0 +1,27 @@ + + \ No newline at end of file diff --git a/blossom-web/src/views/article/Articles.vue b/blossom-web/src/views/article/Articles.vue new file mode 100644 index 0000000..b2a6d10 --- /dev/null +++ b/blossom-web/src/views/article/Articles.vue @@ -0,0 +1,792 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/article/DocTitle.vue b/blossom-web/src/views/article/DocTitle.vue new file mode 100644 index 0000000..6c25cdd --- /dev/null +++ b/blossom-web/src/views/article/DocTitle.vue @@ -0,0 +1,104 @@ + + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/article/index.d.ts b/blossom-web/src/views/article/index.d.ts new file mode 100644 index 0000000..4011d07 --- /dev/null +++ b/blossom-web/src/views/article/index.d.ts @@ -0,0 +1,51 @@ +declare interface DocTree { + /** ID */ + i: number, + /** Name */ + n: string, + /** open: 0:否;1:是; */ + o: number, + /** Tags */ + t: string[], + icon: string, + /** 文档类型: 1:文件夹|2:文章; */ + ty: DocType, + star: number, + children?: DocTree[] +} + +/** + * 文章详情 + */ +declare interface DocInfo { + id: number, + pid: number, + name: string, + icon?: string, + tags: string[], + sort: number, + cover?: string, + describes?: string, + starStatus: number, + pv?: number, + uv?: number, + likes?: number, + words?: number, + openVersion?: number, + version?: number, + storePath?: string, + subjectWords?: string, + subjectUpdTime?: string, + type: DocType, + creTime?: string, + updTime?: string, + html?: string, + // 公开文章的信息 + openStatus: number, + openTime?: string, + openVersion?: number, + syncTime?: string, +} + +/** 文档类型: 1:文件夹|2:文章; */ +declare type DocType = 1 | 2 | 3; \ No newline at end of file diff --git a/blossom-web/src/views/index/ChartHeatmap.vue b/blossom-web/src/views/index/ChartHeatmap.vue new file mode 100644 index 0000000..eaad0a0 --- /dev/null +++ b/blossom-web/src/views/index/ChartHeatmap.vue @@ -0,0 +1,247 @@ + + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/index/ChartLineWords.vue b/blossom-web/src/views/index/ChartLineWords.vue new file mode 100644 index 0000000..aa6a8d7 --- /dev/null +++ b/blossom-web/src/views/index/ChartLineWords.vue @@ -0,0 +1,245 @@ + + + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/index/Home.vue b/blossom-web/src/views/index/Home.vue new file mode 100644 index 0000000..3315b39 --- /dev/null +++ b/blossom-web/src/views/index/Home.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/index/HomeSubject.vue b/blossom-web/src/views/index/HomeSubject.vue new file mode 100644 index 0000000..f2a69cf --- /dev/null +++ b/blossom-web/src/views/index/HomeSubject.vue @@ -0,0 +1,178 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/index/HomeUserInfo.vue b/blossom-web/src/views/index/HomeUserInfo.vue new file mode 100644 index 0000000..cf754c3 --- /dev/null +++ b/blossom-web/src/views/index/HomeUserInfo.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/src/views/index/IndexHeader.vue b/blossom-web/src/views/index/IndexHeader.vue new file mode 100644 index 0000000..f2ecf35 --- /dev/null +++ b/blossom-web/src/views/index/IndexHeader.vue @@ -0,0 +1,162 @@ + + + + + \ No newline at end of file diff --git a/blossom-web/stats.html b/blossom-web/stats.html new file mode 100644 index 0000000..1fb4430 --- /dev/null +++ b/blossom-web/stats.html @@ -0,0 +1,6177 @@ + + + + + + + + Rollup Visualizer + + + +
    + + + + + diff --git a/blossom-web/tsconfig.config.json b/blossom-web/tsconfig.config.json new file mode 100644 index 0000000..3dbe5df --- /dev/null +++ b/blossom-web/tsconfig.config.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.node.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "types": [ + "node" + ], + "lib": [ + + ] + } +} \ No newline at end of file diff --git a/blossom-web/tsconfig.json b/blossom-web/tsconfig.json new file mode 100644 index 0000000..83b7a87 --- /dev/null +++ b/blossom-web/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@vue/tsconfig/tsconfig.web.json", + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue", + "src/**/*.d.ts", + "declaration.d.ts" + ], + "compilerOptions": { + "baseUrl": "./", + "types": [ + "node" + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "references": [ + { + "path": "./tsconfig.config.json" + } + ] +} \ No newline at end of file diff --git a/blossom-web/vite.config.ts b/blossom-web/vite.config.ts new file mode 100644 index 0000000..cf82b45 --- /dev/null +++ b/blossom-web/vite.config.ts @@ -0,0 +1,71 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import { visualizer } from 'rollup-plugin-visualizer'; +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +// 为 Element Plus 按需引入样式。 +import ElementPlus from 'unplugin-element-plus/vite' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: './', + server: { + host: "0.0.0.0", + port: 5174, + //vue3 vite配置热更新不用手动刷新 + hmr: true + }, + plugins: [ + vue(), + vueJsx(), + visualizer({ + emitFile: false, + filename: "stats.html", //分析图生成的文件名 + open: true //如果存在本地服务端口,将在打包后自动展示 + }), + // ElementPlus 按需引入的插件 + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ElementPlus({ + // options + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + css: { + preprocessorOptions: { + scss: { + /**如果引入多个文件,可以使用 + * '@import "@/assets/scss/globalVariable1.scss"; + * @import"@/assets/scss/globalVariable2.scss";' + **/ + additionalData: '@import "@/assets/styles/config.scss";', + } + } + }, + build: { + // 警告大小, 单位kb + // chunkSizeWarningLimit: 1000, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + } + } + } + } +}) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/imgs/home_ld.jpg b/doc/imgs/home_ld.jpg new file mode 100644 index 0000000..9ad507a Binary files /dev/null and b/doc/imgs/home_ld.jpg differ