149 Commits

Author SHA1 Message Date
meilin.huang
63f0615445 feat: v1.7.0 2024-01-17 17:02:15 +08:00
kanzihuang
94da6df33e !84 fix: 修复数据库备份与恢复问题
* refactor dbScheduler
* fix: 按团队名称检索团队
* feat: 创建数据库资源时支持全选数据库
* refactor dbScheduler
* fix: 修复数据库备份与恢复问题
2024-01-17 08:37:22 +00:00
meilin.huang
cc3981d99c fix: 数据库编辑导致标签关联删除修复、cron组件调整 2024-01-17 12:13:18 +08:00
meilin.huang
8332d9b354 feat: 新增cron组件、cron支持6位秒级别 2024-01-16 20:04:04 +08:00
meilin.huang
493925064c feat: 机器定时删除终端操作记录 2024-01-15 20:51:41 +08:00
kanzihuang
c0232c4c75 fix: 修复数据库备份与恢复问题 (#85)
* fix: 修复数据库备份与恢复问题

* fix: 修复数据库备份与恢复问题
2024-01-15 20:11:28 +08:00
zongyangleo
b873855b44 !82 feat: dbms支持oracle数据库
* fix:oracle bug修复
* feat: dbms支持oracle数据库
2024-01-15 11:55:59 +00:00
meilin.huang
9c524292f0 refactor: 数组比较方法优化等 2024-01-13 13:38:53 +08:00
meilin.huang
bfd346e65a fix: 机器文件下载问题修复&dbm重构 2024-01-12 13:15:30 +08:00
meilin.huang
bc811cbd49 refactor: 数据同步编辑页优化等 2024-01-11 12:35:44 +08:00
kanzihuang
bbec3eca0d feat: 实现数据库备份和恢复并发调度 (#84) 2024-01-11 11:35:51 +08:00
meilin.huang
3857d674ba refactor: 数据同步编辑页调整、echarts组件重构 2024-01-10 23:41:55 +08:00
meilin.huang
25b0ae4d2f fix: model.FillBaseInfo遗漏调整完善等 2024-01-09 21:37:56 +08:00
meilin.huang
b7aa281611 refactor: code review 2024-01-09 17:31:21 +08:00
meilin.huang
3c89a285f5 feat: 数据库表格数据表头支持展示备注 2024-01-09 12:19:20 +08:00
Coder慌
d3d26c85c3 !81 fix: 数据同步相关bug修复
Merge pull request !81 from zongyangleo/dev_0109
2024-01-09 04:12:53 +00:00
刘宗洋
a764c4f974 fix: 数据同步相关bug修复 2024-01-09 10:35:13 +08:00
meilin.huang
af454f7d5d refactor: 数据同步优化, base.App、base.Repo新增Save方法 2024-01-07 21:46:25 +08:00
meilin.huang
eea759e10e refactor: code review 2024-01-06 22:36:50 +08:00
meilin.huang
e158422091 refactor: code review、数据库备份恢复支持ssh隧道操作 2024-01-05 22:16:38 +08:00
meilin.huang
5ada63d4a1 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-01-05 20:51:50 +08:00
Coder慌
5ef35001cc !80 fix: 字段映射大小写等问题
Merge pull request !80 from zongyangleo/dev_0105_table_sync_fix
2024-01-05 12:51:32 +00:00
kanzihuang
0be50f0995 feat: 新增数据库类型 mariadb, 分别为 mysql 和 mariadb 设置不同的数据库备份与恢复程序路径 (#81) 2024-01-05 17:23:29 +08:00
刘宗洋
61e1b0ca70 fix: 字段映射大小写等问题 2024-01-05 14:43:51 +08:00
zongyangleo
85910bf440 !79 feat: 支持自定义数据定时同步
* fix: 达梦数据权限问题
* feat: 支持自定义数据定时同步
2024-01-05 05:31:32 +00:00
meilin.huang
7a7a7020b4 feat: 新增系统样式配置,支持改logo图标与标题 2024-01-05 12:09:12 +08:00
kanzihuang
ae3d2659aa 重构数据库备份与恢复模块 (#80)
* fix: 保存 LastResult 时截断字符串过长部分,以避免数据库报错

* refactor: 新增 entity.DbTaskBase 和 persistence.dbTaskBase, 用于实现数据库备份和恢复任务处理相关部分

* fix: aeskey变更后,解密密码出现数组越界访问错误

* fix: 时间属性为零值时,保存到 mysql 数据库报错

* refactor db.infrastructure.service.scheduler

* feat: 实现立即备份功能

* refactor db.infrastructure.service.db_instance

* refactor: 从数据库中获取数据库备份目录、mysql文件路径等配置信息

* fix: 数据库备份和恢复问题

* fix: 修改 .gitignore 文件,忽略数据库备份目录和数据库程序目录
2024-01-05 08:55:34 +08:00
meilin.huang
76fd6675b5 refactor: code review 2023-12-29 16:48:15 +08:00
may-fly
664118a709 Merge pull request #78 from kanzihuang/feat-db-bak
feat: 实现数据库备份与恢复
2023-12-29 08:57:57 +08:00
kanzihuang
e344722794 feat: 实现数据库备份与恢复 2023-12-29 08:30:10 +08:00
meilin.huang
1a7d425f60 refactor: 动态路由重构 2023-12-28 17:21:33 +08:00
meilin.huang
a0582192bf refactor: code review 2023-12-27 19:55:36 +08:00
meilin.huang
4ac9f02d6a refactor: code review 2023-12-26 22:31:51 +08:00
meilin.huang
7e0febef8f refactor: 页面小优化 2023-12-25 17:43:42 +08:00
meilin.huang
94ed4b77d6 refactor: 单元格编辑优化 2023-12-22 17:04:06 +08:00
meilin.huang
2b419bca11 refactor: 数据库表支持editor编辑调整 2023-12-22 12:29:46 +08:00
meilin.huang
54a0f0b3c7 refactor: 数据库虚拟table卡顿优化 2023-12-22 00:47:01 +08:00
Coder慌
86bccc3b3d !78 feat: 表格支持编辑json、xml数据
Merge pull request !78 from zongyangleo/dev_1221
2023-12-21 10:55:30 +00:00
刘宗洋
0e601b5033 feat: 表格支持编辑json、xml数据 2023-12-21 15:03:11 +08:00
meilin.huang
85d745fcee feat: 数据库表单元格编辑封装 2023-12-21 13:07:02 +08:00
meilin.huang
550631c03b refactor: 达梦ssh连接调整 2023-12-20 23:01:51 +08:00
Coder慌
f29a1560aa !77 fix: 达梦支持ssh
Merge pull request !77 from zongyangleo/dev_1220
2023-12-20 13:25:45 +00:00
刘宗洋
8c4c41cf0b fix: 达梦支持ssh 2023-12-20 20:37:29 +08:00
meilin.huang
f5c90277b1 feat: 新增数据库版本查看 2023-12-20 17:29:16 +08:00
meilin.huang
2ae0cd7ab4 refactor: 数据库表操作界面优化 2023-12-19 19:23:33 +08:00
meilin.huang
1f6c14ee2f refactor: 系统模块角色分配相关优化 2023-12-18 22:39:32 +08:00
meilin.huang
574d27f6da refactor: PageTable优化 2023-12-17 14:38:53 +08:00
meilin.huang
7d62841783 refactor: rsa存储方式调整等 2023-12-17 01:43:38 +08:00
Coder慌
970d74bd70 !75 refactor: 表格日期组件大小调整
Merge pull request !75 from zongyangleo/dev_1216
2023-12-16 16:46:00 +00:00
刘宗洋
0c797f8da9 refactor: 表格日期组件大小调整 2023-12-16 23:10:38 +08:00
meilin.huang
68f8603c75 refactor: PageTable优化 2023-12-16 17:41:15 +08:00
meilin.huang
06bce33c48 refactor: 样式调整 2023-12-15 17:33:22 +08:00
Coder慌
f8837f28c3 !74 fix: 数据库查询结果日期格式化
Merge pull request !74 from zongyangleo/dev_1215_fix
2023-12-15 07:27:47 +00:00
刘宗洋
61aed08dde fix: 数据库查询结果日期格式化 2023-12-15 15:21:54 +08:00
meilin.huang
a5a813f95f refactor: sql执行列返回字段类型 2023-12-14 21:27:11 +08:00
meilin.huang
18cf2e54c4 refactor: 样式调整 2023-12-14 13:05:21 +08:00
Coder慌
5c72b1de57 !73 fix: 达梦ddl相关
Merge pull request !73 from zongyangleo/dev_1214
2023-12-14 01:30:10 +00:00
刘宗洋
6e44e90d67 fix: 达梦数据库ddl 2023-12-14 08:24:14 +08:00
Coder慌
0e699ba20e !71 fix: 达梦数据库操作bug
Merge pull request !71 from zongyangleo/dev_1213_1
2023-12-13 11:39:42 +00:00
刘宗洋
cf24c2671f fix: 达梦数据库操作bug 2023-12-13 18:39:44 +08:00
meilin.huang
d3b99ec88d Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2023-12-13 17:36:50 +08:00
meilin.huang
0b5ab090a4 refactor: 报错代码调整 2023-12-13 17:32:17 +08:00
Coder慌
454698286c !70 fix: 达梦数据库大小写敏感问题
Merge pull request !70 from zongyangleo/dev_1213
2023-12-13 09:31:14 +00:00
刘宗洋
14e0aadbba fix: 达梦数据库操作bug 2023-12-13 17:26:53 +08:00
meilin.huang
73986a834c fix: 删除机器、数据库时关联的标签未删除 2023-12-13 14:01:13 +08:00
meilin.huang
8faf1831d9 refactor: PageTable组件重构 2023-12-12 23:31:53 +08:00
Coder慌
d86ef0a9ab !69 fix: 库名提示,兼容mysql\pgsql\dm
Merge pull request !69 from zongyangleo/dev_1212
2023-12-12 14:30:51 +00:00
刘宗洋
c2bb0c589e fix: 库名提示,兼容mysql\pgsql\dm 2023-12-12 22:06:49 +08:00
Coder慌
9994f20a2c !66 refactor: sql代码提示重构
Merge pull request !66 from zongyangleo/dev_1212
2023-12-12 09:04:48 +00:00
刘宗洋
b5014c307f refactor: sql代码提示重构 2023-12-12 16:42:45 +08:00
meilin.huang
75bd4ca3df refactor: 前端样式调整 2023-12-11 17:08:52 +08:00
meilin.huang
d00bd2ed72 fix: PageTable调整后一些页面问题修复 2023-12-11 11:00:20 +08:00
meilin.huang
e444500835 refactor: PageTable组件重构、使用useFetch封装接口请求 2023-12-11 01:00:09 +08:00
meilin.huang
6709135a0b refactor: sql取消执行逻辑调整、前端使用vueuse重构部分代码 2023-12-09 16:17:26 +08:00
meilin.huang
59a7ff9ac7 feat: 常用操作界面支持Splitpane等 2023-12-07 23:57:11 +08:00
Coder慌
172c729535 !65 feat: 达梦数据库支持编辑表结构、索引
Merge pull request !65 from zongyangleo/dev_1207_dm
2023-12-07 10:45:23 +00:00
刘宗洋
ac5198db1c feat: 达梦数据库支持编辑表结构、索引 2023-12-07 17:32:32 +08:00
刘宗洋
5c5c2c2037 Merge remote-tracking branch 'upstream/dev' into dev_1207_dm 2023-12-07 14:32:07 +08:00
meilin.huang
1db990b554 refactor: 新增达梦图标、调整前端DbDialect接口 2023-12-07 11:48:17 +08:00
刘宗洋
70c887a16a fix: 支持达梦数据库查询索引 2023-12-07 10:03:50 +08:00
Coder慌
2430c4f6aa !64 feat: 支持达梦数据库查询
Merge pull request !64 from zongyangleo/dev_1207_dm
2023-12-07 01:28:44 +00:00
刘宗洋
84fd14c129 feat: 支持达梦数据库查询 2023-12-07 09:21:40 +08:00
meilin.huang
a376a82240 feat: 数据库sql执行支持取消执行操作 2023-12-07 01:07:34 +08:00
meilin.huang
e1e03dc09a fix: 字段补充 2023-12-06 16:02:07 +08:00
meilin.huang
790d644c34 refactor: 终端记录调整 2023-12-06 13:17:50 +08:00
meilin.huang
9de8dae954 feat: 前后端传递sql编码处理 2023-12-06 09:23:23 +08:00
meilin.huang
57361d8241 feat: 支持关联多标签、计划任务立即执行、标签相关操作优化 2023-12-05 23:03:51 +08:00
meilin.huang
b347bd7ef5 feat: 数据库超时时间设置 2023-11-30 15:02:48 +08:00
meilin.huang
070c8ac0da fix: 排序导致条件丢失 2023-11-29 20:13:29 +08:00
meilin.huang
e221c2f42e feat: 新增系统全局分页size配置,可根据屏幕大小自行设置 2023-11-29 17:34:54 +08:00
Coder慌
c7bab3a71b !62 fix:gauss驱动支持ssh
Merge pull request !62 from zongyangleo/dev_1128
2023-11-29 08:40:25 +00:00
刘宗洋
82c17a51a2 fix:libpq驱动支持gaussdb sha256加密登录 2023-11-28 22:49:42 +08:00
meilin.huang
e4447e6bc2 refactor: code review 2023-11-27 17:40:47 +08:00
Coder慌
b9570d9a5f !61 fix: 1、pgsql驱动换成高斯db驱动 2、sql格式化支持传数据库类型
Merge pull request !61 from zongyangleo/dev_1127
2023-11-27 08:58:32 +00:00
刘宗洋
01e8a2c14d fix:
1、pgsql驱动换成高斯db驱动
2、sql格式化支持传数据库类型
2023-11-27 16:55:00 +08:00
meilin.huang
64bd51c3b0 refactor: code review 2023-11-26 21:21:35 +08:00
Coder慌
54ab34df3f !60 feat: 支持pgsql编辑表、索引
Merge pull request !60 from zongyangleo/dev_1126
2023-11-26 02:32:27 +00:00
刘宗洋
206490ba3e feat: 支持pgsql编辑表、索引 2023-11-26 01:47:49 +08:00
meilin.huang
16612d2c9c refactor: 多tab结果集调整 2023-11-24 17:03:08 +08:00
meilin.huang
6b65605360 feat: sql查询支持多tab结果集 2023-11-24 12:12:47 +08:00
meilin.huang
bb37ed3b95 refactor: 数据库操作界面小优化 2023-11-22 12:19:07 +08:00
meilin.huang
d102cc8c08 feat: 数据库表操作支持复制单元格数据菜单 2023-11-20 12:21:27 +08:00
meilin.huang
a6df74d63d refactor: 虚拟表格优化 2023-11-18 21:15:33 +08:00
meilin.huang
f79760943e refactor: 虚拟表格与contextmenu菜单优化 2023-11-18 15:22:25 +08:00
meilin.huang
a40ec21a05 refactor: 数据库表使用虚拟表替换,提升数据量较大时的渲染速度 2023-11-17 13:31:28 +08:00
meilin.huang
43230267b6 refactor: 界面小调整 2023-11-15 12:28:49 +08:00
meilin.huang
0ae99cdaf9 refactor: contextmenu组件优化、标签&资源替换为contextmenu操作 2023-11-14 17:36:51 +08:00
meilin.huang
f234c72514 refactor: DB-数据操作优化 2023-11-13 17:41:03 +08:00
meilin.huang
76527d95bd refactor: 机器相关配置迁移至系统配置、pgsql数据操作完善、新增context-path 2023-11-12 20:14:44 +08:00
meilin.huang
27c53385f2 refactor: 列表操作按钮调整 2023-11-09 12:11:11 +08:00
Coder慌
a1b25e9766 !59 fix: sql代码提示修复:支持跨schema提示
Merge pull request !59 from zongyangleo/dev_1109
2023-11-09 02:13:31 +00:00
刘宗洋
abad0ed481 fix: sql代码提示修复:支持跨schema提示 2023-11-09 09:48:58 +08:00
meilin.huang
eddda41291 feat: 机器列表新增运行状态 & refactor: 登录账号信息存储与context 2023-11-07 21:05:21 +08:00
meilin.huang
d9adf0fd25 fix: 表问题修复 2023-11-03 22:29:01 +08:00
meilin.huang
0ce82b41ba refactor: 标签树展示调整 2023-11-03 17:09:20 +08:00
meilin.huang
37026f3269 feat: 数据库表操作显示表size&其他小优化 2023-11-02 12:46:21 +08:00
meilin.huang
3155380f16 refactor: tooltip延迟显示等 2023-10-31 12:36:04 +08:00
meilin.huang
f2b0f294d8 refactor: machine包调整 2023-10-30 17:34:56 +08:00
meilin.huang
12f63ef3dd refactor: db/redis/mongo连接代码包独立 2023-10-27 17:41:45 +08:00
meilin.huang
a1303b52eb refactor: 新增base.Repo与base.App,重构repo与app层代码 2023-10-26 17:15:49 +08:00
meilin.huang
10f6b03fb5 refactor: code review 2023-10-20 21:31:46 +08:00
may-fly
45d2449221 Merge pull request #74 from kanzihuang/fix-showcreatetable
fix: show create table for postgres
2023-10-20 21:15:54 +08:00
wanli
9e5f146e05 fix: show create table for postgres 2023-10-20 17:37:09 +08:00
meilin.huang
2b91bbe185 refactor: websocket支持单用户多连接 2023-10-19 19:00:23 +08:00
may-fly
747ea6404d Merge pull request #73 from kanzihuang/feat-notify
feature: 每个客户端独立处理后端发送的系统消息
2023-10-18 08:36:21 -05:00
wanli
ccfc6bd1df feature: 每个客户端独立处理后端发送的系统消息 2023-10-18 20:31:27 +08:00
kanzihuang
361eafedae feature: 执行 SQL 脚本时显示执行进度 2023-10-18 15:41:57 +08:00
meilin.huang
a64b894b08 refactor: sqlite依赖替换 2023-10-17 12:32:59 +08:00
may-fly
0ad805c170 Merge pull request #72 from kanzihuang/refactor-dbtype
refactor: 实现 DbType 类型,集中处理部分差异化的数据库操作
2023-10-15 19:41:19 -05:00
kanzihuang
ba82b5b516 refactor: 实现 DbType 类型,集中处理部分差异化的数据库操作 2023-10-15 11:39:42 +08:00
may-fly
f04b82c933 Merge pull request #71 from kanzihuang/fix-exec-postgres-sql-pullrequest
fix: 执行或导入 SQL 脚本支持 PostgreSQL
2023-10-14 09:57:12 -05:00
kanzihuang
23b137ab9b fix: 执行或导入 SQL 脚本支持 PostgreSQL 2023-10-14 21:10:53 +08:00
meilin.huang
a4d3a4627a refactor: 系统水印优化等 2023-10-14 16:00:16 +08:00
meilin.huang
77ae6e3bab refactor: 系统水印重构 2023-10-14 00:38:51 +08:00
meilin.huang
e0f1f40ba0 fix: 缓存使用redis无法set问题修复&admin账号默认有所有菜单 2023-10-12 21:50:55 +08:00
meilin.huang
d300f604f1 review 2023-10-12 12:14:56 +08:00
may-fly
2c2c0ff40b Merge pull request #69 from kanzihuang/feat-progress-notify-pullrequest
feat: 显示 SQL 文件执行进度
2023-10-10 20:52:24 -05:00
kanzihuang
b4ddbbd38f fix: 使用最新版 vitess sqlparser 解析 SQL 语句
解决 xwb1989/sqlparser 不支持 current_timestamp() 的问题
2023-10-10 23:56:01 +08:00
wanli
7544288451 feat: 前端显示 SQL 文件执行进度 2023-10-10 23:28:25 +08:00
meilin.huang
41443dccc0 feat: 支持sqlite存储数据 2023-10-10 23:21:29 +08:00
meilin.huang
22e218fc5f refactor: 系统消息调整 2023-10-10 17:39:46 +08:00
meilin.huang
4da0d1abaa refactor: form表单label统一去除':' 2023-10-09 17:29:52 +08:00
meilin.huang
6563b53436 refactor: 包依赖升级等 2023-10-08 12:14:19 +08:00
meilin.huang
fac71a4794 fix: 前端代理默认端口调整&水印开关不生效 2023-09-27 17:19:58 +08:00
meilin.huang
92dff6fdc3 refactor: review 2023-09-26 17:38:52 +08:00
meilin.huang
a1eca3d691 refactor: 登录页调整 2023-09-23 22:52:05 +08:00
meilin.huang
6681dc1057 refactor: 数据库sql提示优化&机器终端支持全屏 2023-09-20 20:42:23 +08:00
meilin.huang
829a68feaa fix: 数据库多库切换关键字提示错误修复&sql编辑器组件统一 2023-09-19 23:00:32 +08:00
meilin.huang
72677e270d feat: 前端用户信息迁移至localstorage 2023-09-16 17:07:48 +08:00
554 changed files with 32166 additions and 14578 deletions

View File

@@ -1,16 +1,16 @@
# 构建前端资源
FROM node:18-alpine3.16 as fe-builder
FROM node:18-bookworm-slim as fe-builder
WORKDIR /mayfly
COPY mayfly_go_web .
RUN yarn
RUN yarn build
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
yarn install && \
yarn build
# 构建后端资源
FROM golang:1.21.0 as be-builder
FROM golang:1.21.5 as be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
@@ -18,7 +18,7 @@ WORKDIR /mayfly
# Copy the go source for building server
COPY server .
RUN go mod download
RUN go mod tidy && go mod download
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
@@ -27,9 +27,11 @@ RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \
-o mayfly-go main.go
FROM alpine:3.16
FROM debian:bookworm-slim
RUN apk add --no-cache ca-certificates bash expat
RUN apt-get update && \
apt-get install -y ca-certificates expat libncurses5 && \
apt-get clean
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

View File

@@ -1,19 +1,19 @@
# 🌈mayfly-go
<p align="center">
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
</a>
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
<a href="https://github.com/dromara/mayfly-go" target="_blank">
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
</a>
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.20%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -22,7 +22,7 @@
### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程)、数据库mysql postgres、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
### 开发语言与主要框架
@@ -100,4 +100,4 @@ http://go.mayfly.run
#### 💌 支持作者
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/may-fly/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/objs/mayfly-go">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。

View File

@@ -56,7 +56,7 @@ function build() {
if [ "${os}" == "windows" ];then
execFileName="${execFileName}.exe"
fi
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -o ${execFileName} main.go
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
echo_green "目标文件夹已存在,清空文件夹"
@@ -74,12 +74,13 @@ function build() {
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"

View File

@@ -1,5 +1,11 @@
# 本地环境
ENV = 'development'
VITE_OPEN = true
# 本地环境接口地址
VITE_API_URL = '/api'
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -2,4 +2,8 @@
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = '/api'
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -1,76 +1,76 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'error',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
'no-console': 'error',
'no-redeclare': 'off',
},
};
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'error',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
// 'no-console': 'error',
'no-redeclare': 'off',
},
};

View File

@@ -1,7 +1,8 @@
.DS_Store
node_modules
/dist
*.lock
pnpm-lock.yaml
# local env files
.env.local

View File

@@ -9,28 +9,32 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.5.0",
"axios": "^1.5.0",
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.7.2",
"asciinema-player": "^3.6.3",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"countup.js": "^2.7.0",
"cropperjs": "^1.5.11",
"echarts": "^5.4.0",
"element-plus": "^2.3.12",
"jsencrypt": "^3.3.1",
"echarts": "^5.4.3",
"element-plus": "^2.5.1",
"js-base64": "^3.7.5",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.43.0",
"monaco-editor": "^0.45.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"qrcode.vue": "^3.4.0",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.1",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sql-formatter": "^12.1.2",
"vue": "^3.3.4",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.2.4",
"sortablejs": "^1.15.0",
"splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2",
"uuid": "^9.0.1",
"vue": "^3.4.14",
"vue-router": "^4.2.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
@@ -38,22 +42,21 @@
},
"devDependencies": {
"@types/lodash": "^4.14.178",
"@types/node": "^15.6.0",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.10.6",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.0.11",
"dotenv": "^10.0.0",
"@types/sortablejs": "^1.15.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/compiler-sfc": "^3.4.14",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.3.0",
"sass": "^1.62.0",
"sass-loader": "^13.2.0",
"typescript": "^5.0.2",
"vite": "^4.4.9",
"vue-eslint-parser": "^9.1.1"
"eslint-plugin-vue": "^9.19.2",
"prettier": "^3.1.0",
"sass": "^1.69.0",
"typescript": "^5.3.2",
"vite": "^5.0.11",
"vue-eslint-parser": "^9.4.0"
},
"browserslist": [
"> 1%",

View File

@@ -1,20 +1,31 @@
<template>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
<div class="h100">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
:content="themeConfig.watermarkText"
class="h100"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
</template>
<script setup lang="ts" name="app">
import { ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
// import { useTagsViewRoutes } from '@/store/tagsViewRoutes';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLocal } from '@/common/utils/storage';
import LockScreen from '@/views/layout/lockScreen/index.vue';
import Setings from '@/views/layout/navBars/breadcrumb/setings.vue';
import Watermark from '@/common/utils/wartermark';
import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { useIntervalFn } from '@vueuse/core';
const setingsRef = ref();
const route = useRoute();
@@ -27,14 +38,6 @@ const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
};
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
// setIntroduction.cssCdn();
// // 设置批量第三方 js
// setIntroduction.jsCdn();
});
// 页面加载时
onMounted(() => {
nextTick(() => {
@@ -42,14 +45,47 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => {
openSetingsDrawer();
});
// 获取缓存中的布局配置
if (getLocal('themeConfig')) {
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
}
// 初始化系统主题
themeConfigStores.initThemeConfig();
});
});
// 监听 themeConfig isWartermark配置文件的变化
watch(
() => themeConfig.value.isWatermark,
(val) => {
if (val) {
setTimeout(() => {
setWatermarkContent();
refreshWatermarkTime();
resume();
}, 500);
} else {
pause();
}
}
);
// 刷新水印时间
const { pause, resume } = useIntervalFn(() => {
if (!themeConfig.value.isWatermark) {
pause();
}
refreshWatermarkTime();
}, 60000);
const setWatermarkContent = () => {
themeConfigStores.setWatermarkUser();
};
/**
* 刷新水印时间
*/
const refreshWatermarkTime = () => {
themeConfigStores.setWatermarkNowTime();
};
// 页面销毁时,关闭监听布局配置
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
@@ -60,8 +96,6 @@ watch(
() => route.path,
() => {
nextTick(() => {
// 路由变化更新水印
Watermark.use();
document.title = `${route.meta.title} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
});
}

File diff suppressed because one or more lines are too long

View File

@@ -26,6 +26,13 @@
"unicode": "e8b7",
"unicode_decimal": 59575
},
{
"icon_id": "12295203",
"name": "达梦数据库",
"font_class": "db-dm",
"unicode": "e6f0",
"unicode_decimal": 59120
},
{
"icon_id": "10055634",
"name": "云数据库MongoDB",
@@ -46,6 +53,20 @@
"font_class": "redis",
"unicode": "e619",
"unicode_decimal": 58905
},
{
"icon_id": "11617944",
"name": "oracle",
"font_class": "oracle",
"unicode": "e6ea",
"unicode_decimal": 59114
},
{
"icon_id": "8105644",
"name": "mariadb",
"font_class": "mariadb",
"unicode": "e513",
"unicode_decimal": 58643
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1,4 +1,5 @@
import request from './request';
import { useApiFetch } from '@/hooks/useRequest';
/**
* 可用于各模块定义各自api请求
@@ -14,11 +15,27 @@ class Api {
*/
method: string;
/**
* 请求前处理函数
* param1: param请求参数
*/
beforeHandler: Function;
constructor(url: string, method: string) {
this.url = url;
this.method = method;
}
/**
* 设置请求前处理回调函数
* @param func 请求前处理器
* @returns this
*/
withBeforeHandler(func: Function) {
this.beforeHandler = func;
return this;
}
/**
* 获取权限的完整url
*/
@@ -27,11 +44,34 @@ class Api {
}
/**
* 请求对应的该api
* 响应式使用该api
* @param params 响应式params
* @param reqOptions 其他可选值
* @returns
*/
useApi<T>(params: any = null, reqOptions: RequestInit = {}) {
return useApiFetch<T>(this, params, reqOptions);
}
/**
* fetch 请求对应的该api
* @param {Object} param 请求该api的参数
*/
request(param: any = null, options: any = null, headers: any = null): Promise<any> {
return request.request(this.method, this.url, param, headers, options);
async request(param: any = null, options: any = {}): Promise<any> {
const { execute, data } = this.useApi(param, options);
await execute();
return data.value;
}
/**
* xhr 请求对应的该api
* @param {Object} param 请求该api的参数
*/
async xhrReq(param: any = null, options: any = {}): Promise<any> {
if (this.beforeHandler) {
this.beforeHandler(param);
}
return request.xhrReq(this.method, this.url, param, options);
}
/** 静态方法 **/
@@ -79,3 +119,8 @@ class Api {
}
export default Api;
export class PageRes {
list: any[] = [];
total: number = 0;
}

View File

@@ -0,0 +1,9 @@
import EnumValue from './Enum';
// 标签关联的资源类型
export const TagResourceTypeEnum = {
Machine: EnumValue.of(1, '机器'),
Db: EnumValue.of(2, '数据库'),
Redis: EnumValue.of(3, 'redis'),
Mongo: EnumValue.of(4, 'mongo'),
};

View File

@@ -3,6 +3,10 @@ function getBaseApiUrl() {
if (path == '/') {
return window.location.host;
}
if (path.endsWith('/')) {
// 去除最后一个/
return window.location.host + path.replace(/\/$/, '');
}
return window.location.host + path;
}
@@ -11,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.5.2',
version: 'v1.7.0',
};
export default config;

View File

@@ -1,176 +0,0 @@
{
"seriesCnt": "4",
"backgroundColor": "rgba(0,0,0,0)",
"titleColor": "#008acd",
"subtitleColor": "#aaaaaa",
"textColorShow": false,
"textColor": "#333",
"markTextColor": "#eeeeee",
"color": [
"#2ec7c9",
"#b6a2de",
"#5ab1ef",
"#ffb980",
"#d87a80",
"#8d98b3",
"#e5cf0d",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#c05050",
"#59678c",
"#c9ab00",
"#7eb00a",
"#6f5553",
"#c14089"
],
"borderColor": "#ccc",
"borderWidth": 0,
"visualMapColor": [
"#5ab1ef",
"#e0ffff"
],
"legendTextColor": "#333333",
"kColor": "#d87a80",
"kColor0": "#2ec7c9",
"kBorderColor": "#d87a80",
"kBorderColor0": "#2ec7c9",
"kBorderWidth": 1,
"lineWidth": 2,
"symbolSize": 3,
"symbol": "emptyCircle",
"symbolBorderWidth": 1,
"lineSmooth": true,
"graphLineWidth": 1,
"graphLineColor": "#aaaaaa",
"mapLabelColor": "#d87a80",
"mapLabelColorE": "rgb(100,0,0)",
"mapBorderColor": "#eeeeee",
"mapBorderColorE": "#444",
"mapBorderWidth": 0.5,
"mapBorderWidthE": 1,
"mapAreaColor": "#dddddd",
"mapAreaColorE": "rgba(254,153,78,1)",
"axes": [
{
"type": "all",
"name": "通用坐标轴",
"axisLineShow": true,
"axisLineColor": "#eeeeee",
"axisTickShow": true,
"axisTickColor": "#eeeeee",
"axisLabelShow": true,
"axisLabelColor": "#eeeeee",
"splitLineShow": true,
"splitLineColor": [
"#aaaaaa"
],
"splitAreaShow": false,
"splitAreaColor": [
"#eeeeee"
]
},
{
"type": "category",
"name": "类目坐标轴",
"axisLineShow": true,
"axisLineColor": "#008acd",
"axisTickShow": true,
"axisTickColor": "#333",
"axisLabelShow": true,
"axisLabelColor": "#333",
"splitLineShow": false,
"splitLineColor": [
"#eee"
],
"splitAreaShow": false,
"splitAreaColor": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
},
{
"type": "value",
"name": "数值坐标轴",
"axisLineShow": true,
"axisLineColor": "#008acd",
"axisTickShow": true,
"axisTickColor": "#333",
"axisLabelShow": true,
"axisLabelColor": "#333",
"splitLineShow": true,
"splitLineColor": [
"#eee"
],
"splitAreaShow": true,
"splitAreaColor": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
},
{
"type": "log",
"name": "对数坐标轴",
"axisLineShow": true,
"axisLineColor": "#008acd",
"axisTickShow": true,
"axisTickColor": "#333",
"axisLabelShow": true,
"axisLabelColor": "#333",
"splitLineShow": true,
"splitLineColor": [
"#eee"
],
"splitAreaShow": true,
"splitAreaColor": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
},
{
"type": "time",
"name": "时间坐标轴",
"axisLineShow": true,
"axisLineColor": "#008acd",
"axisTickShow": true,
"axisTickColor": "#333",
"axisLabelShow": true,
"axisLabelColor": "#333",
"splitLineShow": true,
"splitLineColor": [
"#eee"
],
"splitAreaShow": false,
"splitAreaColor": [
"rgba(250,250,250,0.3)",
"rgba(200,200,200,0.3)"
]
}
],
"axisSeperateSetting": true,
"toolboxColor": "#2ec7c9",
"toolboxEmphasisColor": "#18a4a6",
"tooltipAxisColor": "#008acd",
"tooltipAxisWidth": "1",
"timelineLineColor": "#008acd",
"timelineLineWidth": 1,
"timelineItemColor": "#008acd",
"timelineItemColorE": "#a9334c",
"timelineCheckColor": "#2ec7c9",
"timelineCheckBorderColor": "#2ec7c9",
"timelineItemBorderWidth": 1,
"timelineControlColor": "#008acd",
"timelineControlBorderColor": "#008acd",
"timelineControlBorderWidth": 0.5,
"timelineLabelColor": "#008acd",
"datazoomBackgroundColor": "rgba(47,69,84,0)",
"datazoomDataColor": "#efefff",
"datazoomFillColor": "rgba(182,162,222,0.2)",
"datazoomHandleColor": "#008acd",
"datazoomHandleWidth": "100",
"datazoomLabelColor": "#333333"
}

View File

@@ -1,38 +0,0 @@
// import * as echarts from 'echarts'
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core';
/** 图表后缀都为 Chart */
import { PieChart } from 'echarts/charts';
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import { TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, LegendComponent } from 'echarts/components';
// 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
// BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
// LineChart,
PieChart,
]);
export default function (dom: any, theme: any = null, option: any) {
let chart = echarts.init(dom, theme);
chart.setOption(option);
return chart;
}

View File

@@ -1,9 +1,21 @@
import router from '../router';
import Axios from 'axios';
import config from './config';
import { getSession } from './utils/storage';
import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { useApiFetch } from '../hooks/useRequest';
import Api from './Api';
export default {
request,
xhrReq,
get,
post,
put,
del,
getApiUrl,
};
export interface Result {
/**
@@ -20,7 +32,7 @@ export interface Result {
data?: any;
}
enum ResultEnum {
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
@@ -28,8 +40,9 @@ enum ResultEnum {
NO_PERMISSION = 501,
}
const baseUrl: string = config.baseApiUrl;
const baseWsUrl: string = config.baseWsUrl;
export const baseUrl: string = config.baseApiUrl;
// const baseUrl: string = 'http://localhost:18888/api';
// const baseWsUrl: string = config.baseWsUrl;
/**
* 通知错误消息
@@ -41,19 +54,20 @@ function notifyErrorMsg(msg: string) {
}
// create an axios instance
const service = Axios.create({
const axiosInst = axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 20000, // request timeout
timeout: 60000, // request timeout
});
// request interceptor
service.interceptors.request.use(
axiosInst.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getSession('token');
const token = getToken();
if (token) {
// 设置token
config.headers['Authorization'] = token;
config.headers['ClientId'] = getClientId();
}
return config;
},
@@ -63,24 +77,16 @@ service.interceptors.request.use(
);
// response interceptor
service.interceptors.response.use(
(response) => {
// 获取请求返回结果
const data: Result = response.data;
if (data.code === ResultEnum.SUCCESS) {
return data.data;
}
// 如果提示没有权限则移除token使其重新登录
if (data.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
return Promise.reject(data);
},
axiosInst.interceptors.response.use(
(response) => response,
(e: any) => {
const rejectPromise = Promise.reject(e);
if (axios.isCancel(e)) {
console.log('请求已取消');
return rejectPromise;
}
const statusCode = e.response?.status;
if (statusCode == 500) {
notifyErrorMsg('服务器未知异常');
@@ -111,44 +117,62 @@ service.interceptors.response.use(
);
/**
* 请求uri
* xhr请求url
*
* @param method 请求方法
* @param url url
* @param params 参数
* @param options 可选
* @returns
*/
export function xhrReq(method: string, url: string, params: any = null, options: any = {}) {
if (!url) {
throw new Error('请求url不能为空');
}
// 简单判断该url是否是restful风格
if (url.indexOf('{') != -1) {
url = templateResolve(url, params);
}
const req: any = {
method,
url,
...options,
};
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
req.data = params;
} else {
req.params = params;
}
return axiosInst
.request(req)
.then((response) => {
// 获取请求返回结果
const result: Result = response.data;
return parseResult(result);
})
.catch((e) => {
return Promise.reject(e);
});
}
/**
* fetch请求url
*
* 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
*
* @param {Object} method 请求方法(GET,POST,PUT,DELTE等)
* @param {Object} uri uri
* @param {Object} params 参数
*/
function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
if (!url) throw new Error('请求url不能为空');
// 简单判断该url是否是restful风格
if (url.indexOf('{') != -1) {
url = templateResolve(url, params);
}
const query: any = {
method,
url: url,
...options,
};
if (headers) {
query.headers = headers;
}
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
query.data = params;
} else {
query.params = params;
}
return service
.request(query)
.then((res) => res)
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可
if (e.msg) {
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
});
async function request(method: string, url: string, params: any = null, options: any = {}): Promise<any> {
const { execute, data } = useApiFetch(Api.create(url, method), params, options);
await execute();
return data.value;
}
/**
@@ -158,32 +182,48 @@ function request(method: string, url: string, params: any = null, headers: any =
* @param {Object} url uri
* @param {Object} params 参数
*/
function get(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('get', url, params, headers, options);
function get(url: string, params: any = null, options: any = {}): Promise<any> {
return request('get', url, params, options);
}
function post(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('post', url, params, headers, options);
function post(url: string, params: any = null, options: any = {}): Promise<any> {
return request('post', url, params, options);
}
function put(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('put', url, params, headers, options);
function put(url: string, params: any = null, options: any = {}): Promise<any> {
return request('put', url, params, options);
}
function del(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('delete', url, params, headers, options);
function del(url: string, params: any = null, options: any = {}): Promise<any> {
return request('delete', url, params, options);
}
function getApiUrl(url: string) {
// 只是返回api地址而不做请求用在上传组件之类的
return baseUrl + url + '?token=' + getSession('token');
return baseUrl + url + '?' + joinClientParams();
}
export default {
request,
get,
post,
put,
del,
getApiUrl,
};
// 组装客户端参数,包括 token 和 clientId
export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;
}
// 如果提示没有权限则移除token使其重新登录
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(result.msg);
}
return Promise.reject(result);
}

View File

@@ -1,45 +0,0 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getSession } from '@/common/utils/storage';
export default {
/**
* 全局系统消息websocket
*/
sysMsgSocket() {
const token = getSession('token');
if (!token) {
return null;
}
return SocketBuilder.builder(`${Config.baseWsUrl}/sysmsg?token=${token}`)
.message((event: { data: string }) => {
const message = JSON.parse(event.data);
let mtype: string;
switch (message.type) {
case 0:
mtype = 'error';
break;
case 2:
mtype = 'info';
break;
case 1:
mtype = 'success';
break;
default:
mtype = 'info';
}
if (mtype == undefined) {
return;
}
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: mtype as any,
});
})
.open((event: any) => console.log(event))
.build();
},
};

View File

@@ -1,9 +1,77 @@
import openApi from './openApi';
// 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWartermarkConfigKey = 'UseWartermark';
const AccountLoginSecurityKey = 'AccountLoginSecurity';
const MachineConfigKey = 'MachineConfig';
const SysStyleConfigKey = 'SysStyleConfig';
/**
* 获取账号登录安全配置
*
* @returns
*/
export async function getAccountLoginSecurity(): Promise<any> {
const value = await getConfigValue(AccountLoginSecurityKey);
if (!value) {
return null;
}
const jsonValue = JSON.parse(value);
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
return jsonValue;
}
/**
* 获取全局系统样式配置logo、title等
*
* @returns
*/
export async function getSysStyleConfig(): Promise<any> {
const value = await getConfigValue(SysStyleConfigKey);
const defaultValue = {
useWatermark: true,
};
if (!value) {
return defaultValue;
}
const jsonValue = JSON.parse(value);
// 将字符串转为bool
jsonValue.useWatermark = convertBool(jsonValue.useWatermark, true);
return jsonValue;
}
/**
* 获取LDAP登录配置
*
* @returns
*/
export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}
/**
* 获取机器配置
*
* @returns
*/
export async function getMachineConfig(): Promise<any> {
const value = await getConfigValue(MachineConfigKey);
const defaultValue = {
// 默认1gb
uploadMaxFileSize: '1GB',
};
if (!value) {
return defaultValue;
}
try {
const jsonValue = JSON.parse(value);
return jsonValue;
} catch (e) {
return defaultValue;
}
}
/**
* 获取系统配置值
@@ -27,54 +95,9 @@ export async function getBoolConfigValue(key: string, defaultValue: boolean): Pr
return convertBool(value, defaultValue);
}
/**
* 获取账号登录安全配置
*
* @returns
*/
export async function getAccountLoginSecurity(): Promise<any> {
const value = await getConfigValue(AccountLoginSecurity);
if (!value) {
return null;
}
const jsonValue = JSON.parse(value);
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
return jsonValue;
}
/**
* 是否使用登录验证码
*
* @returns
*/
export async function useLoginCaptcha(): Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true);
}
/**
* 是否启用水印
*
* @returns
*/
export async function useWartermark(): Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true);
}
function convertBool(value: string, defaultValue: boolean) {
if (!value) {
return defaultValue;
}
return value == '1' || value == 'true';
}
/**
* 获取LDAP登录配置
*
* @returns
*/
export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}

View File

@@ -0,0 +1,107 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import { joinClientParams } from './request';
class SysSocket {
/**
* socket连接
*/
socket: any;
/**
* key -> 消息类别value -> 消息对应的处理器函数
*/
categoryHandlers: Map<string, any> = new Map();
/**
* 消息类型
*/
messageTypes = {
0: 'error',
1: 'success',
2: 'info',
};
/**
* 初始化全局系统消息websocket
*/
init() {
// 存在则不需要重新建立连接
if (this.socket) {
return;
}
const token = getToken();
if (!token) {
return null;
}
console.log('init system ws');
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl)
.message((event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
} catch (e) {
console.error('解析ws消息失败', e);
return;
}
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
const handler = this.categoryHandlers.get(message.category);
if (handler) {
handler(message);
return;
}
const type = this.getMsgType(message.type);
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: type,
});
})
.open((event: any) => console.log(event))
.close(() => {
console.log('close sys socket');
this.socket = null;
})
.build();
}
destory() {
this.socket?.close();
this.socket = null;
this.categoryHandlers?.clear();
}
/**
* 注册消息处理函数
*
* @param category 消息类别
* @param handlerFunc 消息处理函数
*/
registerMsgHandler(category: any, handlerFunc: any) {
this.init();
if (this.categoryHandlers.has(category)) {
console.log(`${category}该类别消息处理器已存在...`);
return;
}
if (typeof handlerFunc != 'function') {
throw new Error('message handler需为函数');
}
this.categoryHandlers.set(category, handlerFunc);
}
getMsgType(msgType: any) {
return this.messageTypes[msgType];
}
}
// 全局系统消息websocket;
const sysSocket = new SysSocket();
export default sysSocket;

View File

@@ -7,6 +7,7 @@ export function dateFormat2(fmt: string, date: Date) {
'H+': date.getHours().toString(), // 时
'm+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString(), // 秒
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const k in opt) {
@@ -23,5 +24,8 @@ export function dateStrFormat(fmt: string, dateStr: string) {
}
export function dateFormat(dateStr: string) {
if (!dateStr) {
return '';
}
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
}

View File

@@ -29,15 +29,19 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
cvsData.push(dataValueArr);
}
const csvString = cvsData.map((e) => e.join(',')).join('\n');
exportFile(`${filename}.csv`, csvString);
}
export function exportFile(filename: string, content: string) {
// 导出
let link = document.createElement('a');
let exportContent = '\uFEFF';
let blob = new Blob([exportContent + csvString], {
let blob = new Blob([exportContent + content], {
type: 'text/plain;charset=utf-8',
});
link.id = 'download-csv';
link.id = 'download-file';
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', `${filename}.csv`);
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
}

View File

@@ -3,21 +3,47 @@
* @param size byte size
* @returns
*/
export function formatByteSize(size: any) {
const value = Number(size);
if (size && !isNaN(value)) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
let index = 0;
let k = value;
if (value >= 1024) {
while (k > 1024) {
k = k / 1024;
index++;
}
}
return `${k.toFixed(2)}${units[index]}`;
export function formatByteSize(size: number, fixed = 2) {
if (size === 0) {
return '0B';
}
return '-';
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const base = 1024;
const exponent = Math.floor(Math.log(size) / Math.log(base));
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
}
/**
* 容量转为对应的字节大小,如 1KB转为 1024
* @param sizeString 1kb 1gb等
* @returns
*/
export function convertToBytes(sizeStr: string) {
sizeStr = sizeStr.trim();
const unit = sizeStr.slice(-2);
const valueStr = sizeStr.slice(0, -2);
const value = parseInt(valueStr, 10);
let bytes = 0;
switch (unit.toUpperCase()) {
case 'KB':
bytes = value * 1024;
break;
case 'MB':
bytes = value * 1024 * 1024;
break;
case 'GB':
bytes = value * 1024 * 1024 * 1024;
break;
default:
throw new Error('Invalid size unit');
}
return bytes;
}
/**

View File

@@ -1,46 +1,46 @@
import { nextTick } from 'vue';
import loadingCss from '@/theme/loading.scss?inline';
import '@/theme/loading.scss';
// 定义方法
/**
* 页面全局 Loading
* @method start 创建 loading
* @method done 移除 loading
*/
export const NextLoading = {
// 载入 css
setCss: () => {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = loadingCss;
link.crossOrigin = 'anonymous';
document.getElementsByTagName('head')[0].appendChild(link);
},
// 创建 loading
start: () => {
const bodys: any = document.body;
const div = document.createElement('div');
const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next');
const htmls = `
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
</div>
`;
div.innerHTML = htmls;
bodys.insertBefore(div, bodys.childNodes[0]);
},
// 移除 loading
done: () => {
done: (time: number = 1000) => {
nextTick(() => {
setTimeout(() => {
const el = document.querySelector('.loading-next');
el && el.parentNode?.removeChild(el);
}, 1000);
const el = <HTMLElement>document.querySelector('.loading-next');
el?.parentNode?.removeChild(el);
}, time);
});
},
};
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -1,17 +1,92 @@
import { randomUuid } from './string';
const TokenKey = 'm-token';
const UserKey = 'm-user';
const TagViewsKey = 'm-tagViews';
const ClientIdKey = 'm-clientId';
// 获取请求token
export function getToken(): string {
return getLocal(TokenKey);
}
// 保存用户访问token
export function saveToken(token: string) {
setLocal(TokenKey, token);
}
// 获取登录用户基础信息
export function getUser() {
return getLocal(UserKey);
}
// 保存用户信息
export function saveUser(userinfo: any) {
setLocal(UserKey, userinfo);
}
export function saveThemeConfig(themeConfig: any) {
setLocal('themeConfig', themeConfig);
}
export function getThemeConfig() {
return getLocal('themeConfig');
}
/**
* 清除当前登录用户相关信息
*/
export function clearUser() {
removeLocal(TokenKey);
removeLocal(UserKey);
}
export function getTagViews() {
return getSession(TagViewsKey);
}
export function setTagViews(tagViews: Array<object>) {
setSession(TagViewsKey, tagViews);
}
export function removeTagViews() {
removeSession(TagViewsKey);
}
// 获取客户端UUID
export function getClientId(): string {
let uuid = getSession(ClientIdKey);
if (uuid == null) {
uuid = randomUuid();
setSession(ClientIdKey, uuid);
}
return uuid;
}
// 1. localStorage
// 设置永久缓存
export function setLocal(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
if (typeof val == 'object') {
val = JSON.stringify(val);
}
window.localStorage.setItem(key, val);
}
// 获取永久缓存
export function getLocal(key: string) {
let json: any = window.localStorage.getItem(key);
return JSON.parse(json);
let val: any = window.localStorage.getItem(key);
try {
return JSON.parse(val);
} catch (e) {
return val;
}
}
// 移除永久缓存
export function removeLocal(key: string) {
window.localStorage.removeItem(key);
}
// 移除全部永久缓存
export function clearLocal() {
window.localStorage.clear();
@@ -20,35 +95,29 @@ export function clearLocal() {
// 2. sessionStorage
// 设置临时缓存
export function setSession(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
if (typeof val == 'object') {
val = JSON.stringify(val);
}
window.sessionStorage.setItem(key, val);
}
// 获取临时缓存
export function getSession(key: string) {
let json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
let val: any = window.sessionStorage.getItem(key);
try {
return JSON.parse(val);
} catch (e) {
return val;
}
}
// 移除临时缓存
export function removeSession(key: string) {
window.sessionStorage.removeItem(key);
}
// 移除全部临时缓存
export function clearSession() {
clearUser();
window.sessionStorage.clear();
}
export function getUserInfo4Session() {
return getSession('userInfo');
}
export function setUserInfo2Session(userinfo: any) {
setSession('userInfo', userinfo);
}
// 获取是否开启水印
export function getUseWatermark4Session() {
return getSession('useWatermark');
}
export function setUseWatermark2Session(useWatermark: boolean) {
setSession('useWatermark', useWatermark);
}

View File

@@ -1,3 +1,7 @@
import { v1 as uuidv1 } from 'uuid';
import Clipboard from 'clipboard';
import { ElMessage } from 'element-plus';
/**
* 模板字符串解析template = 'hahaha{name}_{id}' ,param = {name: 'hh', id: 1}
* 解析后为 hahahahh_1
@@ -129,3 +133,49 @@ export function getContentWidth(content: any): number {
// }
return flexWidth;
}
/**
*
* @returns uuid
*/
export function randomUuid() {
return uuidv1();
}
/**
* 拷贝文本至剪贴板
* @param txt 需要拷贝到剪贴板的文本
* @param selector click事件对应的元素selector默认为 #copyValue
* @returns
*/
export async function copyToClipboard(txt: string, selector: string = '#copyValue') {
// navigator clipboard 需要https等安全上下文
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 向剪贴板写文本
try {
// 拷贝单元格数据
await navigator.clipboard.writeText(txt);
ElMessage.success('复制成功');
} catch (e: any) {
ElMessage.error('复制失败');
}
return;
}
let clipboard = new Clipboard(selector, {
text: function () {
return txt;
},
});
clipboard.on('success', () => {
ElMessage.success('复制成功');
// 释放内存
clipboard.destroy();
});
clipboard.on('error', () => {
// 不支持复制
ElMessage.error('该浏览器不支持自动复制');
// 释放内存
clipboard.destroy();
});
}

View File

@@ -0,0 +1,13 @@
const mode = import.meta.env.VITE_ROUTER_MODE;
/**
* @description 获取不同路由模式所对应的 url
* @returns {String}
*/
export function getNowUrl() {
const url = {
hash: location.hash.substring(1),
history: location.pathname + location.search,
};
return url[mode];
}

View File

@@ -1,65 +0,0 @@
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage';
import { dateFormat2 } from '@/common/utils/date';
// 页面添加水印效果
const setWatermark = (str: any) => {
const id = '1.23452384164.123412416';
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
const can = document.createElement('canvas');
can.width = 400;
can.height = 250;
const cans: any = can.getContext('2d');
cans.rotate((-20 * Math.PI) / 180);
cans.font = '14px Vedana';
cans.fillStyle = 'rgba(200, 200, 200, 0.35)';
cans.textAlign = 'left';
cans.textBaseline = 'Middle';
// cans.fillText('mayfly go', can.width / 4, can.height )
cans.fillText(str, can.width / 8, can.height / 2);
const div = document.createElement('div');
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '30px';
div.style.left = '0px';
div.style.position = 'fixed';
div.style.zIndex = '10000000';
div.style.width = document.documentElement.clientWidth + 'px';
div.style.height = document.documentElement.clientHeight + 'px';
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
document.body.appendChild(div);
return id;
};
function set(str: any) {
let id = setWatermark(str);
if (document.getElementById(id) === null) id = setWatermark(str);
}
function del() {
let id = '1.23452384164.123412416';
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
}
const watermark = {
use: () => {
setTimeout(() => {
const userinfo = getUserInfo4Session();
if (userinfo && getUseWatermark4Session()) {
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`);
} else {
del();
}
}, 1500);
},
// 设置水印
set: (str: any) => {
set(str);
},
// 删除水印
del: () => {
del();
},
};
export default watermark;

View File

@@ -0,0 +1,66 @@
<template>
<div v-show="isShow" :style="style">
<slot></slot>
</div>
</template>
<script setup lang="ts" name="GridItem">
import { computed, inject, Ref, ref, useAttrs, watch } from 'vue';
import { BreakPoint, Responsive } from '../interface/index';
type Props = {
offset?: number;
span?: number;
suffix?: boolean;
xs?: Responsive;
sm?: Responsive;
md?: Responsive;
lg?: Responsive;
xl?: Responsive;
};
const props = withDefaults(defineProps<Props>(), {
offset: 0,
span: 1,
suffix: false,
xs: undefined,
sm: undefined,
md: undefined,
lg: undefined,
xl: undefined,
});
const attrs = useAttrs() as { index: string };
const isShow = ref(true);
// 注入断点
const breakPoint = inject<Ref<BreakPoint>>('breakPoint', ref('xl'));
const shouldHiddenIndex = inject<Ref<number>>('shouldHiddenIndex', ref(-1));
watch(
() => [shouldHiddenIndex.value, breakPoint.value],
(n) => {
if (attrs.index) {
isShow.value = !(n[0] !== -1 && parseInt(attrs.index) >= Number(n[0]));
}
},
{ immediate: true }
);
const gap = inject('gap', 0);
const cols = inject('cols', ref(4));
const style = computed(() => {
let span = props[breakPoint.value]?.span ?? props.span;
let offset = props[breakPoint.value]?.offset ?? props.offset;
if (props.suffix) {
return {
gridColumnStart: cols.value - span - offset + 1,
gridColumnEnd: `span ${span + offset}`,
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
};
} else {
return {
gridColumn: `span ${span + offset > cols.value ? cols.value : span + offset}/span ${span + offset > cols.value ? cols.value : span + offset}`,
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
};
}
});
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div :style="style">
<slot></slot>
</div>
</template>
<script setup lang="ts" name="Grid">
import { ref, watch, useSlots, computed, provide, onBeforeMount, onMounted, onUnmounted, onDeactivated, onActivated, VNodeArrayChildren, VNode } from 'vue';
import type { BreakPoint } from './interface/index';
type Props = {
cols?: number | Record<BreakPoint, number>;
collapsed?: boolean;
collapsedRows?: number;
gap?: [number, number] | number;
};
const props = withDefaults(defineProps<Props>(), {
cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
collapsed: false,
collapsedRows: 1,
gap: 0,
});
onBeforeMount(() => props.collapsed && findIndex());
onMounted(() => {
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
window.addEventListener('resize', resize);
});
onActivated(() => {
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
window.addEventListener('resize', resize);
});
onUnmounted(() => {
window.removeEventListener('resize', resize);
});
onDeactivated(() => {
window.removeEventListener('resize', resize);
});
// 监听屏幕变化
const resize = (e: UIEvent) => {
let width = (e.target as Window).innerWidth;
switch (!!width) {
case width < 768:
breakPoint.value = 'xs';
break;
case width >= 768 && width < 992:
breakPoint.value = 'sm';
break;
case width >= 992 && width < 1200:
breakPoint.value = 'md';
break;
case width >= 1200 && width < 1920:
breakPoint.value = 'lg';
break;
case width >= 1920:
breakPoint.value = 'xl';
break;
}
};
// 注入 gap 间距
provide('gap', Array.isArray(props.gap) ? props.gap[0] : props.gap);
// 注入响应式断点
let breakPoint = ref<BreakPoint>('xl');
provide('breakPoint', breakPoint);
// 注入要开始折叠的 index
const hiddenIndex = ref(-1);
provide('shouldHiddenIndex', hiddenIndex);
// 注入 cols
const gridCols = computed(() => {
if (typeof props.cols === 'object') return props.cols[breakPoint.value] ?? props.cols;
return props.cols;
});
provide('cols', gridCols);
// 寻找需要开始折叠的字段 index
const slots = useSlots().default!();
const findIndex = () => {
let fields: VNodeArrayChildren = [];
let suffix: VNode | null = null;
slots.forEach((slot: any) => {
// suffix
if (typeof slot.type === 'object' && slot.type.__name === 'GridItem' && slot.props?.suffix !== undefined) {
suffix = slot;
}
// slot children
if (typeof slot.type === 'symbol' && Array.isArray(slot.children)) {
fields.push(...slot.children);
}
});
// 计算 suffix 所占用的列
let suffixCols = 0;
if (suffix) {
suffixCols =
((suffix as VNode).props![breakPoint.value]?.span ?? (suffix as VNode).props?.span ?? 1) +
((suffix as VNode).props![breakPoint.value]?.offset ?? (suffix as VNode).props?.offset ?? 0);
}
try {
let find = false;
fields.reduce((prev = 0, current, index) => {
prev +=
((current as VNode)!.props![breakPoint.value]?.span ?? (current as VNode)!.props?.span ?? 1) +
((current as VNode)!.props![breakPoint.value]?.offset ?? (current as VNode)!.props?.offset ?? 0);
if (Number(prev) > props.collapsedRows * gridCols.value - suffixCols) {
hiddenIndex.value = index;
find = true;
throw 'find it';
}
return prev;
}, 0);
if (!find) hiddenIndex.value = -1;
} catch (e) {
// console.warn(e);
}
};
// 断点变化时执行 findIndex
watch(
() => breakPoint.value,
() => {
if (props.collapsed) findIndex();
}
);
// 监听 collapsed
watch(
() => props.collapsed,
(value) => {
if (value) return findIndex();
hiddenIndex.value = -1;
}
);
// 设置间距
const gridGap = computed(() => {
if (typeof props.gap === 'number') return `${props.gap}px`;
if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`;
return 'unset';
});
// 设置 style
const style = computed(() => {
return {
display: 'grid',
gridGap: gridGap.value,
gridTemplateColumns: `repeat(${gridCols.value}, minmax(0, 1fr))`,
};
});
defineExpose({ breakPoint });
</script>

View File

@@ -0,0 +1,6 @@
export type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl";
export type Responsive = {
span?: number;
offset?: number;
};

View File

@@ -0,0 +1,84 @@
<template>
<component
:is="item?.render ?? `el-${item.type}`"
v-bind="{ ...handleSearchProps, ...placeholder, clearable: true }"
v-on="{ ...handleEvents }"
v-model.trim="itemValue"
:data="item.type === 'tree-select' ? item.options : []"
:options="['cascader', 'select-v2'].includes(item.type!) ? item.options : []"
>
<template v-if="item.type === 'cascader'" #default="{ data }">
<span>{{ data[fieldNames.label] }}</span>
</template>
<template v-if="item.type === 'select'">
<component
:is="`el-option`"
v-for="(col, index) in item.options"
:key="index"
:label="col[fieldNames.label]"
:value="col[fieldNames.value]"
></component>
</template>
<slot v-else></slot>
</component>
</template>
<script setup lang="ts" name="SearchFormItem">
import { computed } from 'vue';
import { SearchItem } from '../index';
interface SearchFormItemProps {
item: SearchItem;
}
const props = defineProps<SearchFormItemProps>();
const itemValue = defineModel('modelValue');
// 判断 fieldNames 设置 label && value && children 的 key 值
const fieldNames = computed(() => {
return {
label: props.item?.fieldNames?.label ?? 'label',
value: props.item?.fieldNames?.value ?? 'value',
children: props.item.fieldNames?.children ?? 'children',
};
});
// 处理透传的 searchProps (type 为 tree-select、cascader 的时候需要给下默认 label && value && children)
const handleSearchProps = computed(() => {
const label = fieldNames.value.label;
const value = fieldNames.value.value;
const children = fieldNames.value.children;
const searchEl = props.item?.type;
let searchProps = props.item?.props ?? {};
if (searchEl === 'tree-select') {
searchProps = { ...searchProps, props: { ...searchProps.props, label, children }, nodeKey: value };
}
if (searchEl === 'cascader') {
searchProps = { ...searchProps, props: { ...searchProps.props, label, value, children } };
}
return searchProps;
});
// 处理透传的 事件
const handleEvents = computed(() => {
let itemEvents = props.item?.events ?? {};
return itemEvents;
});
// 处理默认 placeholder
const placeholder = computed(() => {
const search = props.item;
const label = search.label;
if (['datetimerange', 'daterange', 'monthrange'].includes(search?.props?.type) || search?.props?.isRange) {
return {
rangeSeparator: search?.props?.rangeSeparator ?? '至',
startPlaceholder: search?.props?.startPlaceholder ?? '开始时间',
endPlaceholder: search?.props?.endPlaceholder ?? '结束时间',
};
}
const placeholder = search?.props?.placeholder ?? (search?.type?.includes('input') ? `请输入${label}` : `请选择${label}`);
return { placeholder };
});
</script>

View File

@@ -0,0 +1,318 @@
import Api from '@/common/Api';
import { VNode, ref, toValue } from 'vue';
export type FieldNamesProps = {
label: string;
value: string;
children?: string;
};
export type SearchItemType =
| 'input'
| 'input-number'
| 'select'
| 'select-v2'
| 'tree-select'
| 'cascader'
| 'date-picker'
| 'time-picker'
| 'time-select'
| 'switch'
| 'slider';
/**
* 表单组件可选项的api信息
*/
export class OptionsApi {
/**
* 请求获取options的api
*/
api: Api;
/**
* 请求参数
*/
params: any;
/**
* 是否立即执行否则在组件focus事件中获取
*/
immediate: boolean = false;
/**
* 是否只获取一次即若以获取则不继续调用该api
*/
once: boolean = true;
/**
* 转换函数主要用于将响应的api结果转换为满足组件options的结构
*/
convertFn: (apiResp: any) => any;
// remote: boolean = false;
/**
* 远程方法参数属性字段存在该值则说明使用remote-method进行远程搜索
*/
remoteMethodParamProp: string;
withConvertFn(fn: (apiResp: any) => any) {
this.convertFn = fn;
return this;
}
/**
* 立即获取该可选值
* @returns
*/
withImmediate() {
this.immediate = true;
return this;
}
/**
* 设为非一次性api即每次组件focus获取的时候都允许重新获取options
* @returns this
*/
withNoOnce() {
this.once = false;
return this;
}
/**
* 是否使用select的remote方式远程搜索调用
* @param remoteReqParamKey remote请求参数对应的prop需要将输入的value赋值给params[paramProp]进行远程搜索
*/
isRemote(paramProp: string) {
this.remoteMethodParamProp = paramProp;
return this;
}
/**
* 调用api获取组件可选项
* @returns 组件可选项信息
*/
async getOptions() {
let res = await this.api.request(toValue(this.params));
if (this.convertFn) {
res = this.convertFn(res);
}
return res;
}
static new(api: Api, params: any): OptionsApi {
const oa = new OptionsApi();
oa.api = api;
oa.params = params;
return oa;
}
}
/**
* 搜索项
*/
export class SearchItem {
/**
* 属性字段
*/
prop: string;
/**
* 当前项搜索框的 label
*/
label: string;
/**
* 表单项类型input、select、date等
*/
type: SearchItemType;
/**
* select等组件的可选值
*/
options: any;
/**
* 获取可选项的api信息
*/
optionsApi: OptionsApi;
/**
* 插槽名
*/
slot: string;
/**
* 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件
*/
props?: any;
/**
* 搜索项事件,根据 element plus 官方文档来传递,该属性所有值会透传到组件
*/
events?: any;
/**
* 搜索提示
*/
tooltip?: string;
/**
* 搜索项所占用的列数,默认为 1 列
*/
span?: number;
/**
* 搜索字段左侧偏移列数
*/
offset?: number;
/**
* 指定 label && value && children 的 key 值用于select等类型组件
*/
fieldNames: FieldNamesProps;
/**
* 自定义搜索内容渲染tsx语法
*/
render?: (scope: any) => VNode;
constructor(prop: string, label: string) {
this.prop = prop;
this.label = label;
}
static new(prop: string, label: string): SearchItem {
return new SearchItem(prop, label);
}
static input(prop: string, label: string): SearchItem {
const tq = new SearchItem(prop, label);
tq.type = 'input';
return tq;
}
static select(prop: string, label: string): SearchItem {
const tq = new SearchItem(prop, label);
tq.type = 'select';
tq.withOneProps('filterable', true);
return tq;
}
static datePicker(prop: string, label: string): SearchItem {
const tq = new SearchItem(prop, label);
tq.type = 'date-picker';
return tq;
}
static slot(prop: string, label: string, slotName: string): SearchItem {
const tq = new SearchItem(prop, label);
tq.slot = slotName;
return tq;
}
/**
* 为组件设置一个props属性
* @param propsKey 属性key
* @param propsValue 属性value
* @returns
*/
withOneProps(propsKey: string, propsValue: any): SearchItem {
if (!this.props) {
this.props = {};
}
this.props[propsKey] = propsValue;
return this;
}
/**
* 为组件传递组件自身的props属性 (根据 element plus 官方文档来传递,该属性所有值会透传到组件)
* @returns this
*/
withProps(props: any = {}): SearchItem {
this.props = props;
return this;
}
/**
* 为组件传递组件自身事件函数
* @param event 事件名称
* @param fn 事件处理函数
* @returns
*/
bindEvent(event: string, eventFn: any): SearchItem {
if (!this.events) {
this.events = {};
}
this.events[event] = eventFn;
return this;
}
/**
* 设置枚举值用于选择等
* @param enumValues 枚举值对象
* @returns
*/
withEnum(enumValues: any): SearchItem {
this.options = Object.values(enumValues);
return this;
}
/**
* 设置获取组件options可选项值的api配置
* @param optionsApi 可选项api配置
* @returns this
*/
withOptionsApi(optionsApi: OptionsApi): SearchItem {
this.optionsApi = optionsApi;
// 使用api获取组件可选项需要将options转为响应式否则组件无法响应式获取组件可选项
this.options = ref(null);
// 存在远程搜索请求参数prop则为使用远程搜索可选项
if (optionsApi.remoteMethodParamProp) {
return this.withOneProps('remote', true).withOneProps('remote-method', async (value: any) => {
if (!value) {
this.options.value = [];
return;
}
// 将输入的内容赋值为真实api请求参数中指定的属性字段
optionsApi.params[optionsApi.remoteMethodParamProp] = value;
this.options.value = await this.optionsApi.getOptions();
});
}
// 立即执行则直接调用api获取并赋值options
if (this.optionsApi.immediate) {
this.optionsApi.getOptions().then((res) => {
this.options.value = res;
});
} else {
// 注册focus事件在触发focus时赋值options
this.bindEvent('focus', async () => {
if (!toValue(this.options) || !optionsApi.once) {
this.options.value = await this.optionsApi.getOptions();
}
});
}
return this;
}
withSpan(span: number): SearchItem {
this.span = span;
return this;
}
withOptions(options: any): SearchItem {
this.options = options;
return this;
}
/**
* 赋值placeholder
* @param val placeholder
* @returns
*/
withPlaceholder(val: string): SearchItem {
return this.withOneProps('placeholder', val);
}
}

View File

@@ -0,0 +1,138 @@
<template>
<div v-if="items.length" class="search-form">
<el-form ref="formRef" :model="searchParam" label-width="auto">
<Grid ref="gridRef" :collapsed="collapsed" :gap="[20, 0]" :cols="searchCol">
<GridItem v-for="(item, index) in items" :key="item.prop" v-bind="getResponsive(item)" :index="index">
<el-form-item>
<template #label>
<el-space :size="4">
<span>{{ `${item?.label}` }}</span>
<el-tooltip v-if="item.tooltip" :content="item?.tooltip" placement="top">
<SvgIcon name="QuestionFilled" />
</el-tooltip>
</el-space>
<span>:</span>
</template>
<SearchFormItem @keyup.enter="handleItemKeyupEnter(item)" v-if="!item.slot" :item="item" v-model="searchParam[item.prop]" />
<slot v-else :name="item.slot"></slot>
</el-form-item>
</GridItem>
<GridItem suffix>
<div class="operation">
<el-button type="primary" :icon="Search" @click="search" plain> 搜索 </el-button>
<el-button :icon="Delete" @click="reset"> 重置 </el-button>
<el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
{{ collapsed ? '展开' : '合并' }}
<el-icon class="el-icon--right">
<component :is="collapsed ? ArrowDown : ArrowUp"></component>
</el-icon>
</el-button>
</div>
</GridItem>
</Grid>
</el-form>
</div>
</template>
<script setup lang="ts" name="SearchForm">
import { computed, ref } from 'vue';
import { BreakPoint } from '@/components/Grid/interface/index';
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import SearchFormItem from './components/SearchFormItem.vue';
import Grid from '@/components/Grid/index.vue';
import GridItem from '@/components/Grid/components/GridItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { SearchItem } from './index';
interface ProTableProps {
items: SearchItem[]; // 搜索配置项
searchCol: number | Record<BreakPoint, number>;
search: (params: any) => void; // 搜索方法
reset: (params: any) => void; // 重置方法
}
// 默认值
const props = withDefaults(defineProps<ProTableProps>(), {
items: () => [],
modelValue: () => ({}),
});
const searchParam: any = defineModel('modelValue');
// 获取响应式设置
const getResponsive = (item: SearchItem) => {
return {
span: item?.span,
offset: item.offset ?? 0,
// xs: item.search?.xs,
// sm: item.search?.sm,
// md: item.search?.md,
// lg: item.search?.lg,
// xl: item.search?.xl,
};
};
// 是否默认折叠搜索项
const collapsed = ref(true);
// 获取响应式断点
const gridRef = ref();
const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
// 判断是否显示 展开/合并 按钮
const showCollapse = computed(() => {
let show = false;
props.items.reduce((prev, current) => {
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
if (typeof props.searchCol !== 'number') {
if (prev >= props.searchCol[breakPoint.value]) show = true;
} else {
if (prev >= props.searchCol) show = true;
}
return prev;
}, 0);
return show;
});
const handleItemKeyupEnter = (item: SearchItem) => {
if (item.type == 'input') {
props.search(searchParam);
}
};
</script>
<style lang="scss">
.search-form {
padding: 18px 18px 0;
margin-bottom: 10px;
box-sizing: border-box;
overflow-x: hidden;
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
box-shadow: 0 0 12px rgb(0 0 0 / 5%);
.el-form {
.el-form-item__content > * {
width: 100%;
}
// 去除时间选择器上下 padding
.el-range-editor.el-input__wrapper {
padding: 0 10px;
}
.el-form-item {
margin-bottom: 18px !important;
}
}
.operation {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 18px;
}
}
</style>

View File

@@ -6,6 +6,9 @@ import { useUserInfo } from '@/store/userInfo';
* @returns
*/
export function hasPerm(code: string) {
if (!code) {
return true;
}
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
}

View File

@@ -0,0 +1,59 @@
import Contextmenu from './index.vue';
class ContextmenuItem {
clickId: any;
txt: string;
icon: string;
affix: boolean;
permission: string;
/**
* 是否隐藏回调函数
*/
hideFunc: (data: any) => boolean;
onClickFunc: (data: any) => void;
constructor(clickId: any, txt: string) {
this.clickId = clickId;
this.txt = txt;
}
withIcon(icon: string) {
this.icon = icon;
return this;
}
withPermission(permission: string) {
this.permission = permission;
return this;
}
withHideFunc(func: (data: any) => boolean) {
this.hideFunc = func;
return this;
}
withOnClick(func: (data: any) => void) {
this.onClickFunc = func;
return this;
}
/**
* 是否隐藏
* @param data 点击数据项
* @returns
*/
isHide(data: any) {
if (this.hideFunc) {
return this.hideFunc(data);
}
return false;
}
}
export { Contextmenu, ContextmenuItem };

View File

@@ -1,36 +1,41 @@
<template>
<transition name="el-zoom-in-center">
<transition @enter="onEnter" name="el-zoom-in-center">
<div
aria-hidden="true"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
:key="Math.random()"
v-show="state.isShow"
v-show="state.isShow && !allHide"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li
:id="v.clickId"
v-auth="v.permission"
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
v-if="!v.affix && !v.isHide(state.item)"
@click="onCurrentContextmenuClick(v)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
<div v-if="state.arrowLeft > 0" class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { ContextmenuItem } from './index';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useWindowSize } from '@vueuse/core';
// 定义父组件传过来的值
const props = defineProps({
@@ -44,38 +49,82 @@ const props = defineProps({
},
},
items: {
type: Array,
default: [],
type: Array<ContextmenuItem>,
default: () => [],
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
const { width: vw, height: vh } = useWindowSize();
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
dropdownList: [] as ContextmenuItem[],
item: {} as any,
arrowLeft: 10,
dropdown: {
x: 0,
y: 0,
},
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
// 下拉菜单宽高
let contextmenuWidth = 117;
let contextmenuHeight = 117;
// 下拉菜单元素
let ele = null as any;
const onEnter = (el: any) => {
if (ele || el.offsetHeight == 0) {
return;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, item: state.item });
ele = el;
contextmenuHeight = el.offsetHeight;
contextmenuWidth = el.offsetWidth;
setDropdowns(props.dropdown);
};
const setDropdowns = (dropdown: any) => {
let { x, y } = dropdown;
state.arrowLeft = 10;
// `Dropdown 下拉菜单` 的宽度
if (x + contextmenuWidth > vw.value) {
state.arrowLeft = contextmenuWidth - (vw.value - x);
x = vw.value - contextmenuWidth - 5;
}
if (y + contextmenuHeight > vh.value) {
y = vh.value - contextmenuHeight - 5;
state.arrowLeft = 0;
}
state.dropdown.x = x;
state.dropdown.y = y;
};
const allHide = computed(() => {
for (let item of state.dropdownList) {
if (!item.isHide(state.item)) {
return false;
}
}
return true;
});
// 当前项菜单点击
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
// 存在点击事件,则触发该事件函数
if (ci.onClickFunc) {
ci.onClickFunc(state.item);
}
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
@@ -84,29 +133,34 @@ const openContextmenu = (item: any) => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
state.dropdownList = props.items;
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
() => {
// 元素置为空重新在onEnter赋值元素否则会造成堆栈溢出
ele = null;
},
{
deep: true,
}
);
watch(
() => props.items,
(x: any) => {
@@ -130,6 +184,10 @@ defineExpose({
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
padding: 5px 12px;
}
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
@@ -140,3 +198,4 @@ defineExpose({
}
}
</style>
.

View File

@@ -0,0 +1,294 @@
<template>
<div class="crontab">
<el-tabs v-model="state.activeName" @tab-change="changeTab(state.activeName)" type="border-card">
<el-tab-pane label="秒" name="second" v-if="shouldHide('second')">
<CrontabSecond :cron="crontabValueObj" ref="secondRef" />
</el-tab-pane>
<el-tab-pane label="分钟" name="min" v-if="shouldHide('min')">
<CrontabMin :cron="crontabValueObj" ref="minRef" />
</el-tab-pane>
<el-tab-pane label="小时" name="hour" v-if="shouldHide('hour')">
<CrontabHour :cron="crontabValueObj" ref="hourRef" />
</el-tab-pane>
<el-tab-pane label="日" name="day" v-if="shouldHide('day')">
<CrontabDay :cron="crontabValueObj" ref="dayRef" />
</el-tab-pane>
<el-tab-pane label="月" name="mouth" v-if="shouldHide('mouth')">
<CrontabMouth :cron="crontabValueObj" ref="mouthRef" />
</el-tab-pane>
<el-tab-pane label="周" name="week" v-if="shouldHide('week')">
<CrontabWeek :cron="crontabValueObj" ref="weekRef" />
</el-tab-pane>
<el-tab-pane label="年" name="year" v-if="shouldHide('year')">
<CrontabYear :cron="crontabValueObj" ref="yearRef" />
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<table>
<thead>
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
<th>crontab完整表达式</th>
</thead>
<tbody>
<td>
<span>{{ crontabValueObj.second }}</span>
</td>
<td>
<span>{{ crontabValueObj.min }}</span>
</td>
<td>
<span>{{ crontabValueObj.hour }}</span>
</td>
<td>
<span>{{ crontabValueObj.day }}</span>
</td>
<td>
<span>{{ crontabValueObj.mouth }}</span>
</td>
<td>
<span>{{ crontabValueObj.week }}</span>
</td>
<td>
<span>{{ crontabValueObj.year }}</span>
</td>
<td>
<span>{{ contabValueString }}</span>
</td>
</tbody>
</table>
</div>
<CrontabResult :ex="contabValueString"></CrontabResult>
<div class="pop_btn">
<el-button size="small" @click="hidePopup">取消</el-button>
<el-button size="small" type="warning" @click="clearCron">重置</el-button>
<el-button size="small" type="primary" @click="submitFill">确定</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, toRefs, onMounted, reactive, ref, nextTick, watch } from 'vue';
import CrontabSecond from './CrontabSecond.vue';
import CrontabMin from './CrontabMin.vue';
import CrontabHour from './CrontabHour.vue';
import CrontabDay from './CrontabDay.vue';
import CrontabMouth from './CrontabMouth.vue';
import CrontabWeek from './CrontabWeek.vue';
import CrontabYear from './CrontabYear.vue';
import CrontabResult from './CrontabResult.vue';
const secondRef: any = ref(null);
const minRef: any = ref(null);
const hourRef: any = ref(null);
const dayRef: any = ref(null);
const mouthRef: any = ref(null);
const weekRef: any = ref(null);
const yearRef: any = ref(null);
const props = defineProps({
expression: {
type: String,
required: true,
},
hideComponent: {
type: Array,
},
});
//定义事件
const emit = defineEmits(['hide', 'fill']);
const state = reactive({
tabTitles: ['秒', '分钟', '小时', '日', '月', '周', '年'],
tabActive: 0,
activeName: 'second',
myindex: 0,
crontabValueObj: {
second: '*',
min: '*',
hour: '*',
day: '*',
mouth: '*',
week: '?',
year: '',
},
});
const { tabTitles, crontabValueObj } = toRefs(state);
onMounted(() => {
resolveExp();
});
watch(
() => props.expression,
() => {
resolveExp();
}
);
function shouldHide(key: string) {
if (props.hideComponent && props.hideComponent.includes(key)) return false;
return true;
}
function resolveExp() {
//反解析 表达式
if (props.expression) {
let arr = props.expression.split(' ');
if (arr.length >= 6) {
//6 位以上是合法表达式
let obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
mouth: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : '',
};
state.crontabValueObj = {
...obj,
};
}
changeTab(state.activeName);
} else {
//没有传入的表达式 则还原
clearCron();
}
}
// 改变tab
const changeTab = (name: string) => {
nextTick(() => {
getRefByName(name).value?.parse();
});
};
const getRefByName = (name: string) => {
switch (name) {
case 'second':
return secondRef;
case 'min':
return minRef;
case 'hour':
return hourRef;
case 'day':
return dayRef;
case 'mouth':
return mouthRef;
case 'week':
return weekRef;
case 'year':
return yearRef;
}
};
// 隐藏弹窗
function hidePopup() {
emit('hide');
}
// 填充表达式
const submitFill = () => {
emit('fill', contabValueString.value);
hidePopup();
};
const clearCron = () => {
// 还原选择项
state.crontabValueObj = {
second: '*',
min: '*',
hour: '*',
day: '*',
mouth: '*',
week: '?',
year: '',
};
changeTab(state.activeName);
};
const contabValueString = computed(() => {
let obj = state.crontabValueObj;
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
return str;
});
</script>
<style scoped lang="scss">
.pop_btn {
text-align: right;
}
.popup-main {
position: relative;
margin: 10px auto;
background: var(--el-bg-color-overlay);
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
box-sizing: border-box;
line-height: 24px;
margin: 15px auto;
padding: 15px 20px 10px;
border: 1px solid var(--el-border-color);
position: relative;
}
.popup-result .title {
position: absolute;
top: -18px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: var(--el-bg-color-overlay);
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid var(--el-border-color);
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
.crontab {
::v-deep(.el-form-item) {
margin-bottom: 10px !important;
}
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 允许的通配符[, - * / L M] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2"> 不指定 </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
周期从
<el-input-number v-model="cycle01" :min="0" :max="31" /> - <el-input-number v-model="cycle02" :min="0" :max="31" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
<el-input-number v-model="average01" :min="0" :max="31" /> 号开始,每 <el-input-number v-model="average02" :min="0" :max="31" /> 日执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="5">
每月
<el-input-number v-model="workday" :min="0" :max="31" /> 号最近的那个工作日
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="6"> 本月最后一天 </el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="7" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 7" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 31" :key="item" :value="`${item}`">{{ item }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 1,
workday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, workday, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
if (state.radioValue === 1) {
cron.value.day = '*';
cron.value.week = '?';
cron.value.mouth = '*';
} else {
if (cron.value.hour === '*') {
cron.value.hour = '0';
}
if (cron.value.min === '*') {
cron.value.min = '0';
}
if (cron.value.second === '*') {
cron.value.second = '0';
}
}
switch (state.radioValue) {
case 2:
cron.value.day = '?';
break;
case 3:
cron.value.day = state.cycle01 + '-' + state.cycle02;
break;
case 4:
cron.value.day = state.average01 + '/' + state.average02;
break;
case 5:
cron.value.day = state.workday + 'W';
break;
case 6:
cron.value.day = 'L';
break;
case 7:
cron.value.day = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
if (state.radioValue == 3) {
cron.value.day = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 1, 31);
state.average02 = checkNumber(state.average02, 1, 31);
if (state.radioValue == 4) {
cron.value.day = averageTotal.value;
}
}
// 最近工作日值变化时
function workdayChange() {
state.workday = checkNumber(state.workday, 1, 31);
if (state.radioValue == 5) {
cron.value.day = state.workday + 'W';
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 7) {
cron.value.day = checkboxString.value;
}
}
// 父组件传递的week发生变化触发
// function weekChange() {
// //判断week值与day不能同时为“?”
// if (cron.value.week == '?' && state.radioValue == 2) {
// state.radioValue = 1;
// } else if (cron.value.week !== '?' && state.radioValue != 2) {
// state.radioValue = 2;
// }
// }
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算工作日格式
const workdayCheck = computed(() => {
return state.workday;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(workdayCheck, () => {
workdayChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let value = cron.value.day;
if (value === '*') {
state.radioValue = 1;
} else if (value == '?') {
state.radioValue = 2;
} else if (value.indexOf('-') > -1) {
state.radioValue = 3;
let indexArr = value.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
} else if (value.indexOf('/') > -1) {
state.radioValue = 4;
let indexArr = value.split('/') as any;
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
} else if (value.indexOf('W') > -1) {
state.radioValue = 5;
let indexArr = value.split('W') as any;
isNaN(indexArr[0]) ? (state.workday = 0) : (state.workday = indexArr[0]);
} else if (value === 'L') {
state.radioValue = 6;
} else {
state.checkboxList = value.split(',');
state.radioValue = 7;
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,156 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 小时允许的通配符[, - * /] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" /> 小时
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="60" /> 小时开始,每 <el-input-number v-model="average02" :min="0" :max="60" /> 小时执行一次
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
if (state.radioValue === 1) {
cron.value.hour = '*';
cron.value.day = '*';
} else {
if (cron.value.min === '*') {
cron.value.min = '0';
}
if (cron.value.second === '*') {
cron.value.second = '0';
}
}
switch (state.radioValue) {
case 2:
cron.value.hour = state.cycle01 + '-' + state.cycle02;
break;
case 3:
cron.value.hour = state.average01 + '/' + state.average02;
break;
case 4:
cron.value.hour = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, 0, 23);
state.cycle02 = checkNumber(state.cycle02, 0, 23);
if (state.radioValue == 2) {
cron.value.hour = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 0, 23);
state.average02 = checkNumber(state.average02, 0, 23);
if (state.radioValue == 3) {
cron.value.hour = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 4) {
cron.value.hour = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let ins = cron.value.hour;
if (ins === '*') {
state.radioValue = 1;
} else if (ins.indexOf('-') > -1) {
state.radioValue = 2;
let indexArr = ins.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
} else if (ins.indexOf('/') > -1) {
state.radioValue = 3;
let indexArr = ins.split('/') as any;
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
} else {
state.radioValue = 4;
state.checkboxList = ins.split(',');
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,19 @@
<template>
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
<template #prepend>
<el-button @click="showCron = true" icon="Pointer"></el-button>
</template>
</el-input>
<el-dialog title="生成 cron" v-model="showCron" width="600px">
<Crontab :expression="cron" @hide="showCron = false" @fill="(ex: any) => (cron = ex)" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Crontab from '@/components/crontab/Crontab.vue';
const cron = defineModel<string>('modelValue', { required: true });
const showCron = ref(false);
</script>

View File

@@ -0,0 +1,152 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 分钟允许的通配符[, - * /] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" /> 分钟
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="60" /> 分钟开始,每 <el-input-number v-model="average02" :min="0" :max="60" /> 分钟执行一次
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
if (state.radioValue !== 1 && cron.value.second === '*') {
cron.value.second = '0';
}
switch (state.radioValue) {
case 1:
cron.value.min = '*';
cron.value.hour = '*';
break;
case 2:
cron.value.min = state.cycle01 + '-' + state.cycle02;
break;
case 3:
cron.value.min = state.average01 + '/' + state.average02;
break;
case 4:
cron.value.min = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, 0, 59);
state.cycle02 = checkNumber(state.cycle02, 0, 59);
if (state.radioValue == 2) {
cron.value.min = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 0, 59);
state.average02 = checkNumber(state.average02, 1, 59);
if (state.radioValue == 3) {
cron.value.min = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 4) {
cron.value.min = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let ins = cron.value.min;
if (ins === '*') {
state.radioValue = 1;
} else if (ins.indexOf('-') > -1) {
state.radioValue = 2;
let indexArr = ins.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
} else if (ins.indexOf('/') > -1) {
state.radioValue = 3;
let indexArr = ins.split('/') as any;
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
} else {
state.radioValue = 4;
state.checkboxList = ins.split(',');
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,163 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 允许的通配符[, - * /] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="1" :max="12" /> - <el-input-number v-model="cycle02" :min="1" :max="12" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="1" :max="12" /> 月开始,每 <el-input-number v-model="average02" :min="1" :max="12" /> 月月执行一次
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 12" :key="item" :value="`${item}`">{{ item }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
if (state.radioValue === 1) {
cron.value.mouth = '*';
cron.value.year = '*';
} else {
if (cron.value.day === '*') {
cron.value.day = '0';
}
if (cron.value.hour === '*') {
cron.value.hour = '0';
}
if (cron.value.min === '*') {
cron.value.min = '0';
}
if (cron.value.second === '*') {
cron.value.second = '0';
}
}
switch (state.radioValue) {
case 2:
cron.value.mouth = state.cycle01 + '-' + state.cycle02;
break;
case 3:
cron.value.mouth = state.average01 + '/' + state.average02;
break;
case 4:
cron.value.mouth = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, 1, 12);
state.cycle02 = checkNumber(state.cycle02, 1, 12);
if (state.radioValue == 2) {
cron.value.mouth = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 1, 12);
state.average02 = checkNumber(state.average02, 1, 12);
if (state.radioValue == 3) {
cron.value.mouth = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 4) {
cron.value.mouth = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let ins = cron.value.mouth;
if (ins === '*') {
state.radioValue = 1;
} else if (ins.indexOf('-') > -1) {
state.radioValue = 2;
let indexArr = ins.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
} else if (ins.indexOf('/') > -1) {
state.radioValue = 3;
let indexArr = ins.split('/') as any;
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
} else {
state.radioValue = 4;
state.checkboxList = ins.split(',');
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,588 @@
<template>
<div class="popup-result">
<p class="title">最近5次运行时间</p>
<ul class="popup-result-scroll">
<template v-if="isShow">
<li v-for="item in resultList" :key="item">{{ item }}</li>
</template>
<li v-else>计算结果中...</li>
</ul>
</div>
</template>
<script lang="js" setup>
import { toRefs, watch, onMounted, reactive } from 'vue';
const props = defineProps({
ex: {
type: String,
required: true,
},
});
const state = reactive({
dayRule: '',
dayRuleSup: '',
dateArr: [],
resultList: [],
isShow: false,
});
const { resultList, isShow } = toRefs(state);
watch(
() => props.ex,
() => {
expressionChange();
}
);
onMounted(() => {
expressionChange();
});
// 表达式值变化时,开始去计算结果
function expressionChange() {
// 计算开始-隐藏结果
state.isShow = false;
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = props.ex.split(' ');
// 用于记录进入循环的次数
let nums = 0;
// 用于暂时存符号时间规则结果的数组
let resultArr = [];
// 获取当前时间精确至[年、月、日、时、分、秒]
let nTime = new Date();
let nYear = nTime.getFullYear();
let nMouth = nTime.getMonth() + 1;
let nDay = nTime.getDate();
let nHour = nTime.getHours();
let nMin = nTime.getMinutes();
let nSecond = nTime.getSeconds();
// 根据规则获取到近100年可能年数组、月数组等等
getSecondArr(ruleArr[0]);
getMinArr(ruleArr[1]);
getHourArr(ruleArr[2]);
getDayArr(ruleArr[3]);
getMouthArr(ruleArr[4]);
getWeekArr(ruleArr[5]);
getYearArr(ruleArr[6], nYear);
// 将获取到的数组赋值-方便使用
let sDate = state.dateArr[0];
let mDate = state.dateArr[1];
let hDate = state.dateArr[2];
let DDate = state.dateArr[3];
let MDate = state.dateArr[4];
let YDate = state.dateArr[5];
// 获取当前时间在数组中的索引
let sIdx = getIndex(sDate, nSecond);
let mIdx = getIndex(mDate, nMin);
let hIdx = getIndex(hDate, nHour);
let DIdx = getIndex(DDate, nDay);
let MIdx = getIndex(MDate, nMouth);
let YIdx = getIndex(YDate, nYear);
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function () {
sIdx = 0;
nSecond = sDate[sIdx];
};
const resetMin = function () {
mIdx = 0;
nMin = mDate[mIdx];
resetSecond();
};
const resetHour = function () {
hIdx = 0;
nHour = hDate[hIdx];
resetMin();
};
const resetDay = function () {
DIdx = 0;
nDay = DDate[DIdx];
resetHour();
};
const resetMouth = function () {
MIdx = 0;
nMouth = MDate[MIdx];
resetDay();
};
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMouth();
}
// 如果当前月份不为数组中当前值
if (nMouth !== MDate[MIdx]) {
resetDay();
}
// 如果当前“日”不为数组中当前值
if (nDay !== DDate[DIdx]) {
resetHour();
}
// 如果当前“时”不为数组中当前值
if (nHour !== hDate[hIdx]) {
resetMin();
}
// 如果当前“分”不为数组中当前值
if (nMin !== mDate[mIdx]) {
resetSecond();
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi];
// 如果到达最大值时
if (nMouth > MDate[MDate.length - 1]) {
resetMouth();
continue;
}
// 循环月份数组
goMouth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 赋值、方便后面运算
let MM = MDate[Mi];
MM = MM < 10 ? '0' + MM : MM;
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue;
}
// 循环日期数组
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 赋值、方便后面运算
let DD = DDate[Di];
let thisDD = DD < 10 ? '0' + DD : DD;
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour();
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue goMouth;
}
continue;
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (
checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true &&
state.dayRule !== 'workDay' &&
state.dayRule !== 'lastWeek' &&
state.dayRule !== 'lastDay'
) {
resetDay();
continue goMouth;
}
// 如果日期规则中有值时
if (state.dayRule == 'lastDay') {
//如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
}
}
} else if (state.dayRule == 'workDay') {
//校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
}
}
// 获取达到条件的日期是星期X
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
// 当星期日时
if (thisWeek == 0) {
//先找下一个日,并判断是否为月底
DD++;
thisDD = DD < 10 ? '0' + DD : DD;
//判断下一日已经不是合法日期
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3;
}
} else if (thisWeek == 6) {
//当星期6时只需判断不是1号就可进行操作
if (state.dayRuleSup !== 1) {
DD--;
} else {
DD += 2;
}
}
} else if (state.dayRule == 'weekDay') {
//如果指定了是星期几
//获取当前日期是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
//校验当前星期是否在星期池state.dayRuleSup
if (Array.indexOf(state.dayRuleSup, thisWeek) < 0) {
// 如果到达最大值时
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue goMouth;
}
continue;
}
} else if (state.dayRule == 'assWeek') {
//如果指定了是第几周的星期几
//获取每月1号是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
if (state.dayRuleSup[1] >= thisWeek) {
DD = (state.dayRuleSup[0] - 1) * 7 + state.dayRuleSup[1] - thisWeek + 1;
} else {
DD = state.dayRuleSup[0] * 7 + state.dayRuleSup[1] - thisWeek + 1;
}
} else if (state.dayRule == 'lastWeek') {
//如果指定了每月最后一个星期几
//校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
}
}
//获取月末最后一天是星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
//找到要求中最近的那个星期几
if (state.dayRuleSup < thisWeek) {
DD -= thisWeek - state.dayRuleSup;
} else if (state.dayRuleSup > thisWeek) {
DD -= 7 - (state.dayRuleSup - thisWeek);
}
}
// 判断时间值是否小于10置换成“05”这种格式
DD = DD < 10 ? '0' + DD : DD;
// 循环“时”数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi];
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin();
if (hi == hDate.length - 1) {
resetHour();
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue goMouth;
}
continue goDay;
}
continue;
}
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi];
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond();
if (mi == mDate.length - 1) {
resetMin();
if (hi == hDate.length - 1) {
resetHour();
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue goMouth;
}
continue goDay;
}
continue goHour;
}
continue;
}
// 循环"秒"数组
// eslint-disable-next-line no-unused-labels
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si];
// 添加当前时间(时间合法性在日期循环时已经判断)
if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss);
nums++;
}
//如果条数满了就退出循环
if (nums == 5) break goYear;
//如果到达最大值时
if (si == sDate.length - 1) {
resetSecond();
if (mi == mDate.length - 1) {
resetMin();
if (hi == hDate.length - 1) {
resetHour();
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMouth();
continue goYear;
}
continue goMouth;
}
continue goDay;
}
continue goHour;
}
continue goMin;
}
} //goSecond
} //goMin
} //goHour
} //goDay
} //goMouth
}
// 判断100年内的结果条数
if (resultArr.length == 0) {
state.resultList = ['没有达到条件的结果!'];
} else {
state.resultList = resultArr;
if (resultArr.length !== 5) {
state.resultList.push('最近100年内只有上面' + resultArr.length + '条结果!');
}
}
// 计算完成-显示结果
state.isShow = true;
}
//用于计算某位数字在数组中的索引
function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0;
} else {
for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) {
return i + 1;
}
}
}
}
// 获取"年"数组
function getYearArr(rule, year) {
state.dateArr[5] = getOrderArr(year, year + 100);
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
state.dateArr[5] = getCycleArr(rule, year + 100, false);
} else if (rule.indexOf('/') >= 0) {
state.dateArr[5] = getAverageArr(rule, year + 100);
} else if (rule !== '*') {
state.dateArr[5] = getAssignArr(rule);
}
}
}
// 获取"月"数组
function getMouthArr(rule) {
state.dateArr[4] = getOrderArr(1, 12);
if (rule.indexOf('-') >= 0) {
state.dateArr[4] = getCycleArr(rule, 12, false);
} else if (rule.indexOf('/') >= 0) {
state.dateArr[4] = getAverageArr(rule, 12);
} else if (rule !== '*') {
state.dateArr[4] = getAssignArr(rule);
}
}
// 获取"日"数组-主要为日期规则
function getWeekArr(rule) {
//只有当日期规则的两个值均为“”时则表达日期是有选项的
if (state.dayRule == '' && state.dayRuleSup == '') {
if (rule.indexOf('-') >= 0) {
state.dayRule = 'weekDay';
state.dayRuleSup = getCycleArr(rule, 7, false);
} else if (rule.indexOf('#') >= 0) {
state.dayRule = 'assWeek';
let matchRule = rule.match(/[0-9]{1}/g);
state.dayRuleSup = [Number(matchRule[0]), Number(matchRule[1])];
state.dateArr[3] = [1];
if (state.dayRuleSup[1] == 7) {
state.dayRuleSup[1] = 0;
}
} else if (rule.indexOf('L') >= 0) {
state.dayRule = 'lastWeek';
state.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
state.dateArr[3] = [31];
if (state.dayRuleSup == 7) {
state.dayRuleSup = 0;
}
} else if (rule !== '*' && rule !== '?') {
state.dayRule = 'weekDay';
state.dayRuleSup = getAssignArr(rule);
}
//如果weekDay时将7调整为0【week值0即是星期日】
if (state.dayRule == 'weekDay') {
for (let i = 0; i < state.dayRuleSup.length; i++) {
if (state.dayRuleSup[i] == 7) {
state.dayRuleSup[i] = 0;
}
}
}
}
}
// 获取"日"数组-少量为日期规则
function getDayArr(rule) {
state.dateArr[3] = getOrderArr(1, 31);
state.dayRule = '';
state.dayRuleSup = '';
if (rule.indexOf('-') >= 0) {
state.dateArr[3] = getCycleArr(rule, 31, false);
state.dayRuleSup = 'null';
} else if (rule.indexOf('/') >= 0) {
state.dateArr[3] = getAverageArr(rule, 31);
state.dayRuleSup = 'null';
} else if (rule.indexOf('W') >= 0) {
state.dayRule = 'workDay';
state.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
state.dateArr[3] = [state.dayRuleSup];
} else if (rule.indexOf('L') >= 0) {
state.dayRule = 'lastDay';
state.dayRuleSup = 'null';
state.dateArr[3] = [31];
} else if (rule !== '*' && rule !== '?') {
state.dateArr[3] = getAssignArr(rule);
state.dayRuleSup = 'null';
} else if (rule == '*') {
state.dayRuleSup = 'null';
}
}
// 获取"时"数组
function getHourArr(rule) {
state.dateArr[2] = getOrderArr(0, 23);
if (rule.indexOf('-') >= 0) {
state.dateArr[2] = getCycleArr(rule, 24, true);
} else if (rule.indexOf('/') >= 0) {
state.dateArr[2] = getAverageArr(rule, 23);
} else if (rule !== '*') {
state.dateArr[2] = getAssignArr(rule);
}
}
// 获取"分"数组
function getMinArr(rule) {
state.dateArr[1] = getOrderArr(0, 59);
if (rule.indexOf('-') >= 0) {
state.dateArr[1] = getCycleArr(rule, 60, true);
} else if (rule.indexOf('/') >= 0) {
state.dateArr[1] = getAverageArr(rule, 59);
} else if (rule !== '*') {
state.dateArr[1] = getAssignArr(rule);
}
}
// 获取"秒"数组
function getSecondArr(rule) {
state.dateArr[0] = getOrderArr(0, 59);
if (rule.indexOf('-') >= 0) {
state.dateArr[0] = getCycleArr(rule, 60, true);
} else if (rule.indexOf('/') >= 0) {
state.dateArr[0] = getAverageArr(rule, 59);
} else if (rule !== '*') {
state.dateArr[0] = getAssignArr(rule);
}
}
// 根据传进来的min-max返回一个顺序的数组
function getOrderArr(min, max) {
let arr = [];
for (let i = min; i <= max; i++) {
arr.push(i);
}
return arr;
}
// 根据规则中指定的零散值返回一个数组
function getAssignArr(rule) {
let arr = [];
let assiginArr = rule.split(',');
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i]);
}
arr.sort(compare);
return arr;
}
// 根据一定算术规则计算返回一个数组
function getAverageArr(rule, limit) {
let arr = [];
let agArr = rule.split('/');
let min = Number(agArr[0]);
let step = Number(agArr[1]);
while (min <= limit) {
arr.push(min);
min += step;
}
return arr;
}
// 根据规则返回一个具有周期性的数组
function getCycleArr(rule, limit, status) {
//status--表示是否从0开始则从1开始
let arr = [];
let cycleArr = rule.split('-');
let min = Number(cycleArr[0]);
let max = Number(cycleArr[1]);
if (min > max) {
max += limit;
}
for (let i = min; i <= max; i++) {
let add = 0;
if (status == false && i % limit == 0) {
add = limit;
}
arr.push(Math.round((i % limit) + add));
}
arr.sort(compare);
return arr;
}
//比较数字大小用于Array.sort
function compare(value1, value2) {
if (value2 - value1 > 0) {
return -1;
} else {
return 1;
}
}
// 格式化日期格式如2017-9-19 18:04:33
function formatDate(value, type) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value;
let Y = time.getFullYear();
let M = time.getMonth() + 1;
let D = time.getDate();
let h = time.getHours();
let m = time.getMinutes();
let s = time.getSeconds();
let week = time.getDay();
// 如果传递了type的话
if (type == undefined) {
return (
Y +
'-' +
(M < 10 ? '0' + M : M) +
'-' +
(D < 10 ? '0' + D : D) +
' ' +
(h < 10 ? '0' + h : h) +
':' +
(m < 10 ? '0' + m : m) +
':' +
(s < 10 ? '0' + s : s)
);
} else if (type == 'week') {
return week;
}
}
// 检查日期是否存在
function checkDate(value) {
let time = new Date(value);
let format = formatDate(time);
return value == format ? true : false;
}
</script>

View File

@@ -0,0 +1,149 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 允许的通配符[, - * /] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="60" /> 秒开始,每 <el-input-number v-model="average02" :min="0" :max="60" /> 秒执行一次
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
switch (state.radioValue) {
case 1:
cron.value.second = '*';
cron.value.min = '*';
break;
case 2:
cron.value.second = state.cycle01 + '-' + state.cycle02;
break;
case 3:
cron.value.second = state.average01 + '/' + state.average02;
break;
case 4:
cron.value.second = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, 0, 59);
state.cycle02 = checkNumber(state.cycle02, 0, 59);
if (state.radioValue == 2) {
cron.value.second = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 0, 59);
state.average02 = checkNumber(state.average02, 1, 59);
if (state.radioValue == 3) {
cron.value.second = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 4) {
cron.value.second = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let ins = cron.value.second;
if (ins === '*') {
state.radioValue = 1;
} else if (ins.indexOf('-') > -1) {
state.radioValue = 2;
let indexArr = ins.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
} else if (ins.indexOf('/') > -1) {
state.radioValue = 3;
let indexArr = ins.split('/') as any;
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
} else {
state.radioValue = 4;
state.checkboxList = ins.split(',');
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,205 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> 允许的通配符[, - * / L #] </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2"> 不指定 </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
周期从星期
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
<el-input-number v-model="cycle02" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
<el-input-number v-model="average01" :min="1" :max="4" /> 周的星期
<el-input-number v-model="average02" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="5">
本月最后一个星期
<el-input-number v-model="weekday" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="6" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 6" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ item }}</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
radioValue: 2,
weekday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [] as any,
weekList: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList, weekday, weekList } = toRefs(state);
// 单选按钮值变化时
function radioChange() {
if (state.radioValue === 1) {
cron.value.week = '*';
cron.value.year = '*';
} else {
if (cron.value.mouth === '*') {
cron.value.mouth = '0';
}
if (cron.value.day === '*') {
cron.value.day = '0';
}
if (cron.value.hour === '*') {
cron.value.hour = '0';
}
if (cron.value.min === '*') {
cron.value.min = '0';
}
if (cron.value.second === '*') {
cron.value.second = '0';
}
}
switch (state.radioValue) {
case 2:
cron.value.week = '?';
break;
case 3:
cron.value.week = state.cycle01 + '-' + state.cycle02;
break;
case 4:
cron.value.week = state.average01 + '#' + state.average02;
break;
case 5:
cron.value.week = state.weekday + 'L';
break;
case 6:
cron.value.week = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, 1, 7);
state.cycle02 = checkNumber(state.cycle02, 1, 7);
if (state.radioValue == 3) {
cron.value.week = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, 1, 4);
state.average02 = checkNumber(state.average02, 1, 7);
if (state.radioValue == 4) {
cron.value.week = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 6) {
cron.value.week = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '#' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
watch(
() => state.weekday,
() => {
state.weekday = checkNumber(state.weekday, 1, 7);
if (state.radioValue == 5) {
cron.value.week = state.weekday + 'L';
}
}
);
const parse = () => {
//反解析
let value = cron.value.week;
if (!value) {
return;
}
if (value === '*') {
state.radioValue = 1;
} else if (value == '?') {
state.radioValue = 2;
} else if (value.indexOf('-') > -1) {
let indexArr = value.split('-') as any;
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
state.cycle02 = indexArr[1];
state.radioValue = 3;
} else if (value.indexOf('#') > -1) {
let indexArr = value.split('#') as any;
isNaN(indexArr[0]) ? (state.average01 = 1) : (state.average01 = indexArr[0]);
state.average02 = indexArr[1];
state.radioValue = 4;
} else if (value.indexOf('L') > -1) {
let indexArr = value.split('L') as any;
isNaN(indexArr[0]) ? (state.weekday = 1) : (state.weekday = indexArr[0]);
state.radioValue = 5;
} else {
state.checkboxList = value.split(',');
state.radioValue = 6;
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,159 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio :label="1" v-model="radioValue"> 不填允许的通配符[, - * /] </el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="2" v-model="radioValue"> 每年 </el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="3" v-model="radioValue">
周期从
<el-input-number v-model="cycle01" :min="fullYear" /> -
<el-input-number v-model="cycle02" :min="fullYear" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="4" v-model="radioValue">
<el-input-number v-model="average01" :min="fullYear" /> 年开始,每 <el-input-number v-model="average02" :min="fullYear" /> 年执行一次
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="5" class="mr5"> 指定 </el-radio>
<el-select @click="radioValue = 5" class="w100" clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 9" :key="item" :value="`${item - 1 + fullYear}`" :label="item - 1 + fullYear" />
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, toRefs, watch, onMounted, reactive } from 'vue';
import { checkNumber, CrontabValueObj } from './index';
const cron = defineModel<CrontabValueObj>('cron', { required: true });
const state = reactive({
fullYear: 0,
radioValue: 1,
cycle01: 0,
cycle02: 0,
average01: 0,
average02: 1,
checkboxList: [] as any,
});
const { radioValue, cycle01, cycle02, average01, average02, checkboxList, fullYear } = toRefs(state);
onMounted(() => {
// 仅获取当前年份
state.fullYear = Number(new Date().getFullYear());
});
// 单选按钮值变化时
function radioChange() {
switch (state.radioValue) {
case 1:
cron.value.year = '';
break;
case 2:
cron.value.year = '*';
break;
case 3:
cron.value.year = state.cycle01 + '-' + state.cycle02;
break;
case 4:
cron.value.year = state.average01 + '/' + state.average02;
break;
case 5:
cron.value.year = checkboxString.value;
break;
}
}
// 周期两个值变化时
function cycleChange() {
state.cycle01 = checkNumber(state.cycle01, state.fullYear, state.fullYear + 100);
state.cycle02 = checkNumber(state.cycle02, state.fullYear + 1, state.fullYear + 101);
if (state.radioValue == 3) {
cron.value.year = cycleTotal.value;
}
}
// 平均两个值变化时
function averageChange() {
state.average01 = checkNumber(state.average01, state.fullYear, state.fullYear + 100);
state.average02 = checkNumber(state.average02, 1, 10);
if (state.radioValue == 4) {
cron.value.year = averageTotal.value;
}
}
// checkbox值变化时
function checkboxChange() {
if (state.radioValue == 5) {
cron.value.year = checkboxString.value;
}
}
// 计算两个周期值
const cycleTotal = computed(() => {
return state.cycle01 + '-' + state.cycle02;
});
// 计算平均用到的值
const averageTotal = computed(() => {
return state.average01 + '/' + state.average02;
});
// 计算勾选的checkbox值合集
const checkboxString = computed(() => {
let str = state.checkboxList.join();
return str == '' ? '*' : str;
});
watch(
() => state.radioValue,
() => {
radioChange();
}
);
watch(cycleTotal, () => {
cycleChange();
});
watch(averageTotal, () => {
averageChange();
});
watch(checkboxString, () => {
checkboxChange();
});
const parse = () => {
//反解析
let value = cron.value.year;
if (value == '') {
state.radioValue = 1;
} else if (value == '*') {
state.radioValue = 2;
} else if (value.indexOf('-') > -1) {
state.radioValue = 3;
} else if (value.indexOf('/') > -1) {
state.radioValue = 4;
} else {
state.checkboxList = value.split(',');
state.radioValue = 5;
}
};
defineExpose({ parse });
</script>

View File

@@ -0,0 +1,21 @@
// 表单选项的子组件校验数字格式
export function checkNumber(value: any, minLimit: number, maxLimit: number) {
//检查必须为整数
value = Math.floor(value);
if (value < minLimit) {
value = minLimit;
} else if (value > maxLimit) {
value = maxLimit;
}
return value;
}
export interface CrontabValueObj {
second: string;
min: string;
hour: string;
day: string;
mouth: string;
week: string;
year: string;
}

View File

@@ -1,212 +1,39 @@
<template>
<div class="dynamic-form">
<el-form
:model="form"
ref="dynamicForm"
:label-width="formInfo.labelWidth ? formInfo.labelWidth : '100px'"
:size="formInfo.size ? formInfo.size : 'small'"
>
<el-row v-for="fr in formInfo.formRows" :key="fr.key">
<el-col v-for="item in fr" :key="item.key" :span="item.span ? item.span : 24 / fr.length">
<el-form-item :prop="item.name" :label="item.label" :label-width="item.labelWidth" :required="item.required" :rules="item.rules">
<!-- input输入框 -->
<el-input
v-if="item.type === 'input'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:type="item.inputType"
clearable
<div class="dynamic-form">
<el-form v-bind="$attrs" ref="formRef" :model="modelValue" label-width="auto">
<el-form-item v-for="item in props.formItems as any" :key="item.name" :prop="item.model" :label="item.name" :required="item.required ?? true">
<el-input v-if="!item.options" v-model="modelValue[item.model]" :placeholder="item.placeholder" autocomplete="off" clearable></el-input>
@change="item.change ? item.change(form) : ''"
></el-input>
<!-- 普通文本信息可用于不可修改字段等 -->
<span v-else-if="item.type === 'text'">{{ form[item.name] }}</span>
<!-- select选择框 -->
<!-- optionProps.label: 指定option中的label为options对象的某个属性值默认就是label字段 -->
<!-- optionProps.value: 指定option中的value为options对象的某个属性值默认就是value字段 -->
<el-select
v-else-if="item.type === 'select'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:filterable="item.filterable"
:remote="item.remote"
:remote-method="item.remoteMethod"
@focus="item.focus ? item.focus(form) : ''"
clearable
:disabled="item.updateDisabled && form.id != null"
style="width: 100%"
>
<el-option
v-for="i in item.options"
:key="i.key"
:label="i[item.optionProps ? item.optionProps.label || 'label' : 'label']"
:value="i[item.optionProps ? item.optionProps.value || 'value' : 'value']"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row type="flex" justify="center">
<slot name="btns" :submitDisabled="submitDisabled" :data="form" :submit="submit">
<el-button @click="reset" size="small"> </el-button>
<el-button type="primary" @click="submit" size="small"> </el-button>
</slot>
</el-row>
</el-form>
</div>
<el-select v-else v-model="modelValue[item.model]" :placeholder="item.placeholder" filterable autocomplete="off" clearable style="width: 100%">
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue';
import { ElMessage } from 'element-plus';
<script lang="ts" setup>
import { ref } from 'vue';
export default defineComponent({
name: 'DynamicForm',
props: {
formInfo: { type: Object },
formData: { type: [Object, Boolean] },
},
setup(props: any, context) {
const dynamicForm: any = ref();
const state = reactive({
form: {},
submitDisabled: false,
});
watch(props.formData, (newValue, oldValue) => {
if (props.formData) {
state.form = { ...props.formData };
}
});
const submit = () => {
dynamicForm.value.validate((valid: boolean) => {
if (valid) {
// 提交的表单数据
const subform = { ...state.form };
const operation = state.form['id'] ? props.formInfo['updateApi'] : props.formInfo['createApi'];
if (operation) {
state.submitDisabled = true;
operation.request(state.form).then(
(res: any) => {
ElMessage.success('保存成功');
context.emit('submitSuccess', subform);
state.submitDisabled = false;
// this.cancel()
},
(e: any) => {
state.submitDisabled = false;
}
);
} else {
ElMessage.error('表单未设置对应的提交权限');
}
} else {
return false;
}
});
};
const reset = () => {
context.emit('reset');
resetFieldsAndData();
};
/**
* 重置表单以及表单数据
*/
const resetFieldsAndData = () => {
// 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
const df: any = dynamicForm;
df.resetFields();
// 重置表单数据
state.form = {};
};
return {
...toRefs(state),
dynamicForm,
submit,
reset,
resetFieldsAndData,
};
},
const props = defineProps({
formItems: { type: Array },
});
// @Component({
// name: 'DynamicForm'
// })
// export default class DynamicForm extends Vue {
// @Prop()
// formInfo: object
// @Prop()
// formData: [object,boolean]|undefined
// form = {}
// submitDisabled = false
const formRef: any = ref();
// @Watch('formData', { deep: true })
// onRoleChange() {
// if (this.formData) {
// this.form = { ...this.formData }
// }
// }
const modelValue: any = defineModel();
// submit() {
// const dynamicForm: any = this.$refs['dynamicForm']
// dynamicForm.validate((valid: boolean) => {
// if (valid) {
// // 提交的表单数据
// const subform = { ...this.form }
// const operation = this.form['id']
// ? this.formInfo['updateApi']
// : this.formInfo['createApi']
// if (operation) {
// this.submitDisabled = true
// operation.request(this.form).then(
// (res: any) => {
// ElMessage.success('保存成功')
// this.$emit('submitSuccess', subform)
// this.submitDisabled = false
// // this.cancel()
// },
// (e: any) => {
// this.submitDisabled = false
// }
// )
// } else {
// ElMessage.error('表单未设置对应的提交权限')
// }
// } else {
// return false
// }
// })
// }
const validate = async (func: any) => {
await formRef.value.validate(func);
};
// reset() {
// this.$emit('reset')
// this.resetFieldsAndData()
// }
const resetFields = () => {
formRef.value.resetFields();
};
// /**
// * 重置表单以及表单数据
// */
// resetFieldsAndData() {
// // 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
// const df: any = this.$refs['dynamicForm']
// df.resetFields()
// // 重置表单数据
// this.form = {}
// }
// mounted() {
// // 组件可能还没有初始化第一次初始化的时候无法watch对象
// this.form = { ...this.formData }
// }
// }
defineExpose({
validate,
resetFields,
});
</script>
<style lang="scss"></style>

View File

@@ -1,60 +1,52 @@
<template>
<div class="form-dialog">
<el-dialog :title="title" v-model="visible" :width="dialogWidth ? dialogWidth : '500px'">
<dynamic-form ref="df" :form-info="formInfo" :form-data="formData" @submitSuccess="submitSuccess">
<template #btns="props">
<slot name="btns">
<el-button :disabled="props.submitDisabled" type="primary" @click="props.submit" size="small"> </el-button>
<el-button :disabled="props.submitDisabled" @click="close()" size="small"> </el-button>
</slot>
</template>
</dynamic-form>
</el-dialog>
</div>
<div class="form-dialog">
<el-dialog @close="close" v-bind="$attrs" :title="title" v-model="dialogVisible" :width="width">
<dynamic-form ref="df" :form-items="props.formItems" v-model="formData" />
<template #footer>
<span>
<slot name="btns">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="confirm"> </el-button>
</slot>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import DynamicForm from './DynamicForm.vue';
export default defineComponent({
name: 'DynamicFormDialog',
components: {
DynamicForm,
},
props: {
visible: { type: Boolean },
dialogWidth: { type: String },
title: { type: String },
formInfo: { type: Object },
formData: { type: [Object, Boolean] },
},
setup(props: any, context) {
const df: any = ref();
const emit = defineEmits(['close', 'confirm']);
const close = () => {
// 更新父组件visible prop对应的值为false
context.emit('update:visible', false);
// 关闭窗口则将表单数据置为null
context.emit('update:formData', null);
context.emit('close');
// 取消动态表单的校验以及form数据
setTimeout(() => {
df.resetFieldsAndData();
}, 200);
};
const submitSuccess = (form: any) => {
context.emit('submitSuccess', form);
close();
};
return {
df,
close,
submitSuccess,
};
},
const props = defineProps({
title: { type: String },
width: { type: [String, Number], default: '500px' },
formItems: { type: Array },
});
const df: any = ref();
const formData: any = defineModel('modelValue');
const dialogVisible = defineModel<boolean>('visible', { default: false });
const close = () => {
emit('close');
// 取消动态表单的校验
setTimeout(() => {
formData.value = {};
df.value.resetFields();
}, 200);
};
const confirm = () => {
df.value.validate((valid: any) => {
if (!valid) {
return false;
}
emit('confirm', formData.value);
});
};
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="dynamic-form-edit w100">
<el-table :data="formItems" stripe class="w100" empty-text="暂无表单项">
<el-table-column prop="name" label="model" min-width="100px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addItem()"> </el-button>
<span class="ml10">model</span>
</template>
<template #default="scope">
<el-input v-model="scope.row['model']" placeholder="字段model" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="label" min-width="100px">
<template #default="scope">
<el-input v-model="scope.row['name']" placeholder="字段title" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="placeholder" label="字段说明" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['placeholder']" placeholder="字段说明" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="options" label="可选值" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['options']" placeholder="可选值 ,分割" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="required" label="必填" min-width="40px">
<template #default="scope">
<el-checkbox v-model="scope.row['required']" />
</template>
</el-table-column>
<el-table-column label="操作" wdith="20px">
<template #default="scope">
<el-button type="danger" @click="deleteItem(scope.$index)" icon="delete" plain></el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
const formItems: any = defineModel('modelValue');
const addItem = () => {
formItems.value.push({});
};
const deleteItem = (index: any) => {
formItems.value.splice(index, 1);
};
</script>
<style lang="scss"></style>

View File

@@ -1,2 +1,3 @@
export { default as DynamicForm } from './DynamicForm.vue';
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
export { default as DynamicFormEdit } from './DynamicFormEdit.vue';

View File

@@ -0,0 +1,86 @@
<template>
<div id="echarts" ref="chartRef" :style="echartsStyle" />
</template>
<script setup lang="ts" name="ECharts">
import { ref, onMounted, onBeforeUnmount, watch, computed, markRaw, nextTick } from 'vue';
import { EChartsType, ECElementEvent } from 'echarts/core';
import echarts, { ECOption } from './config';
import { useDebounceFn, useEventListener } from '@vueuse/core';
import { light } from './config/theme';
// import { useThemeConfig } from '@/store/themeConfig';
// import { storeToRefs } from 'pinia';
interface Props {
option: ECOption;
renderer?: 'canvas' | 'svg';
resize?: boolean;
theme?: Object | string;
width?: number | string;
height?: number | string;
onClick?: (event: ECElementEvent) => any;
}
const props = withDefaults(defineProps<Props>(), {
renderer: 'canvas',
theme: light as any,
resize: true,
});
const echartsStyle = computed(() => {
return props.width || props.height ? { height: props.height + 'px', width: props.width + 'px' } : { height: '100%', width: '100%' };
});
const chartRef = ref<HTMLDivElement | HTMLCanvasElement>();
const chartInstance = ref<EChartsType>();
const draw = () => {
if (chartInstance.value) {
chartInstance.value.setOption(props.option, { notMerge: true });
}
};
watch(props, () => {
draw();
});
const handleClick = (event: ECElementEvent) => props.onClick && props.onClick(event);
const init = () => {
if (!chartRef.value) return;
chartInstance.value = echarts.getInstanceByDom(chartRef.value);
if (!chartInstance.value) {
chartInstance.value = markRaw(
echarts.init(chartRef.value, props.theme, {
renderer: props.renderer,
})
);
chartInstance.value.on('click', handleClick);
draw();
}
};
const resize = () => {
if (chartInstance.value && props.resize) {
chartInstance.value.resize({ animation: { duration: 300 } });
}
};
const debouncedResize = useDebounceFn(resize, 300, { maxWait: 800 });
onMounted(() => {
nextTick(() => init());
useEventListener('resize', debouncedResize);
});
onBeforeUnmount(() => {
chartInstance.value?.dispose();
});
defineExpose({
getInstance: () => chartInstance.value,
resize,
draw,
});
</script>

View File

@@ -0,0 +1,67 @@
import * as echarts from 'echarts/core';
import { BarChart, LineChart, LinesChart, PieChart, ScatterChart, RadarChart, GaugeChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
PolarComponent,
GeoComponent,
ToolboxComponent,
DataZoomComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import type {
BarSeriesOption,
LineSeriesOption,
LinesSeriesOption,
PieSeriesOption,
ScatterSeriesOption,
RadarSeriesOption,
GaugeSeriesOption,
} from 'echarts/charts';
import type { TitleComponentOption, TooltipComponentOption, GridComponentOption, DatasetComponentOption } from 'echarts/components';
import type { ComposeOption } from 'echarts/core';
// import 'echarts-liquidfill';
export type ECOption = ComposeOption<
| BarSeriesOption
| LineSeriesOption
| LinesSeriesOption
| PieSeriesOption
| RadarSeriesOption
| GaugeSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
| ScatterSeriesOption
>;
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
PolarComponent,
GeoComponent,
ToolboxComponent,
DataZoomComponent,
BarChart,
LineChart,
LinesChart,
PieChart,
ScatterChart,
RadarChart,
GaugeChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
]);
export default echarts;

View File

@@ -0,0 +1,151 @@
const light = {
seriesCnt: '4',
backgroundColor: 'rgba(0,0,0,0)',
titleColor: '#008acd',
subtitleColor: '#aaaaaa',
textColorShow: false,
textColor: '#333',
markTextColor: '#eeeeee',
color: [
'#2ec7c9',
'#b6a2de',
'#5ab1ef',
'#ffb980',
'#d87a80',
'#8d98b3',
'#e5cf0d',
'#97b552',
'#95706d',
'#dc69aa',
'#07a2a4',
'#9a7fd1',
'#588dd5',
'#f5994e',
'#c05050',
'#59678c',
'#c9ab00',
'#7eb00a',
'#6f5553',
'#c14089',
],
borderColor: '#ccc',
borderWidth: 0,
visualMapColor: ['#5ab1ef', '#e0ffff'],
legendTextColor: '#333333',
kColor: '#d87a80',
kColor0: '#2ec7c9',
kBorderColor: '#d87a80',
kBorderColor0: '#2ec7c9',
kBorderWidth: 1,
lineWidth: 2,
symbolSize: 3,
symbol: 'emptyCircle',
symbolBorderWidth: 1,
lineSmooth: true,
graphLineWidth: 1,
graphLineColor: '#aaaaaa',
mapLabelColor: '#d87a80',
mapLabelColorE: 'rgb(100,0,0)',
mapBorderColor: '#eeeeee',
mapBorderColorE: '#444',
mapBorderWidth: 0.5,
mapBorderWidthE: 1,
mapAreaColor: '#dddddd',
mapAreaColorE: 'rgba(254,153,78,1)',
axes: [
{
type: 'all',
name: '通用坐标轴',
axisLineShow: true,
axisLineColor: '#eeeeee',
axisTickShow: true,
axisTickColor: '#eeeeee',
axisLabelShow: true,
axisLabelColor: '#eeeeee',
splitLineShow: true,
splitLineColor: ['#aaaaaa'],
splitAreaShow: false,
splitAreaColor: ['#eeeeee'],
},
{
type: 'category',
name: '类目坐标轴',
axisLineShow: true,
axisLineColor: '#008acd',
axisTickShow: true,
axisTickColor: '#333',
axisLabelShow: true,
axisLabelColor: '#333',
splitLineShow: false,
splitLineColor: ['#eee'],
splitAreaShow: false,
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
},
{
type: 'value',
name: '数值坐标轴',
axisLineShow: true,
axisLineColor: '#008acd',
axisTickShow: true,
axisTickColor: '#333',
axisLabelShow: true,
axisLabelColor: '#333',
splitLineShow: true,
splitLineColor: ['#eee'],
splitAreaShow: true,
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
},
{
type: 'log',
name: '对数坐标轴',
axisLineShow: true,
axisLineColor: '#008acd',
axisTickShow: true,
axisTickColor: '#333',
axisLabelShow: true,
axisLabelColor: '#333',
splitLineShow: true,
splitLineColor: ['#eee'],
splitAreaShow: true,
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
},
{
type: 'time',
name: '时间坐标轴',
axisLineShow: true,
axisLineColor: '#008acd',
axisTickShow: true,
axisTickColor: '#333',
axisLabelShow: true,
axisLabelColor: '#333',
splitLineShow: true,
splitLineColor: ['#eee'],
splitAreaShow: false,
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
},
],
axisSeperateSetting: true,
toolboxColor: '#2ec7c9',
toolboxEmphasisColor: '#18a4a6',
tooltipAxisColor: '#008acd',
tooltipAxisWidth: '1',
timelineLineColor: '#008acd',
timelineLineWidth: 1,
timelineItemColor: '#008acd',
timelineItemColorE: '#a9334c',
timelineCheckColor: '#2ec7c9',
timelineCheckBorderColor: '#2ec7c9',
timelineItemBorderWidth: 1,
timelineControlColor: '#008acd',
timelineControlBorderColor: '#008acd',
timelineControlBorderWidth: 0.5,
timelineLabelColor: '#008acd',
datazoomBackgroundColor: 'rgba(47,69,84,0)',
datazoomDataColor: '#efefff',
datazoomFillColor: 'rgba(182,162,222,0.2)',
datazoomHandleColor: '#008acd',
datazoomHandleWidth: '100',
datazoomLabelColor: '#333333',
};
export { light };

View File

@@ -1,7 +1,7 @@
<template>
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5)">
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select>
</div>
@@ -9,7 +9,6 @@
<script lang="ts" setup>
import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
// import * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
@@ -133,7 +132,7 @@ const languageArr = [
},
];
const options = {
const defaultOptions = {
language: 'shell',
theme: 'SolarizedLight',
automaticLayout: true, //自适应宽高布局
@@ -155,6 +154,20 @@ const options = {
},
};
const monacoTextarea: any = ref();
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
const state = reactive({
languageMode: 'shell',
});
@@ -173,6 +186,7 @@ onBeforeUnmount(() => {
monacoEditorIns.dispose();
}
if (completionItemProvider) {
console.log('unmount=> dispose completion item provider');
completionItemProvider.dispose();
}
});
@@ -203,28 +217,14 @@ watch(
}
);
const monacoTextarea: any = ref(null);
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器');
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
options.language = state.languageMode;
options.theme = themeConfig.value.editorTheme;
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(options, props.options as any));
defaultOptions.language = state.languageMode;
defaultOptions.theme = themeConfig.value.editorTheme;
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(defaultOptions, props.options as any));
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
@@ -260,6 +260,7 @@ const setEditorValue = (value: any) => {
*/
const registerCompletionItemProvider = () => {
if (completionItemProvider) {
console.log('exist competion item provider, dispose now');
completionItemProvider.dispose();
}
if (state.languageMode == 'shell') {
@@ -293,13 +294,19 @@ const registeShell = () => {
};
const format = () => {
/*
触发自动格式化;
*/
// 触发自动格式化;
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
};
defineExpose({ format });
const focus = () => {
monacoEditorIns.focus();
};
const getEditor = () => {
return monacoEditorIns;
};
defineExpose({ getEditor, format, focus });
</script>
<style lang="scss">

View File

@@ -0,0 +1,37 @@
import { VNode, h, render } from 'vue';
import MonacoEditorDialogComp from './MonacoEditorDialogComp.vue';
export type MonacoEditorDialogProps = {
content: string;
title: string;
language: string;
height?: string;
width?: string;
confirmFn?: Function; // 点击确认的回调函数入参editor value
cancelFn?: Function; // 点击取消 或 关闭弹窗的回调函数
};
const boxId = 'monaco-editor-dialog-id';
let boxInstance: VNode;
const MonacoEditorDialog = (props: MonacoEditorDialogProps): void => {
if (!boxInstance) {
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
boxInstance = h(MonacoEditorDialogComp);
// 将虚拟dom渲染到 container dom 上
render(boxInstance, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container);
}
const boxVue = boxInstance.component;
if (boxVue) {
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed?.open(props);
}
};
export default MonacoEditorDialog;

View File

@@ -0,0 +1,136 @@
<template>
<div>
<el-dialog :title="state.title" v-model="state.dialogVisible" :width="state.width" @close="cancel">
<monaco-editor ref="editorRef" :height="state.height" class="editor" :language="state.language" v-model="contentValue" can-change-mode />
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button @click="confirm" type="primary">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, ref, reactive } from 'vue';
import { ElDialog, ElButton, ElMessage } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { MonacoEditorDialogProps } from './MonacoEditorDialog';
const editorRef: any = ref(null);
const state = reactive({
dialogVisible: false,
height: '450px',
width: '800px',
contentValue: '',
title: '',
language: '',
});
let confirmFn: any;
let cancelFn: any;
const { contentValue } = toRefs(state);
function compressHTML(html: string) {
return (
html
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
// .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
.replace(/>\s+</g, '><')
); // 移除标签之间的空格
}
/**
* 确认按钮
*/
const confirm = async () => {
if (confirmFn) {
if (state.language === 'json') {
let val;
try {
val = JSON.parse(contentValue.value);
if (typeof val !== 'object') {
ElMessage.error('请输入正确的json');
return;
}
} catch (e) {
ElMessage.error('请输入正确的json');
return;
}
// 压缩json字符串
confirmFn(JSON.stringify(val));
} else if (state.language === 'html') {
// 压缩html字符串
confirmFn(compressHTML(contentValue.value));
} else {
confirmFn(contentValue.value);
}
}
state.dialogVisible = false;
setTimeout(() => {
state.contentValue = '';
state.title = '';
}, 200);
};
const cancel = () => {
state.dialogVisible = false;
// 没有执行成功,并且取消回调函数存在,则执行
cancelFn && cancelFn();
setTimeout(() => {
state.contentValue = '';
state.title = '';
}, 200);
};
const formatXML = function (xml: string, tab?: string) {
let formatted = '',
indent = '';
tab = tab || ' ';
xml.split(/>\s*</).forEach(function (node) {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += indent + '<' + node + '>\r\n';
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
};
const open = (optionProps: MonacoEditorDialogProps) => {
confirmFn = optionProps.confirmFn;
cancelFn = optionProps.cancelFn;
const language = optionProps.language;
state.language = language;
state.title = optionProps.title;
if (optionProps.height) {
state.height = optionProps.height;
}
state.contentValue = optionProps.content;
// 格式化输出html;
if (language === 'html' || language == 'xml') {
state.contentValue = formatXML(optionProps.content);
}
setTimeout(() => {
editorRef.value?.format();
editorRef.value?.focus();
}, 300);
state.dialogVisible = true;
};
defineExpose({ open });
</script>
<style lang="scss" scoped>
.editor {
font-size: 9pt;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,25 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
/**
* key: language, value: CompletionItemProvider
*/
const completionItemProviders: Map<string, any> = new Map();
export function registerCompletionItemProvider(language: string, completionItemProvider: any, replace: boolean = true) {
const exist = completionItemProviders.get(language);
if (exist) {
if (!replace) {
return;
}
exist.dispose();
}
completionItemProviders.set(language, monaco.languages.registerCompletionItemProvider(language, completionItemProvider));
}
export function dispposeCompletionItemProvider(language: string) {
const exist = completionItemProviders.get(language);
if (exist) {
exist.dispose();
completionItemProviders.delete(language);
}
}

View File

@@ -1,437 +1,460 @@
<template>
<div class="page-table">
<!--
实现通过我们配置好的 查询条件
首先去创建form表单根据我们配置的查询条件去做一个循环判断展示出不用类型所对应不同的输入框
比如text对应普通的输入框select对应下拉选择dateTime对应日期时间选择器
在使用时父组件会传来一个queryForm空的对象
循环出来的输入框会绑定表格配置中的prop字段绑定在queryForm对象中
-->
<el-card>
<div class="query" ref="queryRef">
<div>
<div v-if="props.query.length > 0">
<el-form :model="props.queryForm" label-width="auto" :size="props.size">
<el-row
v-for="i in Math.ceil((props.query.length + 1) / (defaultQueryCount + 1))"
:key="i"
v-show="i == 1 || isOpenMoreQuery"
:class="i > 1 && isOpenMoreQuery ? 'is-open' : ''"
>
<el-form-item
:label="item.label"
style="margin-right: 12px; margin-bottom: 0px"
v-for="item in getRowQueryItem(i)"
:key="item.prop"
>
<!-- 这里只获取指定个数的筛选条件 -->
<el-input
v-model="queryForm[item.prop]"
:placeholder="'输入' + item.label + '关键字'"
clearable
v-if="item.type == 'text'"
></el-input>
<div>
<transition name="el-zoom-in-top">
<!-- 查询表单 -->
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
<!-- 遍历父组件传入的 solts 透传给子组件 -->
<template v-for="(_, key) in useSlots()" v-slot:[key]>
<slot :name="key"></slot>
</template>
</SearchForm>
</transition>
<el-select-v2
v-model="queryForm[item.prop]"
:options="item.options"
clearable
:placeholder="'选择' + item.label + '关键字'"
v-else-if="item.type == 'select'"
/>
<div class="card">
<div class="table-main">
<!-- 表格头部 操作按钮 -->
<div class="table-header">
<div class="header-button-lf">
<slot name="tableHeader" />
</div>
<el-date-picker
v-model="queryForm[item.prop]"
clearable
type="datetimerange"
format="YYYY-MM-DD hh:mm:ss"
value-format="x"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
v-else-if="item.type == 'date'"
/>
<template v-else-if="item.slot == 'queryBtns'">
<template v-if="props.query?.length > defaultQueryCount">
<el-button
@click="isOpenMoreQuery = !isOpenMoreQuery"
v-if="!isOpenMoreQuery"
icon="ArrowDownBold"
circle
></el-button>
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-else icon="ArrowUpBold" circle></el-button>
<div v-if="toolButton" class="header-button-ri">
<slot name="toolButton">
<div class="tool-button">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="simple-search-form">
<el-dropdown v-if="searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="searchItem in searchItems"
:key="searchItem.prop"
@click="changeSimpleFormItem(searchItem)"
>
{{ searchItem.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button @click="queryData()" type="primary" icon="search" plain>查询</el-button>
<el-button @click="reset()" icon="RefreshRight">重置</el-button>
</template>
<div class="simple-search-form-label mt5">
<el-text truncated tag="b">{{ `${nowSearchItem?.label} : ` }}</el-text>
</div>
<slot :name="item.slot"></slot>
</el-form-item>
</el-row>
</el-form>
<el-form-item style="width: 200px" :key="nowSearchItem.prop">
<SearchFormItem
@keyup.enter.native="searchFormItemKeyUpEnter"
v-if="!nowSearchItem.slot"
:item="nowSearchItem"
v-model="queryForm[nowSearchItem.prop]"
/>
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else :name="nowSearchItem.slot"></slot>
</el-form-item>
</div>
<div>
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search" circle @click="search" />
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"
/>
<el-popover
placement="bottom"
title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
width="auto"
trigger="click"
>
<div v-for="(item, index) in tableColumns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
</div>
<template #reference>
<el-button icon="Operation" circle :size="props.size"></el-button>
</template>
</el-popover>
</div>
</div>
</slot>
</div>
</div>
<div class="slot">
<!-- 查询栏右侧slot插槽用来添加表格其他操作比如新增数据删除数据等其他操作 -->
<slot name="queryRight"></slot>
<el-table
ref="tableRef"
v-bind="$attrs"
:max-height="tableMaxHeight"
@selection-change="handleSelectionChange"
:data="tableData"
highlight-current-row
v-loading="loading"
:size="props.size as any"
:border="border"
>
<el-table-column v-if="props.showSelection" :selectable="selectable" type="selection" width="40" />
<!--
动态表头显示根据表格每条配置项中的show字段来决定改列是否显示或者隐藏
columns 就是我们表格配置的数组对象
-->
<el-popover
placement="bottom"
title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
width="auto"
trigger="click"
>
<div v-for="(item, index) in props.columns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
</div>
<template #reference>
<!-- 一个Element Plus中的图标 -->
<el-button icon="Operation" :size="props.size"></el-button>
</template>
</el-popover>
</div>
<template v-for="(item, index) in tableColumns">
<el-table-column
:key="index"
v-if="item.show"
:prop="item.prop"
:label="item.label"
:fixed="item.fixed"
:align="item.align"
:show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth"
:sortable="item.sortable || false"
:type="item.type"
:width="item.width"
>
<!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot">
<slot :name="item.prop" :data="scope.row"></slot>
</template>
<!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
effect="light"
trigger="click"
placement="top"
width="600px"
>
<template #default>
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
</template>
<template #reference>
<el-link
@click="formatText(scope.row[item.prop])"
:underline="false"
type="success"
icon="MagicStick"
class="mr5"
></el-link>
</template>
</el-popover>
<span>{{ item.getValueByData(scope.row) }}</span>
</template>
</el-table-column>
</template>
</el-table>
</div>
<el-table
v-bind="$attrs"
:max-height="tableMaxHeight"
@selection-change="handleSelectionChange"
:data="props.data"
highlight-current-row
v-loading="loadingData"
:size="props.size"
>
<el-table-column v-if="props.showSelection" type="selection" width="40" />
<template v-for="(item, index) in columns">
<el-table-column
:key="index"
v-if="item.show"
:prop="item.prop"
:label="item.label"
:fixed="item.fixed"
:align="item.align"
:show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth"
:sortable="item.sortable || false"
:type="item.type"
:width="item.width"
>
<!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot">
<slot :name="item.prop" :data="scope.row"></slot>
</template>
<!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
effect="light"
trigger="click"
placement="top"
width="600px"
>
<template #default>
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
</template>
<template #reference>
<el-link
@click="formatText(scope.row[item.prop])"
:underline="false"
type="success"
icon="MagicStick"
class="mr5"
></el-link>
</template>
</el-popover>
<span>{{ item.getValueByData(scope.row) }}</span>
</template>
</el-table-column>
</template>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
<el-pagination
:small="props.size == 'small'"
@current-change="handlePageChange"
@size-change="handleSizeChange"
@current-change="handlePageNumChange"
@size-change="handlePageSizeChange"
style="text-align: right"
layout="prev, pager, next, total, sizes, jumper"
:total="props.total"
v-model:current-page="state.pageNum"
v-model:page-size="state.pageSize"
:total="total"
v-model:current-page="queryForm.pageNum"
v-model:page-size="queryForm.pageSize"
:page-sizes="pageSizes"
/>
</el-row>
</el-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue';
import { TableColumn, TableQuery } from './index';
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
import { TableColumn } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import { useEventListener } from '@vueuse/core';
import Api from '@/common/Api';
import SearchForm from '@/components/SearchForm/index.vue';
import { SearchItem } from '../SearchForm/index';
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { usePageTable } from '@/hooks/usePageTable';
import { ElTable } from 'element-plus';
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChange']);
const props = defineProps({
size: {
type: String,
default: '',
export interface PageTableProps {
size?: string;
pageApi: Api; // 请求表格数据的 api
columns: TableColumn[]; // 列配置项 ==> 必传
showSelection?: boolean;
selectable?: (row: any) => boolean; // 是否可选
pageable?: boolean;
showSearch?: boolean; // 是否显示搜索表单
data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
lazy?: boolean; // 是否自动执行请求 api ==> 非必传默认为false
beforeQueryFn?: (params: any) => any; // 执行查询时对查询参数进行处理,调整等
dataHandlerFn?: (data: any) => any; // 数据处理回调函数,用于将请求回来的数据二次加工处理等
searchItems?: SearchItem[];
border?: boolean; // 是否带有纵向边框 ==> 非必传默认为false
toolButton?: ('setting' | 'search')[] | boolean; // 是否显示表格功能按钮 ==> 非必传默认为true
searchCol?: any; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | number 如 3
}
// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<PageTableProps>(), {
columns: () => [],
pageable: true,
showSelection: false,
lazy: false,
border: false,
toolButton: true,
showSearch: false,
searchItems: () => [],
searchCol: () => ({ xs: 1, sm: 3, md: 3, lg: 4, xl: 5 }),
});
// 查询表单参数 ==> 非必传(默认为{pageNum:1, pageSize: 10}
const queryForm: Ref<any> = defineModel('queryForm', {
default: {
pageNum: 1,
pageSize: 0,
},
inputWidth: {
type: [Number, String],
default: 0,
},
// 是否显示选择列
showSelection: {
type: Boolean,
default: false,
},
// 当前选择的数据
selectionData: {
type: Array<any>
},
// 列信息
columns: {
type: Array<TableColumn>,
default: function () {
return [];
},
required: true,
},
// 表格数据
data: {
type: Array,
required: true,
},
total: {
type: [Number],
default: 0,
},
pageNum: {
type: Number,
default: 1,
},
pageSize: {
type: [Number],
default: 10,
},
// 查询条件配置
query: {
type: Array<TableQuery>,
default: function () {
return [];
}
},
// 绑定的查询表单
queryForm: {
type: Object,
default: function () {
return {};
}
},
})
});
// table 实例
const tableRef = ref<InstanceType<typeof ElTable>>();
// 接收 columns 并设置为响应式
const tableColumns = reactive<TableColumn[]>(props.columns);
// 接收 searchItems 并设置为响应式
const tableSearchItems = reactive<SearchItem[]>(props.searchItems);
const { themeConfig } = storeToRefs(useThemeConfig());
// 是否显示搜索模块
const isShowSearch = ref(props.showSearch);
// 控制 ToolButton 显示
const showToolButton = (key: 'setting' | 'search') => {
return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
};
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
/**
* 改变当前的搜索项
* @param searchItem 当前点击的搜索项
*/
const changeSimpleFormItem = (searchItem: SearchItem) => {
// 将之前的值置为空,避免因为只显示一个搜索项却搜索多个条件
queryForm.value[nowSearchItem.value.prop] = null;
nowSearchItem.value = searchItem;
};
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
props.pageable,
props.pageApi,
queryForm,
props.beforeQueryFn,
props.dataHandlerFn
);
const state = reactive({
pageSizes: [] as any, // 可选每页显示的数据量
pageSize: 10,
pageNum: 1,
isOpenMoreQuery: false,
defaultQueryCount: 2, // 默认显示的查询参数个数展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
queryForm: {} as any,
loadingData: false,
// 输入框宽度
inputWidth: "200px" as any,
formatVal: '', // 格式化后的值
tableMaxHeight: window.innerHeight - 240 + 'px',
})
tableMaxHeight: '500px',
});
const {
pageSizes,
isOpenMoreQuery,
defaultQueryCount,
queryForm,
loadingData,
inputWidth,
formatVal,
tableMaxHeight,
} = toRefs(state)
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
watch(() => props.queryForm, (newValue: any) => {
state.queryForm = newValue;
})
watch(() => props.pageNum, (newValue: any) => {
state.pageNum = newValue;
})
watch(() => props.pageSize, (newValue: any) => {
state.pageSize = newValue;
})
watch(() => props.data, (newValue: any) => {
watch(tableData, (newValue: any) => {
if (newValue && newValue.length > 0) {
props.columns.forEach(item => {
props.columns.forEach((item) => {
if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(props.data);
item.autoCalculateMinWidth(tableData.value);
}
})
});
}
})
});
onMounted(() => {
const pageSize = props.pageSize;
watch(isShowSearch, () => {
calcuTableHeight();
});
state.pageNum = props.pageNum;
state.pageSize = pageSize;
state.queryForm = props.queryForm;
onMounted(async () => {
calcuTableHeight();
useEventListener(window, 'resize', calcuTableHeight);
if (props.searchItems.length > 0) {
nowSearchItem.value = props.searchItems[0];
}
let pageSize = queryForm.value.pageSize;
// 如果pageSize设为0则使用系统全局配置的pageSize
if (!pageSize) {
pageSize = themeConfig.value.defaultListPageSize;
// 可能storage已经存在配置json则可能没值需要清storage重试
if (!pageSize) {
pageSize = 10;
}
}
queryForm.value.pageNum = 1;
queryForm.value.pageSize = pageSize;
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
// 如果没传输入框宽度则根据组件size设置默认宽度
if (!props.inputWidth) {
state.inputWidth = props.size == 'small' ? '150px' : '200px';
} else {
state.inputWidth = props.inputWidth;
if (!props.lazy) {
await getTableData();
}
window.addEventListener('resize', () => {
calcuTableHeight();
});
})
});
const calcuTableHeight = () => {
state.tableMaxHeight = window.innerHeight - 240 + 'px';
}
const headerHeight = isShowSearch.value ? 330 : 250;
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
};
const formatText = (data: any)=> {
const searchFormItemKeyUpEnter = (event: any) => {
event.preventDefault();
search();
};
const formatText = (data: any) => {
state.formatVal = '';
try {
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) {
} catch (e) {
state.formatVal = data;
}
}
const getRowQueryItem = (row: number) => {
// 第一行需要加个查询等按钮列
if (row === 1) {
const res = props.query.slice(row - 1, defaultQueryCount.value);
// 查询等按钮列
res.push(TableQuery.slot("", "", "queryBtns"));
return res
}
const columnCount = defaultQueryCount.value + 1;
return props.query.slice((row - 1) * columnCount - 1, row * columnCount - 1);
}
};
const handleSelectionChange = (val: any) => {
emit('update:selectionData', val);
}
};
const handlePageChange = () => {
emit('update:pageNum', state.pageNum);
execQuery();
}
const getData = () => {
return toValue(tableData);
};
const handleSizeChange = () => {
changePageNum(1);
emit('update:pageSize', state.pageSize);
execQuery();
}
const queryData = () => {
changePageNum(1);
execQuery();
}
const reset = () => {
// 将查询参数绑定的值置空,并重新粗发查询接口
for (let qi of props.query) {
state.queryForm[qi.prop] = null;
}
changePageNum(1);
emit('update:queryForm', state.queryForm);
execQuery();
}
const changePageNum = (pageNum: number) => {
state.pageNum = pageNum;
emit('update:pageNum', state.pageNum);
}
const execQuery = () => {
emit('pageChange');
}
/**
* 是否正在加载数据
*/
const loading = (loading: boolean) => {
state.loadingData = loading;
}
defineExpose({ loading })
defineExpose({
tableRef: tableRef,
search: getTableData,
getData,
});
</script>
<style scoped lang="scss">
.page-table {
.query {
margin-bottom: 10px;
overflow: hidden;
.table-box,
.table-main {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
.is-open {
// padding: 10px 0;
max-height: 200px;
margin-top: 10px;
// 表格 header 样式
.table-header {
width: 100%;
.header-button-lf {
float: left;
}
display: flex;
align-items: flex-start;
justify-content: space-between;
.header-button-ri {
float: right;
.slot {
display: flex;
justify-content: flex-end;
.tool-button {
display: flex;
justify-content: space-between;
}
.simple-search-form {
margin-right: 10px;
display: flex;
justify-content: space-between;
::v-deep(.el-form-item__content > *) {
width: 100% !important;
}
.simple-search-form-label {
text-align: right;
margin-right: 5px;
}
.simple-search-form-btn:hover {
color: var(--el-color-primary);
}
}
}
.el-button {
margin-bottom: 10px;
}
}
.page {
margin-top: 10px;
// el-table 表格样式
.el-table {
flex: 1;
// 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
table {
width: 100%;
}
// .el-table__header th {
// height: 45px;
// font-size: 15px;
// font-weight: bold;
// color: var(--el-text-color-primary);
// background: var(--el-fill-color-light);
// }
// .el-table__row {
// height: 45px;
// font-size: 14px;
// .move {
// cursor: move;
// .el-icon {
// cursor: move;
// }
// }
// }
// 设置 el-table 中 header 文字不换行,并省略
.el-table__header .el-table__cell > .cell {
// white-space: nowrap;
white-space: wrap;
}
// 解决表格数据为空时样式不居中问题(仅在element-plus中)
// .el-table__empty-block {
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
// .table-empty {
// line-height: 30px;
// }
// }
// table 中 image 图片样式
.table-image {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
}
::v-deep(.el-form-item__label) {
font-weight: bold;
}
.el-select-v2 {
width: v-bind(inputWidth);
}
.el-input {
width: v-bind(inputWidth);
}
.el-select {
width: v-bind(inputWidth);
}
.el-date-editor {
width: 380px !important;
::v-deep(.el-form-item__label) {
font-weight: bold;
}
}
</style>

View File

@@ -121,7 +121,7 @@ export class TableColumn {
/**
* 使用标签类型展示该列(用于枚举值友好展示)
* @param param 枚举对象
* @param param 枚举对象, 如AccountStatusEnum
* @returns this
*/
typeTag(param: any): TableColumn {
@@ -242,68 +242,3 @@ export class TableColumn {
this.minWidth = (flexWidth > 400 ? 400 : flexWidth) + this.addWidth;
};
}
export class TableQuery {
/**
* 属性字段
*/
prop: string;
/**
* 显示表头
*/
label: string;
/**
* 查询类型text、select、date
*/
type: string;
/**
* select可选值
*/
options: any;
/**
* 插槽名
*/
slot: string;
constructor(prop: string, label: string) {
this.prop = prop;
this.label = label;
}
static new(prop: string, label: string): TableQuery {
return new TableQuery(prop, label);
}
static text(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'text';
return tq;
}
static select(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'select';
return tq;
}
static date(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'date';
return tq;
}
static slot(prop: string, label: string, slotName: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.slot = slotName;
return tq;
}
setOptions(options: any): TableQuery {
this.options = options;
return this;
}
}

View File

@@ -0,0 +1,12 @@
export const buildProgressProps = (): any => {
return {
progress: {
title: {
type: String,
},
executedStatements: {
type: Number,
},
},
};
};

View File

@@ -0,0 +1,34 @@
<template>
<el-descriptions border size="small" :title="`${progress.title}`">
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
</el-descriptions>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive } from 'vue';
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
import { buildProgressProps } from './progress-notify';
const props = defineProps(buildProgressProps());
const state = reactive({
elapsedTime: '00:00:00',
});
let timer: any = undefined;
const startTime = Date.now();
onMounted(async () => {
timer = setInterval(() => {
const elapsed = Date.now() - startTime;
state.elapsedTime = formatTime(elapsed, 'HH:mm:ss');
}, 1000);
});
onUnmounted(async () => {
if (timer != undefined) {
clearInterval(timer); // 在Vue实例销毁前清除我们的定时器
timer = undefined;
}
});
</script>

View File

@@ -19,6 +19,7 @@ import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue'
import TerminalSearch from './TerminalSearch.vue';
import { debounce } from 'lodash';
import { TerminalStatus } from './common';
import { useEventListener } from '@vueuse/core';
const props = defineProps({
/**
@@ -144,10 +145,8 @@ const onConnected = () => {
state.status = TerminalStatus.Connected;
// resize
sendResize(term.cols, term.rows);
// 注册窗口大小监听器
window.addEventListener('resize', debounce(fitTerminal, 400));
useEventListener('resize', debounce(fitTerminal, 400));
focus();
@@ -180,7 +179,8 @@ const clear = () => {
function initSocket() {
if (props.socketUrl) {
socket = new WebSocket(props.socketUrl);
let socketUrl = `${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`;
socket = new WebSocket(socketUrl);
}
// 监听socket连接
@@ -197,8 +197,6 @@ function initSocket() {
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);
// 关闭窗口大小监听器
window.removeEventListener('resize', debounce(fitTerminal, 100));
// 清除 ping
pingInterval && clearInterval(pingInterval);
state.status = TerminalStatus.Disconnected;

View File

@@ -52,13 +52,13 @@
title="最小化"
/>
<!-- <SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" /> -->
<SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" />
<SvgIcon name="Close" class="pointer-icon" @click="close(openTerminal.terminalId)" title="关闭" :size="20" />
</div>
</div>
</template>
<div class="terminal-wrapper" style="height: calc(100vh - 215px)">
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
<TerminalBody
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
@@ -78,7 +78,7 @@
size="small"
@click="maximize(minimizeTerminal.terminalId)"
>
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
<el-tooltip :content="minimizeTerminal.desc" placement="top">
<span>
{{ minimizeTerminal.title }}
</span>
@@ -230,6 +230,16 @@ function maximize(terminalId: any) {
}, 250);
}
const handlerFullScreen = (terminal: any) => {
terminal.fullscreen = !terminal.fullscreen;
const terminalRef = openTerminalRefs[terminal.terminalId];
// fit
setTimeout(() => {
terminalRef?.fitTerminal();
terminalRef?.focus();
}, 250);
};
const closeMinimizeTerminal = (terminalId: any) => {
delete state.minimizeTerminals[terminalId];
close(terminalId);
@@ -249,9 +259,11 @@ defineExpose({
padding: 10px;
}
// .terminal-dialog {
// height: calc(100vh - 200px) !important;
// }
// 取消body最大高度否则全屏有问题
.el-dialog__body {
max-height: 100% !important;
overflow: hidden !important;
}
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
padding: 0px !important;

View File

@@ -0,0 +1,126 @@
import Api from '@/common/Api';
import { isReactive, reactive, toRefs, toValue } from 'vue';
/**
* @description table 页面操作方法封装
* @param pageable 是否为分页获取
* @param {Api} api 获取表格数据 api (必传)
* @param {Object} param 获取数据请求参数 (非必传,默认为{pageNum: 1, pageSize: 10})
* @param {Function} dataCallBack 对api请求返回的数据进行处理的回调方法 (非必传)
* */
export const usePageTable = (
pageable: boolean = true,
api?: Api,
params: any = {
// 当前页数
pageNum: 1,
// 每页显示条数
pageSize: 10,
},
beforeQueryFn?: (params: any) => any,
dataCallBack?: (data: any) => any
) => {
const state = reactive({
// 表格数据
tableData: [],
// 总数量
total: 0,
// 查询参数,包含分页参数
searchParams: params,
loading: false,
});
/**
* @description 获取表格数据
* @return void
* */
const getTableData = async () => {
if (!api) return;
try {
state.loading = true;
let sp = toValue(state.searchParams);
if (beforeQueryFn) {
sp = beforeQueryFn(sp);
if (isReactive(state.searchParams)) {
state.searchParams.value = sp;
} else {
state.searchParams = sp;
}
}
let res = await api.request(sp);
dataCallBack && (res = await dataCallBack(res));
if (pageable) {
state.tableData = res.list;
state.total = res.total;
} else {
state.tableData = res;
}
} finally {
state.loading = false;
}
};
const setPageNum = (pageNum: number) => {
if (!pageable) {
return;
}
state.searchParams.pageNum = pageNum;
};
/**
* @description 表格数据查询pageNum = 1
* @return void
* */
const search = () => {
setPageNum(1);
getTableData();
};
/**
* @description 表格数据重置pageNum = 1,除分页相关参数外其他查询参数置为空
* @return void
* */
const reset = () => {
setPageNum(1);
for (let prop of Object.keys(state.searchParams)) {
if (prop == 'pageNum' || prop == 'pageSize') {
continue;
}
state.searchParams[prop] = null;
}
getTableData();
};
/**
* @description 每页条数改变
* @param {Number} val 当前条数
* @return void
* */
const handlePageSizeChange = (val: number) => {
setPageNum(1);
state.searchParams.pageSize = val;
getTableData();
};
/**
* @description 当前页改变
* @param {Number} val 当前页
* @return void
* */
const handlePageNumChange = (val: number) => {
state.searchParams.pageNum = val;
getTableData();
};
return {
...toRefs(state),
getTableData,
search,
reset,
handlePageSizeChange,
handlePageNumChange,
};
};

View File

@@ -0,0 +1,147 @@
import router from '@/router';
import { getClientId, getToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { ElMessage } from 'element-plus';
import { createFetch } from '@vueuse/core';
import Api from '@/common/Api';
import { Result, ResultEnum } from '@/common/request';
import config from '@/common/config';
import { unref } from 'vue';
import { URL_401 } from '@/router/staticRouter';
const baseUrl: string = config.baseApiUrl;
const useCustomFetch = createFetch({
baseUrl: baseUrl,
combination: 'chain',
options: {
immediate: false,
timeout: 60000,
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
async beforeFetch({ options }) {
const token = getToken();
const headers = new Headers(options.headers || {});
if (token) {
headers.set('Authorization', token);
headers.set('ClientId', getClientId());
}
headers.set('Content-Type', 'application/json');
options.headers = headers;
return { options };
},
async afterFetch(ctx) {
const result: Result = await ctx.response.json();
ctx.data = result;
return ctx;
},
},
});
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
const uaf = useCustomFetch<T>(api.url, {
beforeFetch({ url, options }) {
options.method = api.method;
if (!params) {
return;
}
let paramsValue = unref(params);
if (api.beforeHandler) {
paramsValue = api.beforeHandler(paramsValue);
}
let apiUrl = url;
// 简单判断该url是否是restful风格
if (apiUrl.indexOf('{') != -1) {
apiUrl = templateResolve(apiUrl, paramsValue);
}
if (paramsValue) {
const method = options.method?.toLowerCase();
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
options.body = JSON.stringify(paramsValue);
} else {
const searchParam = new URLSearchParams();
Object.keys(paramsValue).forEach((key) => {
const val = paramsValue[key];
if (val) {
searchParam.append(key, val);
}
});
apiUrl = `${apiUrl}?${searchParam.toString()}`;
}
}
return {
url: apiUrl,
options: {
...options,
...reqOptions,
},
};
},
});
return {
execute: async function () {
try {
await uaf.execute(true);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('请求接口不存在');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('服务器响应异常');
return rejectPromise;
}
console.error(e);
ElMessage.error('网络请求错误');
return rejectPromise;
}
const result: Result = uaf.data.value as any;
if (!result) {
ElMessage.error('网络请求失败');
return Promise.reject(result);
}
// 如果返回为成功结果则将结果的data赋值给响应式data
if (result.code === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
},
isFetching: uaf.isFetching,
data: uaf.data,
abort: uaf.abort,
};
}

View File

@@ -21,8 +21,8 @@ import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Logo from '@/views/layout/logo/index.vue';
import Vertical from '@/views/layout/navMenu/vertical.vue';
import Logo from '@/layout/logo/index.vue';
import Vertical from '@/layout/navMenu/vertical.vue';
import mittBus from '@/common/utils/mitt';
const { proxy } = getCurrentInstance() as any;
@@ -38,8 +38,7 @@ const state: any = reactive({
// /
const setCollapseWidth = computed(() => {
let { layout, isCollapse, menuBar } = themeConfig.value;
let asideBrColor =
menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
let asideBrColor = menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
if (layout === 'columns') {
// 1px
if (isCollapse) {

View File

@@ -6,7 +6,7 @@
<script lang="ts">
import { computed } from 'vue';
import NavBarsIndex from '@/views/layout/navBars/index.vue';
import NavBarsIndex from '@/layout/navBars/index.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutHeader',

View File

@@ -1,17 +1,22 @@
<template>
<el-main class="layout-main">
<el-scrollbar class="layout-scrollbar" ref="layoutScrollbarRef"
v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1"
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }">
<el-scrollbar ref="layoutScrollbarRef" v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1">
<LayoutParentView />
<Footer v-if="themeConfig.isFooter" />
</el-scrollbar>
<Link :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
<Iframes :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
<Link class="h100" :meta="state.currentRouteMeta" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
<Iframes
class="h100"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta" />
@getCurrentRouteMeta="onGetCurrentRouteMeta"
/>
</el-main>
<el-footer v-if="themeConfig.isFooter">
<Footer />
</el-footer>
</template>
<script setup lang="ts" name="layoutMain">
@@ -19,10 +24,10 @@ import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LayoutParentView from '@/views/layout/routerView/parent.vue';
import Footer from '@/views/layout/footer/index.vue';
import Link from '@/views/layout/routerView/link.vue';
import Iframes from '@/views/layout/routerView/iframes.vue';
import LayoutParentView from '@/layout/routerView/parent.vue';
import Footer from '@/layout/footer/index.vue';
import Link from '@/layout/routerView/link.vue';
import Iframes from '@/layout/routerView/iframes.vue';
const { proxy } = getCurrentInstance() as any;
const { themeConfig } = storeToRefs(useThemeConfig());
@@ -48,7 +53,7 @@ const initCurrentRouteMeta = (meta: object) => {
// main
const initHeaderHeight = () => {
let { isTagsview } = themeConfig.value;
if (isTagsview) return (state.headerHeight = `84px`);
if (isTagsview) return (state.headerHeight = `77px`);
else return (state.headerHeight = `50px`);
};
//
@@ -58,7 +63,7 @@ onBeforeMount(() => {
});
// themeConfig el-scrollbar
watch(themeConfig.value, (val) => {
state.headerHeight = val.isTagsview ? '84px' : '50px';
state.headerHeight = val.isTagsview ? '77px' : '50px';
if (val.isFixedHeaderChange !== val.isFixedHeader) {
if (!proxy.$refs.layoutScrollbarRef) return false;
proxy.$refs.layoutScrollbarRef.update();

View File

@@ -1,8 +1,7 @@
<template>
<div class="layout-footer mt15" v-show="isDelayFooter">
<div class="layout-footer flex-all-center" v-show="isDelayFooter">
<div class="layout-footer-warp">
<div>Made by mayfly with </div>
<div class="mt5">mayfly-go</div>
<div>Made by mayfly-go with </div>
</div>
</div>
</template>
@@ -32,13 +31,14 @@ export default {
<style scoped lang="scss">
.layout-footer {
width: 100%;
display: flex;
&-warp {
margin: auto;
color: #9e9e9e;
text-align: center;
animation: logoAnimation 0.3s ease-in-out;
height: 30px;
background-color: var(--el-bg-color);
border-top: 1px solid var(--el-border-color-light);
.layout-footer-warp {
font-size: 14px;
color: var(--el-text-color-secondary);
text-decoration: none;
letter-spacing: 0.5px;
}
}
</style>

View File

@@ -10,10 +10,10 @@ import { onBeforeMount, onUnmounted } from 'vue';
import { getLocal, setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Defaults from '@/views/layout/main/defaults.vue';
import Classic from '@/views/layout/main/classic.vue';
import Transverse from '@/views/layout/main/transverse.vue';
import Columns from '@/views/layout/main/columns.vue';
import Defaults from '@/layout/main/defaults.vue';
import Classic from '@/layout/main/classic.vue';
import Transverse from '@/layout/main/transverse.vue';
import Columns from '@/layout/main/columns.vue';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());

View File

@@ -213,7 +213,7 @@ onUnmounted(() => {
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background: url('@/assets/image/bg-login.png') no-repeat;
background: url('@/assets/image/login-bg-main.svg') no-repeat;
background-size: 100% 100%;
z-index: 9999991;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img src="@/assets/image/logo.svg" class="layout-logo-medium-img" />
<img :src="themeConfig.logoIcon" class="layout-logo-medium-img" />
<span>
{{ `${themeConfig.globalTitle}` }}
<sub
@@ -9,7 +9,7 @@
</span>
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img src="@/assets/image/logo.svg" class="layout-logo-size-img" />
<img :src="themeConfig.logoIcon" class="layout-logo-size-img" />
</div>
</template>

View File

@@ -15,10 +15,10 @@
<script lang="ts" setup name="layoutClassic">
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
const { themeConfig } = storeToRefs(useThemeConfig());
</script>

View File

@@ -15,23 +15,15 @@
</el-container>
</template>
<script lang="ts">
<script lang="ts" setup name="layoutColumns">
import { computed } from 'vue';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import ColumnsAside from '@/views/layout/component/columnsAside.vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import ColumnsAside from '@/layout/component/columnsAside.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutColumns',
components: { Aside, Header, Main, ColumnsAside },
setup() {
const isFixedHeader = computed(() => {
return useThemeConfig().themeConfig.isFixedHeader;
});
return {
isFixedHeader,
};
},
};
const isFixedHeader = computed(() => {
return useThemeConfig().themeConfig.isFixedHeader;
});
</script>

View File

@@ -0,0 +1,37 @@
<template>
<el-container class="layout-container">
<Aside />
<el-container class="flex-center layout-backtop">
<Header v-if="isFixedHeader" />
<el-scrollbar ref="layoutDefaultsScrollbarRef">
<Header v-if="!isFixedHeader" />
<Main />
</el-scrollbar>
</el-container>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script lang="ts" setup name="layoutDefaults">
import { computed, getCurrentInstance, watch } from 'vue';
import { useRoute } from 'vue-router';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import { useThemeConfig } from '@/store/themeConfig';
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const isFixedHeader = computed(() => {
return useThemeConfig().themeConfig.isFixedHeader;
});
// 监听路由的变化
watch(
() => route.path,
() => {
try {
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
} catch (e) {}
}
);
</script>

View File

@@ -6,11 +6,7 @@
</el-container>
</template>
<script lang="ts">
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
export default {
name: 'layoutTransverse',
components: { Header, Main },
};
<script lang="ts" setup name="layoutTransverse">
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
</script>

View File

@@ -14,10 +14,10 @@ import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Breadcrumb from '@/views/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/views/layout/navBars/breadcrumb/user.vue';
import Logo from '@/views/layout/logo/index.vue';
import Horizontal from '@/views/layout/navMenu/horizontal.vue';
import Breadcrumb from '@/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/layout/navBars/breadcrumb/user.vue';
import Logo from '@/layout/logo/index.vue';
import Horizontal from '@/layout/navMenu/horizontal.vue';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());

View File

@@ -61,6 +61,24 @@
</div>
</div>
<!-- 全局设置 -->
<el-divider content-position="left">全局设置</el-divider>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">分页size</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input-number
v-model="themeConfig.defaultListPageSize"
controls-position="right"
:min="10"
:max="50"
@change="setLocalThemeConfig"
size="small"
style="width: 90px"
>
</el-input-number>
</div>
</div>
<!-- 全局主题 -->
<el-divider content-position="left">全局主题</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
@@ -374,7 +392,7 @@
</div>
</div>
</div>
<div class="copy-config">
<!-- <div class="copy-config">
<el-alert title="点击下方按钮,复制布局配置去 /src/store/modules/themeConfig.ts中修改" type="warning" :closable="false"> </el-alert>
<el-button
size="small"
@@ -385,7 +403,7 @@
@click="onCopyConfigClick($event.target)"
>一键复制配置
</el-button>
</div>
</div> -->
</el-scrollbar>
</el-drawer>
</div>
@@ -578,7 +596,7 @@ const openDrawer = () => {
themeConfig.value.isDrawer = true;
nextTick(() => {
//
onCopyConfigClick(copyConfigBtnRef.value.$el);
onCopyConfigClick(copyConfigBtnRef.value?.$el);
});
};
// store

View File

@@ -2,8 +2,8 @@
<div class="layout-navbars-breadcrumb-user" :style="{ flex: layoutUserFlexNum }">
<div class="layout-navbars-breadcrumb-user-icon">
<el-switch
@change="switchDark(state.isDark)"
v-model="state.isDark"
@change="switchDark()"
v-model="isDark"
active-action-icon="Moon"
inactive-action-icon="Sunny"
style="--el-switch-off-color: #c4c9c4; --el-switch-on-color: #2c2c2c"
@@ -56,7 +56,7 @@
<crop />
</el-icon>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link" style="cursor: pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfo.name || userInfo.username }}
@@ -75,7 +75,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { ref, computed, reactive, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -83,23 +83,25 @@ import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig';
import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue';
import { clearSession } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi';
import { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
import { useDark, usePreferredDark } from '@vueuse/core';
const router = useRouter();
const searchRef = ref();
const state = reactive({
isDark: false,
isScreenfull: false,
isShowUserNewsPopover: false,
disabledI18n: 'zh-cn',
disabledSize: '',
});
const { userInfo } = storeToRefs(useUserInfo());
const { themeConfig } = storeToRefs(useThemeConfig());
const themeConfigStore = useThemeConfig();
const { themeConfig } = storeToRefs(themeConfigStore);
//
const layoutUserFlexNum = computed(() => {
@@ -163,17 +165,22 @@ const onHandleCommandClick = (path: string) => {
}
};
const switchDark = (isDark: boolean) => {
themeConfig.value.isDark = isDark;
setLocal('themeConfig', themeConfig.value);
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
const isDark = useDark();
const preDark = usePreferredDark();
watch(preDark, (newValue) => {
isDark.value = newValue;
switchDark();
});
const switchDark = () => {
themeConfig.value.isDark = isDark.value;
if (isDark.value) {
themeConfig.value.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
themeConfig.value.editorTheme = 'SolarizedLight';
themeConfig.value.editorTheme = 'vs';
}
saveThemeConfig(themeConfig.value);
};
// //
@@ -182,18 +189,18 @@ const onSearchClick = () => {
};
//
const onComponentSizeChange = (size: string) => {
removeLocal('themeConfig');
themeConfig.value.globalComponentSize = size;
setLocal('themeConfig', themeConfig.value);
// proxy.$ELEMENT.size = size;
initComponentSize();
window.location.reload();
};
// const onComponentSizeChange = (size: string) => {
// removeLocal('themeConfig');
// themeConfig.value.globalComponentSize = size;
// saveThemeConfig(themeConfig.value);
// // proxy.$ELEMENT.size = size;
// initComponentSize();
// window.location.reload();
// };
//
const initComponentSize = () => {
switch (getLocal('themeConfig').globalComponentSize) {
switch (getThemeConfig().globalComponentSize) {
case '':
state.disabledSize = '';
break;
@@ -211,12 +218,10 @@ const initComponentSize = () => {
//
onMounted(() => {
if (getLocal('themeConfig')) {
const isDark = themeConfig.value.isDark;
state.isDark = isDark;
switchDark(isDark);
const themeConfig = getThemeConfig();
if (themeConfig) {
initComponentSize();
isDark.value = themeConfig.isDark;
}
});
</script>

View File

@@ -8,8 +8,8 @@
<script lang="ts">
import { computed } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
import BreadcrumbIndex from '@/views/layout/navBars/breadcrumb/index.vue';
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
import BreadcrumbIndex from '@/layout/navBars/breadcrumb/index.vue';
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
export default {
name: 'layoutNavBars',
components: { BreadcrumbIndex, TagsView },

View File

@@ -3,7 +3,7 @@
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<li
v-for="(v, k) in state.tagsViewList"
v-for="(v, k) in tagsViews"
:key="k"
class="layout-navbars-tagsview-ul-li"
:data-name="v.name"
@@ -17,8 +17,8 @@
"
>
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.meta.title }}</span>
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.title }}</span>
<template v-if="isActive(v)">
<SvgIcon
name="RefreshRight"
@@ -28,7 +28,7 @@
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
v-if="!v.meta.isAffix"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
@@ -36,42 +36,75 @@
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.meta.isAffix"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
</ul>
</el-scrollbar>
<Contextmenu :dropdown="state.dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
<Contextmenu :items="state.contextmenu.items" :dropdown="state.contextmenu.dropdown" ref="contextmenuRef" />
</div>
</template>
<script lang="ts" setup name="layoutTagsView">
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance, watch } from 'vue';
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getSession, setSession, removeSession } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
import Sortable from 'sortablejs';
import Contextmenu from '@/views/layout/navBars/tagsView/contextmenu.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
import { useTagsViews } from '@/store/tagsViews';
import { useKeepALiveNames } from '@/store/keepAliveNames';
const { proxy } = getCurrentInstance() as any;
const tagsRefs = ref([]) as any;
const scrollbarRef = ref();
const contextmenuRef = ref();
const tagsUlRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig());
const { tagsViews } = storeToRefs(useTagsViews());
const keepAliveNamesStores = useKeepALiveNames();
const route = useRoute();
const router = useRouter();
const contextmenuItems = [
new ContextmenuItem(0, '刷新').withIcon('RefreshRight').withOnClick((data: any) => {
// pathfullPath
let { path } = data;
let currentTag = tagsViews.value.find((v: any) => v.path === path);
refreshCurrentTagsView(path);
router.push({ path, query: currentTag?.query });
}),
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => closeCurrentTagsView(data.path)),
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
let { path } = data;
let currentTag = tagsViews.value.find((v: any) => v.path === path);
router.push({ path, query: currentTag?.query });
closeOtherTagsView(path);
}),
new ContextmenuItem(3, '关闭所有').withIcon('FolderDelete').withOnClick((data: any) => closeAllTagsView(data.path)),
new ContextmenuItem(4, '当前页全屏').withIcon('full-screen').withOnClick((data: any) => openCurrenFullscreen(data.path)),
];
const state = reactive({
routePath: route.fullPath,
dropdown: { x: '', y: '' },
// dropdown: { x: '', y: '' },
tagsRefsIndex: 0,
tagsViewList: [] as any,
sortable: '' as any,
contextmenu: {
items: contextmenuItems,
dropdown: { x: '', y: '' },
},
});
// tagsView
@@ -81,165 +114,177 @@ const setTagsStyle = computed(() => {
// tagsViewList
const addBrowserSetSession = (tagsViewList: Array<object>) => {
setSession('tagsViewList', tagsViewList);
setTagViews(tagsViewList);
};
// vuex tagsViewRoutes
// tagsViewRoutes
const getTagsViewRoutes = () => {
state.routePath = route.fullPath;
state.tagsViewList = [];
if (!themeConfig.value.isCacheTagsView) removeSession('tagsViewList');
tagsViews.value = [];
if (!themeConfig.value.isCacheTagsView) {
removeTagViews();
}
initTagsView();
};
// vuex isAffix
// isAffix
const initTagsView = () => {
if (getSession('tagsViewList') && themeConfig.value.isCacheTagsView) {
state.tagsViewList = getSession('tagsViewList');
const tagViews = getTagViews();
if (tagViews && themeConfig.value.isCacheTagsView) {
tagsViews.value = tagViews;
} else {
state.tagsViewList?.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
tagsViews.value?.map((v: any) => {
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
keepAliveNamesStores.setCacheKeepAlive(v);
}
});
addTagsView(route.fullPath);
}
// (li)
getTagsRefsIndex(route.fullPath);
setTagsRefsIndex(route.fullPath);
//
tagsViewmoveToCurrentTag();
};
// 1 tagsViewisHide tagsView
// pathfullPath
const addTagsView = (path: string, to: any = null) => {
if (!to) {
to = route;
}
path = decodeURI(path);
for (let tv of state.tagsViewList) {
if (tv.fullPath === path) {
return false;
const addTagsView = (path: string, to: any = null, tagViewIndex: number = -1) => {
nextTick(async () => {
if (!to) {
to = route;
}
}
const tagView = { ...to };
// Converting circular structure to JSON
tagView.matched = null;
tagView.redirectedFrom = null;
state.tagsViewList.push(tagView);
addBrowserSetSession(state.tagsViewList);
for (let tv of tagsViews.value) {
if (tv.path === path) {
return false;
}
}
const tagView = {
path: path,
name: to.name,
query: to.query,
title: to.meta.title,
icon: to.meta.icon,
isAffix: to.meta.isAffix,
isKeepAlive: to.meta.isKeepAlive,
};
if (tagViewIndex != -1) {
tagsViews.value.splice(tagViewIndex + 1, 0, tagView);
} else {
tagsViews.value.push(tagView);
}
await keepAliveNamesStores.addCachedView(tagView);
addBrowserSetSession(tagsViews.value);
});
};
// 2 tagsView
// pathfullPath
const refreshCurrentTagsView = (path: string) => {
const refreshCurrentTagsView = async (path: string) => {
const item = getTagsView(path);
await keepAliveNamesStores.delCachedView(item);
keepAliveNamesStores.addCachedView(item);
mittBus.emit('onTagsViewRefreshRouterView', path);
};
const getTagsView = (path: string) => {
return tagsViews.value.find((v: any) => v.path === path);
};
// 3 tagsViewisAffix
// pathfullPath
const closeCurrentTagsView = (path: string) => {
state.tagsViewList.map((v: any, k: number, arr: any) => {
if (!v.meta.isAffix) {
if (v.fullPath === path) {
state.tagsViewList.splice(k, 1);
tagsViews.value.map((v: TagsView, k: number, arr: any) => {
if (!v.isAffix) {
if (v.path === path) {
keepAliveNamesStores.delCachedView(v);
tagsViews.value.splice(k, 1);
setTimeout(() => {
if (state.routePath !== path) {
return;
}
let next;
let next: TagsView;
//
if (state.tagsViewList.length === k) {
if (tagsViews.value.length === k) {
next = k !== arr.length ? arr[k] : arr[arr.length - 1];
} else {
next = arr[k];
}
if (next.meta.isDynamic) {
router.push({ name: next.name, params: next.params });
} else {
if (next) {
router.push({ path: next.path, query: next.query });
} else {
router.push({ path: '/' });
}
}, 0);
}
}
});
addBrowserSetSession(state.tagsViewList);
addBrowserSetSession(tagsViews.value);
};
// 4 tagsViewisAffix
const closeOtherTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: TagsView) => {
if (v.isAffix && !v.isHide) {
keepAliveNamesStores.delOthersCachedViews(v);
tagsViews.value.push({ ...v });
}
});
addTagsView(path);
};
// 5 tagsViewisAffix
const closeAllTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
keepAliveNamesStores.delAllCachedViews();
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
state.tagsViewList.push({ ...v });
if (state.tagsViewList.some((v: any) => v.path === path)) router.push({ path, query: route.query });
else router.push({ path: v.path, query: route.query });
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
if (tagsViews.value.some((v: any) => v.path === path)) {
router.push({ path, query: route.query });
}
}
});
addBrowserSetSession(state.tagsViewList);
if (tagsViews.value) {
router.push({ path: '/' });
}
addBrowserSetSession(tagsViews.value);
};
// 6
const openCurrenFullscreen = (path: string) => {
const item = state.tagsViewList.find((v: any) => v.fullPath === path);
const item = tagsViews.value.find((v: any) => v.path === path);
nextTick(() => {
router.push({ path, query: item.query });
router.push({ path, query: item?.query });
const element = document.querySelector('.layout-main');
const screenfulls: any = screenfull;
screenfulls.request(element);
});
};
//
const onCurrentContextmenuClick = (data: any) => {
// pathfullPath
let { id, path } = data;
let currentTag = state.tagsViewList.find((v: any) => v.fullPath === path);
switch (id) {
case 0:
refreshCurrentTagsView(path);
router.push({ path, query: currentTag.query });
break;
case 1:
closeCurrentTagsView(path);
break;
case 2:
router.push({ path, query: currentTag.query });
closeOtherTagsView(path);
break;
case 3:
closeAllTagsView(path);
break;
case 4:
openCurrenFullscreen(path);
break;
}
};
//
const isActive = (route: any) => {
return route.fullPath === state.routePath;
const isActive = (tagView: TagsView) => {
return tagView.path === state.routePath;
};
// x,y props
const onContextmenu = (v: any, e: any) => {
const { clientX, clientY } = e;
state.dropdown.x = clientX;
state.dropdown.y = clientY;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(v);
};
// tagsView
const onTagsClick = (v: any, k: number) => {
state.routePath = decodeURI(v.fullPath);
state.routePath = decodeURI(v.path);
state.tagsRefsIndex = k;
router.push(v);
try {
router.push(v);
} catch (e) {}
};
//
const updateScrollbar = () => {
@@ -302,9 +347,9 @@ const tagsViewmoveToCurrentTag = () => {
});
};
// tagsView tagsView
const getTagsRefsIndex = (path: string) => {
if (state.tagsViewList.length > 0) {
state.tagsRefsIndex = state.tagsViewList.findIndex((item: any) => item.fullPath === path);
const setTagsRefsIndex = (path: string) => {
if (tagsViews.value.length > 0) {
state.tagsRefsIndex = tagsViews.value.findIndex((item: any) => item.path === path);
}
};
// tagsView
@@ -319,7 +364,7 @@ const initSortable = () => {
onEnd: () => {
const sortEndList: any = [];
state.sortable.toArray().map((val: any) => {
state.tagsViewList.map((v: any) => {
tagsViews.value.map((v: any) => {
if (v.name === val) sortEndList.push({ ...v });
});
});
@@ -329,24 +374,8 @@ const initSortable = () => {
}
};
// tagsView
// watch(
// pinia.state,
// (val) => {
// if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
// getTagsViewRoutes();
// },
// {
// deep: true,
// }
// );
//
onBeforeMount(() => {
// 0 1 2 3 4
mittBus.on('onCurrentContextmenuClick', (data: object) => {
onCurrentContextmenuClick(data);
});
// /
mittBus.on('openOrCloseSortable', () => {
initSortable();
@@ -354,8 +383,6 @@ onBeforeMount(() => {
});
//
onUnmounted(() => {
//
mittBus.off('onCurrentContextmenuClick');
// /
mittBus.off('openOrCloseSortable');
});
@@ -371,9 +398,10 @@ onMounted(() => {
});
//
onBeforeRouteUpdate((to) => {
state.routePath = decodeURI(to.fullPath);
addTagsView(to.fullPath, to);
getTagsRefsIndex(to.fullPath);
const path = decodeURI(to.fullPath);
state.routePath = path;
addTagsView(path, to, state.tagsRefsIndex);
setTagsRefsIndex(path);
tagsViewmoveToCurrentTag();
});
</script>
@@ -494,8 +522,14 @@ onBeforeRouteUpdate((to) => {
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
-webkit-mask-size: 18px 30px, 20px 30px, calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position: right bottom, left bottom, center top;
-webkit-mask-size:
18px 30px,
20px 30px,
calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position:
right bottom,
left bottom,
center top;
-webkit-mask-repeat: no-repeat;
}
@@ -516,14 +550,14 @@ onBeforeRouteUpdate((to) => {
&:hover {
@extend .tgs-style-three-svg;
background: var(--el-color-primary-light-9);
background: var(--tagsview3-active-background-color);
color: unset;
}
}
.is-active {
@extend .tgs-style-three-svg;
background: var(--el-color-primary-light-9) !important;
background: var(--tagsview3-active-background-color) !important;
color: var(--el-color-primary) !important;
z-index: 1;
}

View File

@@ -1,24 +1,23 @@
<template>
<div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
<el-menu router :default-active="state.defaultActive" background-color="transparent" mode="horizontal"
@select="onHorizontalSelect">
<el-menu router :default-active="state.defaultActive" background-color="transparent" mode="horizontal" @select="onHorizontalSelect">
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ val.meta.title }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</a>
</template>
@@ -32,7 +31,7 @@
<script lang="ts" setup name="navMenuHorizontal">
import { reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/views/layout/navMenu/subItem.vue';
import SubItem from '@/layout/navMenu/subItem.vue';
import { useRoutesList } from '@/store/routesList';
import { useThemeConfig } from '@/store/themeConfig';
import mittBus from '@/common/utils/mitt';

View File

@@ -2,19 +2,19 @@
<template v-for="val in chils">
<el-sub-menu :index="val.path" :key="val.path" v-if="val.children && val.children.length > 0">
<template #title>
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ val.meta.title }}</span>
</template>
<sub-item :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<template v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ val.meta.title }}</span>
</template>
<template v-else>
<a :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</a>
</template>
@@ -24,7 +24,6 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import SvgIcon from '@/components/svgIcon/index.vue';
export default defineComponent({
name: 'navMenuSubItem',
props: {

View File

@@ -1,21 +1,28 @@
<template>
<el-menu router :default-active="state.defaultActive" background-color="transparent" :collapse="setIsCollapse"
:unique-opened="themeConfig.isUniqueOpened" :collapse-transition="false">
<el-menu
router
:default-active="state.defaultActive"
background-color="transparent"
:collapse="setIsCollapse"
:unique-opened="themeConfig.isUniqueOpened"
:collapse-transition="false"
>
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ val.meta.title }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<span>{{ val.meta.title }}</span>
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template>
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template
>
</el-menu-item>
</template>
</el-menu>
@@ -26,7 +33,7 @@ import { reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/views/layout/navMenu/subItem.vue';
import SubItem from '@/layout/navMenu/subItem.vue';
import mittBus from '@/common/utils/mitt';
//

View File

@@ -1,5 +1,5 @@
<template>
<div class="layout-scrollbar">
<div>
<div class="layout-view-bg-white flex h100" v-loading="iframeLoading">
<iframe :src="iframeUrl" frameborder="0" height="100%" width="100%" id="iframe" v-show="!iframeLoading"></iframe>
</div>
@@ -7,7 +7,7 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, onBeforeMount, onUnmounted, nextTick, getCurrentInstance } from 'vue';
import { defineComponent, reactive, toRefs, onMounted, onBeforeMount, onUnmounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import mittBus from '@/common/utils/mitt';
export default defineComponent({

View File

@@ -1,5 +1,5 @@
<template>
<div class="layout-scrollbar">
<div>
<div class="layout-view-bg-white flex layout-view-link">
<a :href="currentRouteMeta.link" target="_blank" class="flex-margin">{{ currentRouteMeta.title }}{{ currentRouteMeta.link }}</a>
</div>

View File

@@ -0,0 +1,76 @@
<template>
<router-view v-slot="{ Component }">
<transition appear :name="setTransitionName" mode="out-in">
<keep-alive :include="getKeepAliveNames">
<component :is="Component" :key="state.refreshRouterViewKey" />
</keep-alive>
</transition>
</router-view>
</template>
<script lang="ts" setup name="layoutParentView">
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useKeepALiveNames } from '@/store/keepAliveNames';
import mittBus from '@/common/utils/mitt';
import { getTagViews } from '@/common/utils/storage';
const route = useRoute();
const { themeConfig } = storeToRefs(useThemeConfig());
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
const state: any = reactive({
refreshRouterViewKey: null,
keepAliveNameList: [],
});
// 获取组件缓存列表(name值)
const getKeepAliveNames = computed(() => {
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
});
// 页面加载前,处理缓存,页面刷新时路由缓存处理
onBeforeMount(() => {
state.keepAliveNameList = keepAliveNames.value;
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
if (decodeURI(route.fullPath) !== path) return false;
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
state.refreshRouterViewKey = '';
nextTick(() => {
state.refreshRouterViewKey = path;
state.keepAliveNameList = keepAliveNames.value;
});
});
});
// 页面加载时
onMounted(() => {
nextTick(() => {
setTimeout(() => {
if (themeConfig.value.isCacheTagsView) {
let tagsViewArr: any = getTagViews() || [];
cachedViews.value = tagsViewArr.filter((item: any) => item?.isKeepAlive).map((item: any) => item.name as string);
}
}, 0);
});
});
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
watch(
() => route.fullPath,
() => {
state.refreshRouterViewKey = decodeURI(route.fullPath);
},
{
immediate: true,
}
);
// 设置主界面切换动画
const setTransitionName = computed(() => {
return themeConfig.value.animation;
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('onTagsViewRefreshRouterView');
});
</script>

Some files were not shown because too many files have changed in this diff Show More