179 Commits

Author SHA1 Message Date
meilin.huang
c4cb4234fd fix: 问题修复 2024-05-16 17:26:32 +08:00
meilin.huang
89e12678eb refactor: 引入dayjs、新增refreshToken无感刷新、团队新增有效期、数据库等问题修复 2024-05-13 19:55:43 +08:00
meilin.huang
137ebb8e9e fix: 数据库表新增数据表单全必填问题修复等 2024-05-11 12:09:55 +08:00
meilin.huang
05625bd8c1 feat: 1.8.3 2024-05-10 19:59:49 +08:00
meilin.huang
4afeac5fdd refactor: 代码优化与数据库表列显示配置优化 2024-05-09 21:29:34 +08:00
meilin.huang
1d0e91f1af refactor: 机器计划任务与流程定义关联至标签 2024-05-08 21:04:25 +08:00
Coder慌
cf5111a325 !118 修复空数组分隔异常
Merge pull request !118 from 蒋小小/N/A
2024-05-07 11:59:12 +00:00
meilin.huang
78957a8ebd refactor: base.repo与app重构优化 2024-05-05 14:53:30 +08:00
meilin.huang
4ed892a656 feat: 文档更新与sqlite文件更新 2024-04-29 17:09:41 +08:00
meilin.huang
3486b07003 fix: 修复机器列表查询与放开vnc 2024-04-29 12:50:49 +08:00
meilin.huang
a5cd7caf19 refactor: base.repo与base.app精简优化 2024-04-29 12:29:56 +08:00
meilin.huang
f2c7ef78c0 refactor: 精简base.repo与base.app等 2024-04-28 23:45:57 +08:00
meilin.huang
653953ee76 feat: 机器新增命令过滤配置、首页功能完善(操作记录与快捷操作) 2024-04-27 01:35:21 +08:00
meilin.huang
a831614d5a fix: sql脚本问题修复等 2024-04-23 11:35:45 +08:00
meilin.huang
ebe73e2f19 feat: 标签支持拖拽移动与机器支持执行命令查看 2024-04-21 19:35:58 +08:00
蒋小小
29fd5a25d2 修复空数组分隔异常
Signed-off-by: 蒋小小 <bwcx_jzy@163.com>
2024-04-20 17:33:19 +00:00
zongyangleo
44805ce580 !116 fix: 新版本问题修复
* fix: 新版本问题修复
2024-04-19 11:27:29 +00:00
meilin.huang
2a6d620830 fix: 问题修复 2024-04-18 20:50:14 +08:00
meilin.huang
01d3e1ad28 refactor: 数据库实例与凭证关联至标签&其他问题修复重构等 2024-04-17 21:28:28 +08:00
meilin.huang
f4162c38db fix: 问题修复与redis密码迁移至凭证 2024-04-13 17:01:12 +08:00
zongyangleo
1a4626c24d !115 fix: 新版本问题修复
* fix: 新版本问题修复
2024-04-12 14:39:08 +00:00
meilin.huang
d6eb9683d1 fix: 新版本问题修复 2024-04-12 20:30:28 +08:00
meilin.huang
e2b524dadb feat: release1.8.0 2024-04-12 17:07:28 +08:00
zongyangleo
8998a21626 !114 feat:rdp优化,mssql迁移优化,term支持trzsz
* fix: 合并代码
* refactor: rdp优化,mssql迁移优化,term支持trzsz
2024-04-12 07:53:42 +00:00
meilin.huang
abc015aec0 refactor: 数据库授权凭证迁移 2024-04-12 13:24:20 +08:00
meilin.huang
4ef8d27b1e refactor: 授权凭证优化 2024-04-10 23:17:20 +08:00
meilin.huang
40b6e603fc reafctor: 团队管理与授权凭证优化 2024-04-10 13:04:31 +08:00
meilin.huang
21498584b1 refactor: 初步提交全局授权凭证-资源多账号改造 2024-04-09 12:55:51 +08:00
meilin.huang
408bac09a1 refactor: 标签资源重构 2024-04-06 18:19:17 +08:00
zongyangleo
582d879a77 !112 feat: 机器管理支持ssh+rdp连接win服务器
* feat: rdp 文件管理
* feat: 机器管理支持ssh+rdp连接win服务器
2024-04-06 04:03:38 +00:00
meilin.huang
38ff5152e0 refactor: dbms优化 2024-03-29 21:40:26 +08:00
meilin.huang
d1d372e1bf feat: 数据迁移新增实时日志&数据库游标遍历查询问题修复 2024-03-28 22:20:39 +08:00
Coder慌
5e4793433b !111 refactor:获取表索引,默认过滤主键索引
Merge pull request !111 from zongyangleo/dev_0327_fix
2024-03-27 13:06:45 +00:00
zongyangleo
54ad19f97e refactor:获取表索引,默认过滤主键索引 2024-03-27 08:26:12 +08:00
meilin.huang
fc166650b3 refactor: dbm重构等 2024-03-26 21:46:03 +08:00
zongyangleo
2acc295259 !110 feat: 支持各源数据库导出sql,数据库迁移部分bug修复
* feat: 各源数据库导出
* fix: 数据库迁移 bug修复
2024-03-26 09:05:28 +00:00
meilin.huang
4b3ed1310d refactor: dbms 2024-03-21 20:28:24 +08:00
meilin.huang
b2cfd1517c refactor: dbms与标签管理优化 2024-03-21 17:15:52 +08:00
zongyangleo
b13d27ccd6 !109 refactor:ddl生成方式重构,数据类型和长度重构,所有数据库迁移调试
* feat:同步sqlite全量sql
* refactor:ddl生成方式重构,数据类型和长度重构,所有数据库迁移调试
2024-03-21 03:35:18 +00:00
meilin.huang
68e0088016 refactor: dbms优化 2024-03-18 12:25:40 +08:00
zongyangleo
bd1e83989d !108 feat:支持不同源数据库迁移
* feat:支持不同源数据库迁移
2024-03-15 09:01:51 +00:00
meilin.huang
263dfa6be7 refactor: dbm包重构 2024-03-15 13:31:53 +08:00
meilin.huang
eb55f93864 refactor: dbm包重构 2024-03-11 20:04:20 +08:00
meilin.huang
8589105e44 feat: oracle支持服务名、数据库执行超时时间配置等 2024-03-07 17:26:11 +08:00
meilin.huang
986b187f0a feat: v1.7.4 2024-03-04 20:33:04 +08:00
zongyangleo
008d34c453 !107 feat:支持修改表名、注释,oracle bug修复
* fix:修复oracle查询数据参数超过1000错误 ORA-01795
* feat:支持右键重命名表
* feat:支持修改表名、表注释
2024-03-04 11:32:04 +00:00
meilin.huang
49d3f988c9 feat: redis支持工单流程审批 2024-03-02 19:08:19 +08:00
zongyangleo
76475e807e !106 feat:数据同步支持唯一键冲突策略
* refactor:sql同步
* fix: 表格右键导出菜单换行符修复
* feat:数据同步支持唯一键冲突策略
2024-03-01 04:03:03 +00:00
meilin.huang
f93231da61 feat: dbms新增支持工单流程审批 2024-02-29 22:12:50 +08:00
meilin.huang
bf75483a3c refactor: 简化api层相关调用 2024-02-25 12:46:18 +08:00
meilin.huang
b56b0187cf refactor: api层尽可能屏蔽gin框架相关代码 2024-02-24 16:30:29 +08:00
meilin.huang
7e7f02b502 refactor: 机器终端操作优化 2024-02-23 22:53:17 +08:00
meilin.huang
878985f7c5 refactor: 依赖版本升级等 2024-02-22 21:03:13 +08:00
meilin.huang
2133d9b737 fix: 终端操作col和row初始化问题修复 2024-02-18 18:42:25 +08:00
meilin.huang
d711a36749 feat: v1.7.3 2024-02-08 09:53:48 +08:00
meilin.huang
9dbf104ef1 refactor: 机器操作界面调整 2024-02-07 21:14:29 +08:00
zongyangleo
20eb06fb28 !101 feat: 新增机器操作菜单
* feat: 新增机器操作菜单
2024-02-07 06:37:59 +00:00
meilin.huang
9c20bdef39 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:33:31 +08:00
zongyangleo
3fdd98a390 !99 feat: DBMS新增kingbaseES、vastbase,还有一些优化
* refactor: 重构机器列表展示
* fix:修复编辑表问题
* refactor: 优化下拉实例显示
* feat: DBMS新增kingbaseES(已测试postgres、oracle兼容模式) 、vastbase
2024-02-06 07:32:03 +00:00
meilin.huang
d4f456c0cf Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:17:39 +08:00
kanzihuang
f2b6e15cf4 !100 定时清理数据库备份数据
* feat: 优化数据库 BINLOG 同步机制
* feat: 删除数据库实例前需删除关联的数据库备份与恢复任务
* refactor: 重构数据库备份与恢复模块
* feat: 定时清理数据库备份历史和本地 Binlog 文件
* feat: 压缩数据库备份文件
2024-02-06 07:16:56 +00:00
meilin.huang
6be0ea6aed fix: dbms数据行编辑 2024-02-01 12:05:41 +08:00
meilin.huang
eee08be2cc feat: 数据库支持编辑行数据 2024-01-31 20:41:41 +08:00
meilin.huang
252fc553f2 feat: v1.7.2 2024-01-31 12:53:27 +08:00
meilin.huang
ac2ceed3f9 refactor: code review 2024-01-30 21:56:49 +08:00
kanzihuang
3f828cc5b0 !96 删除数据库备份和恢复历史
* feat: 删除数据库备份历史
* refactor dbScheduler
* feat: 从数据库备份历史中恢复数据库
* feat: 删除数据库恢复历史记录
* refactor dbScheuler
2024-01-30 13:12:43 +00:00
zongyangleo
fc1b9ef35d !97 一些优化
* refactor: 重构表格分页组件,适配大数据量分页
* fix:定时任务修复
* feat: gaussdb单独提出来
2024-01-30 13:09:26 +00:00
meilin.huang
d0b71a1c40 refactor: dialect使用方式调整 2024-01-29 16:02:28 +08:00
meilin.huang
a743a6a05a Merge branch 'master' into dev 2024-01-29 12:21:22 +08:00
zongyangleo
0e6b9713ce !93 feat: DBMS支持mssql和一些功能优化
* feat: 表格+表格元数据缓存
* feat:跳板机支持多段跳
* fix: 所有数据库区分字段主键和自增
* feat: DBMS支持mssql
* refactor: 去除无用的getter方法
2024-01-29 04:20:23 +00:00
meilin.huang
b9afbc764d refactor: 去除无用的getter方法 2024-01-29 11:34:48 +08:00
meilin.huang
923e183a67 refactor: code review 2024-01-26 17:17:26 +08:00
meilin.huang
7e9a381641 refactor: 数据库meta使用注册方式,方便可插拔 2024-01-24 17:01:17 +08:00
zongyangleo
bed95254d0 !91 fix: oracle数据同步 bug
* fix: oracle数据同步 bug
2024-01-24 08:29:16 +00:00
meilin.huang
e4d13f3377 refactor: 引入日志切割库、indexApi拆分等 2024-01-23 19:30:28 +08:00
Coder慌
d530365ef9 !90 fix: 依赖注入支持私有变量
Merge pull request !90 from kanzihuang/feat-db-bak
2024-01-23 09:02:37 +00:00
wanli
070d4ea104 fix: 依赖注入支持私有变量 2024-01-23 16:29:41 +08:00
zongyangleo
3fc86f0fae !88 feat: dbms表支持右键菜单:删除表、编辑表、新建表、复制表
* feat: 支持复制表
* feat: dbms表支持右键菜单:删除表、编辑表、新建表
2024-01-23 04:08:02 +00:00
kanzihuang
3b77ab2727 !89 feat: 给数据库备份和恢复配置操作权限
* feat: 给数据库备份和恢复配置操作权限
* refactor: 数据库备份与恢复采用最新依赖注入机制
2024-01-23 04:06:08 +00:00
meilin.huang
76cb991282 fix: 数据同步更新时间展示等问题 2024-01-23 09:27:05 +08:00
meilin.huang
9efd20f1b9 refactor: ioc与系统初始化处理方式调整 2024-01-22 11:35:28 +08:00
kanzihuang
de5b9e46d3 !87 fix: 修复数据库备份与恢复问题
* feat: 修复数据库备份与恢复问题
* feat: 启用 BINLOG 支持全量备份和增量备份,未启用 BINLOG 仅支持全量备份
* feat: 数据库恢复后自动备份,避免数据丢失
2024-01-22 03:12:16 +00:00
meilin.huang
f27d3d200f feat: 新增简易版ioc 2024-01-21 22:52:20 +08:00
meilin.huang
f4a64b96a9 feat: v1.7.1新增支持sqlite&oracle分页限制等问题修复 2024-01-19 21:33:37 +08:00
zongyangleo
9a59749763 !86 dbms支持sqlite和一些bug修复
* fix: 达梦数据库连接修复,以支持带特殊字符的密码和schema
* fix: oracle bug修复
* feat: dbms支持sqlite
* fix: dbms 修改字段名bug
2024-01-19 08:59:35 +00:00
kanzihuang
b017b902f8 !85 fix: 修复 BINLOG同步任务加载问题
* Merge branch 'dev' of gitee.com:dromara/mayfly-go into feat-db-bak
* fix: 修复 BINLOG 同步任务加载问题
2024-01-19 00:40:44 +00:00
meilin.huang
7c53353c60 fix: sqlite数据问题时间类型问题修复等 2024-01-18 17:18:17 +08:00
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
714 changed files with 65075 additions and 15623 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 WORKDIR /mayfly
COPY mayfly_go_web . COPY mayfly_go_web .
RUN yarn RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn install && \
RUN yarn build yarn build
# 构建后端资源 # 构建后端资源
FROM golang:1.21.0 as be-builder FROM golang:1.22 as be-builder
ENV GOPROXY https://goproxy.cn ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly WORKDIR /mayfly
@@ -18,18 +18,20 @@ WORKDIR /mayfly
# Copy the go source for building server # Copy the go source for building server
COPY server . COPY server .
RUN go mod download RUN go mod tidy && go mod download
COPY --from=fe-builder /mayfly/dist /mayfly/static/static COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build # Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \ RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \ go build -a -ldflags=-w \
-o mayfly-go main.go -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 ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

View File

@@ -13,7 +13,7 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/> <img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a> </a>
<a href="https://github.com/golang/go" target="_blank"> <a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/> <img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
</a> </a>
<a href="https://cn.vuejs.org" target="_blank"> <a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue"> <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 sqlserver 达梦 高斯 sqlite数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架
@@ -45,56 +45,61 @@ http://go.mayfly.run
### 系统核心功能截图 ### 系统核心功能截图
##### 记录操作记录 #### 首页
![记录操作记录](https://objs.gitee.io/mayfly-go-docs/home/log.jpg "屏幕截图.png") ![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
#### 机器操作 #### 机器操作
##### 状态查看 ##### 状态查看
![状态查看](https://objs.gitee.io/mayfly-go-docs/home/machine-status.jpg "屏幕截图.png") ![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
##### ssh 终端 ##### ssh 终端
![ssh终端](https://objs.gitee.io/mayfly-go-docs/home/machine-ssh.jpg "屏幕截图.png") ![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### 文件操作 ##### 文件操作
![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-dir.jpg "屏幕截图.png") ![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-content-update.jpg "屏幕截图.png")
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### 数据库操作 #### 数据库操作
##### sql 编辑器 ##### sql 编辑器
![sql编辑器](https://objs.gitee.io/mayfly-go-docs/home/dbms-sql-editor.jpg "屏幕截图.png") ![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### 在线增删改查数据 ##### 在线增删改查数据
![选表查数据](https://objs.gitee.io/mayfly-go-docs/home/dbms-show-table-data.jpg "屏幕截图.png") ![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis 操作 #### Redis 操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png") ![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo 操作 #### Mongo 操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/mongo-op.jpg "屏幕截图.png") ![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
##### 系统管理 #### 工单流程审批
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")
#### 系统管理
##### 账号管理 ##### 账号管理
![账号管理](https://images.gitee.com/uploads/images/2021/0607/173919_a8d7dc18_1240250.png "屏幕截图.png") ![账号管理](https://foruda.gitee.com/images/1714379179491881231/c6d802ae_1240250.png "屏幕截图")
##### 角色管理 ##### 角色管理
![角色管理](https://images.gitee.com/uploads/images/2021/0607/174028_3654fb28_1240250.png "屏幕截图.png") ![角色管理](https://foruda.gitee.com/images/1714379269408676381/6ac1e85c_1240250.png "屏幕截图")
##### 资源管理 ##### 菜单资源管理
![资源管理](https://images.gitee.com/uploads/images/2021/0607/174436_e9e1535c_1240250.png "屏幕截图.png") ![菜单资源管理](https://foruda.gitee.com/images/1714379321338009940/a00d6a02_1240250.png "屏幕截图")
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go **其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go

View File

@@ -56,7 +56,7 @@ function build() {
if [ "${os}" == "windows" ];then if [ "${os}" == "windows" ];then
execFileName="${execFileName}.exe" execFileName="${execFileName}.exe"
fi 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 if [ -d ${toFolder} ] ; then
echo_green "目标文件夹已存在,清空文件夹" echo_green "目标文件夹已存在,清空文件夹"

View File

@@ -14,7 +14,7 @@ services:
restart: always restart: always
server: server:
image: mayfly-go:v1.3.1 image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -1,6 +1,8 @@
# 本地环境 # 本地环境
ENV = 'development' ENV = 'development'
VITE_OPEN = true
# 本地环境接口地址 # 本地环境接口地址
VITE_API_URL = '/api' VITE_API_URL = '/api'

View File

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

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 lyt-Top
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -9,30 +9,33 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.3.1",
"asciinema-player": "^3.6.2", "@vueuse/core": "^10.9.0",
"asciinema-player": "^3.7.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"countup.js": "^2.7.0", "cropperjs": "^1.6.1",
"cropperjs": "^1.5.11", "dayjs": "^1.11.11",
"echarts": "^5.4.3", "echarts": "^5.5.0",
"element-plus": "^2.4.2", "element-plus": "^2.7.3",
"jsencrypt": "^3.3.1", "js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.44.0", "monaco-editor": "^0.48.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode.vue": "^3.4.0", "qrcode.vue": "^3.4.1",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.2",
"sql-formatter": "^14.0.0", "splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2",
"trzsz": "^1.1.5",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.3.9", "vue": "^3.4.27",
"vue-clipboard3": "^1.0.1", "vue-router": "^4.3.2",
"vue-router": "^4.2.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0", "xterm-addon-search": "^0.13.0",
@@ -40,21 +43,22 @@
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.178", "@types/lodash": "^4.14.178",
"@types/node": "^15.6.0", "@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.3", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^5.0.4",
"@vue/compiler-sfc": "^3.3.4", "@vue/compiler-sfc": "^3.4.27",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.25.0",
"prettier": "^3.0.3", "prettier": "^3.2.5",
"sass": "^1.69.0", "sass": "^1.77.1",
"typescript": "^5.3.2", "typescript": "^5.4.5",
"vite": "^5.0.2", "vite": "^5.2.11",
"vue-eslint-parser": "^9.3.1" "vue-eslint-parser": "^9.4.2"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View File

@@ -4,7 +4,7 @@
:zIndex="10000000" :zIndex="10000000"
:width="210" :width="210"
v-if="themeConfig.isWatermark" v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.5)' }" :font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText" :content="themeConfig.watermarkText"
class="h100" class="h100"
> >
@@ -22,12 +22,10 @@ import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig'; import { useThemeConfig } from '@/store/themeConfig';
import { getLocal } from '@/common/utils/storage';
import LockScreen from '@/layout/lockScreen/index.vue'; import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue'; import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt'; import mittBus from '@/common/utils/mitt';
import { getThemeConfig } from './common/utils/storage'; import { useIntervalFn } from '@vueuse/core';
import { useWatermark } from '@/common/sysconfig';
const setingsRef = ref(); const setingsRef = ref();
const route = useRoute(); const route = useRoute();
@@ -40,12 +38,6 @@ const openSetingsDrawer = () => {
setingsRef.value.openDrawer(); setingsRef.value.openDrawer();
}; };
const prefers = matchMedia('(prefers-color-scheme: dark)');
const switchDarkFollowOS = () => {
// 跟随系统主题
themeConfigStores.switchDark(prefers.matches);
};
// 页面加载时 // 页面加载时
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
@@ -54,18 +46,8 @@ onMounted(() => {
openSetingsDrawer(); openSetingsDrawer();
}); });
// 获取缓存中的布局配置 // 初始化系统主题
const tc = getThemeConfig(); themeConfigStores.initThemeConfig();
if (tc) {
themeConfigStores.setThemeConfig({ themeConfig: tc });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
}
switchDarkFollowOS();
// 是否开启水印
useWatermark().then((res) => {
themeConfigStores.setWatermarkConfig(res);
});
}); });
}); });
@@ -77,36 +59,35 @@ watch(
setTimeout(() => { setTimeout(() => {
setWatermarkContent(); setWatermarkContent();
refreshWatermarkTime(); refreshWatermarkTime();
resume();
}, 500); }, 500);
} else {
pause();
} }
} }
); );
// 刷新水印时间
const { pause, resume } = useIntervalFn(() => {
if (!themeConfig.value.isWatermark) {
pause();
}
refreshWatermarkTime();
}, 60000);
const setWatermarkContent = () => { const setWatermarkContent = () => {
themeConfigStores.setWatermarkUser(); themeConfigStores.setWatermarkUser();
themeConfigStores.setWatermarkNowTime();
}; };
let refreshWatermarkTimeInterval: any = null;
/** /**
* 刷新水印时间 * 刷新水印时间
*/ */
const refreshWatermarkTime = () => { const refreshWatermarkTime = () => {
if (refreshWatermarkTimeInterval) { themeConfigStores.setWatermarkNowTime();
clearInterval(refreshWatermarkTimeInterval);
}
refreshWatermarkTimeInterval = setInterval(() => {
if (themeConfig.value.isWatermark) {
themeConfigStores.setWatermarkNowTime();
} else {
clearInterval(refreshWatermarkTimeInterval);
}
}, 60000);
}; };
// 页面销毁时,关闭监听布局配置 // 页面销毁时,关闭监听布局配置
onUnmounted(() => { onUnmounted(() => {
clearInterval(refreshWatermarkTimeInterval);
mittBus.off('openSetingsDrawer', () => {}); mittBus.off('openSetingsDrawer', () => {});
}); });

File diff suppressed because one or more lines are too long

View File

@@ -26,6 +26,13 @@
"unicode": "e8b7", "unicode": "e8b7",
"unicode_decimal": 59575 "unicode_decimal": 59575
}, },
{
"icon_id": "12295203",
"name": "达梦数据库",
"font_class": "db-dm",
"unicode": "e6f0",
"unicode_decimal": 59120
},
{ {
"icon_id": "10055634", "icon_id": "10055634",
"name": "云数据库MongoDB", "name": "云数据库MongoDB",
@@ -46,6 +53,55 @@
"font_class": "redis", "font_class": "redis",
"unicode": "e619", "unicode": "e619",
"unicode_decimal": 58905 "unicode_decimal": 58905
},
{
"icon_id": "25271976",
"name": "oracle",
"font_class": "oracle",
"unicode": "e507",
"unicode_decimal": 58631
},
{
"icon_id": "8105644",
"name": "mariadb",
"font_class": "mariadb",
"unicode": "e513",
"unicode_decimal": 58643
},
{
"icon_id": "13601813",
"name": "sqlite",
"font_class": "sqlite",
"unicode": "e546",
"unicode_decimal": 58694
},
{
"icon_id": "29340317",
"name": "temp-mssql",
"font_class": "MSSQLNATIVE",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "7699332",
"name": "gaussdb",
"font_class": "gauss",
"unicode": "e683",
"unicode_decimal": 59011
},
{
"icon_id": "34836637",
"name": "kingbase",
"font_class": "kingbase",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "33047500",
"name": "vastbase",
"font_class": "vastbase",
"unicode": "e62b",
"unicode_decimal": 58923
} }
] ]
} }

View File

@@ -1,4 +1,5 @@
import request from './request'; import request from './request';
import { useApiFetch } from '@/hooks/useRequest';
/** /**
* 可用于各模块定义各自api请求 * 可用于各模块定义各自api请求
@@ -14,11 +15,27 @@ class Api {
*/ */
method: string; method: string;
/**
* 请求前处理函数
* param1: param请求参数
*/
beforeHandler: Function;
constructor(url: string, method: string) { constructor(url: string, method: string) {
this.url = url; this.url = url;
this.method = method; this.method = method;
} }
/**
* 设置请求前处理回调函数
* @param func 请求前处理器
* @returns this
*/
withBeforeHandler(func: Function) {
this.beforeHandler = func;
return this;
}
/** /**
* 获取权限的完整url * 获取权限的完整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的参数 * @param {Object} param 请求该api的参数
*/ */
request(param: any = null, options: any = null, headers: any = null): Promise<any> { async request(param: any = null, options: any = {}): Promise<any> {
return request.request(this.method, this.url, param, headers, options); 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 default Api;
export class PageRes {
list: any[] = [];
total: number = 0;
}

View File

@@ -22,6 +22,8 @@ export class EnumValue {
*/ */
tag: EnumValueTag; tag: EnumValueTag;
extra: any;
constructor(value: any, label: string) { constructor(value: any, label: string) {
this.value = value; this.value = value;
this.label = label; this.label = label;
@@ -53,6 +55,11 @@ export class EnumValue {
return this; return this;
} }
setExtra(extra: any): EnumValue {
this.extra = extra;
return this;
}
public static of(value: any, label: string): EnumValue { public static of(value: any, label: string): EnumValue {
return new EnumValue(value, label); return new EnumValue(value, label);
} }
@@ -60,11 +67,12 @@ export class EnumValue {
/** /**
* 根据枚举值获取指定枚举值对象 * 根据枚举值获取指定枚举值对象
* *
* @param enumValues 所有枚举值 * @param enums 枚举对象
* @param value 需要匹配的枚举值 * @param value 需要匹配的枚举值
* @returns 枚举值对象 * @returns 枚举值对象
*/ */
static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null { static getEnumByValue(enums: any, value: any): EnumValue | null {
const enumValues = Object.values(enums) as any;
for (let enumValue of enumValues) { for (let enumValue of enumValues) {
if (enumValue.value == value) { if (enumValue.value == value) {
return enumValue; return enumValue;

View File

@@ -0,0 +1,24 @@
import EnumValue from './Enum';
// 资源类型
export const ResourceTypeEnum = {
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
};
// 标签关联的资源类型
export const TagResourceTypeEnum = {
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: ResourceTypeEnum.Machine,
Db: ResourceTypeEnum.Db,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
};

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`, baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本 // 系统版本
version: 'v1.5.4', version: 'v1.8.5',
}; };
export default config; 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

@@ -2,6 +2,7 @@ import request from './request';
export default { export default {
login: (param: any) => request.post('/auth/accounts/login', param), login: (param: any) => request.post('/auth/accounts/login', param),
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param), otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
getPublicKey: () => request.get('/common/public-key'), getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params), getConfigValue: (params: any) => request.get('/sys/configs/value', params),

View File

@@ -1,4 +1,9 @@
export const AccountUsernamePattern = { export const AccountUsernamePattern = {
pattern: /^[a-zA-Z0-9_]{5,20}$/g, pattern: /^[a-zA-Z0-9_]{5,20}$/g,
message: '只允许输入5-20位大小写字母、数字、下划线', message: '只允许输入5-20位大小写字母、数字、_-.:',
};
export const ResourceCodePattern = {
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
message: '只允许输入1-32位大小写字母、数字、_-.:',
}; };

View File

@@ -1,9 +1,21 @@
import router from '../router'; import router from '../router';
import Axios from 'axios';
import config from './config'; import config from './config';
import { getClientId, getToken } from './utils/storage'; import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string'; import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus'; 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 { export interface Result {
/** /**
@@ -20,15 +32,17 @@ export interface Result {
data?: any; data?: any;
} }
enum ResultEnum { export enum ResultEnum {
SUCCESS = 200, SUCCESS = 200,
ERROR = 400, ERROR = 400,
PARAM_ERROR = 405, PARAM_ERROR = 405,
SERVER_ERROR = 500, SERVER_ERROR = 500,
NO_PERMISSION = 501, NO_PERMISSION = 501,
ACCESS_TOKEN_INVALID = 502, // accessToken失效
} }
const baseUrl: string = config.baseApiUrl; export const baseUrl: string = config.baseApiUrl;
// const baseUrl: string = 'http://localhost:18888/api';
// const baseWsUrl: string = config.baseWsUrl; // const baseWsUrl: string = config.baseWsUrl;
/** /**
@@ -41,13 +55,13 @@ function notifyErrorMsg(msg: string) {
} }
// create an axios instance // create an axios instance
const service = Axios.create({ const axiosInst = axios.create({
baseURL: baseUrl, // url = base url + request url baseURL: baseUrl, // url = base url + request url
timeout: 60000, // request timeout timeout: 60000, // request timeout
}); });
// request interceptor // request interceptor
service.interceptors.request.use( axiosInst.interceptors.request.use(
(config: any) => { (config: any) => {
// do something before request is sent // do something before request is sent
const token = getToken(); const token = getToken();
@@ -64,24 +78,16 @@ service.interceptors.request.use(
); );
// response interceptor // response interceptor
service.interceptors.response.use( axiosInst.interceptors.response.use(
(response) => { (response) => 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);
},
(e: any) => { (e: any) => {
const rejectPromise = Promise.reject(e); const rejectPromise = Promise.reject(e);
if (axios.isCancel(e)) {
console.log('请求已取消');
return rejectPromise;
}
const statusCode = e.response?.status; const statusCode = e.response?.status;
if (statusCode == 500) { if (statusCode == 500) {
notifyErrorMsg('服务器未知异常'); notifyErrorMsg('服务器未知异常');
@@ -112,44 +118,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继续处理 * 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
* *
* @param {Object} method 请求方法(GET,POST,PUT,DELTE等) * @param {Object} method 请求方法(GET,POST,PUT,DELTE等)
* @param {Object} uri uri * @param {Object} uri uri
* @param {Object} params 参数 * @param {Object} params 参数
*/ */
function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { async function request(method: string, url: string, params: any = null, options: any = {}): Promise<any> {
if (!url) throw new Error('请求url不能为空'); const { execute, data } = useApiFetch(Api.create(url, method), params, options);
// 简单判断该url是否是restful风格 await execute();
if (url.indexOf('{') != -1) { return data.value;
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则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (e.msg && e?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
});
} }
/** /**
@@ -159,20 +183,20 @@ function request(method: string, url: string, params: any = null, headers: any =
* @param {Object} url uri * @param {Object} url uri
* @param {Object} params 参数 * @param {Object} params 参数
*/ */
function get(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { function get(url: string, params: any = null, options: any = {}): Promise<any> {
return request('get', url, params, headers, options); return request('get', url, params, options);
} }
function post(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { function post(url: string, params: any = null, options: any = {}): Promise<any> {
return request('post', url, params, headers, options); return request('post', url, params, options);
} }
function put(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { function put(url: string, params: any = null, options: any = {}): Promise<any> {
return request('put', url, params, headers, options); return request('put', url, params, options);
} }
function del(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { function del(url: string, params: any = null, options: any = {}): Promise<any> {
return request('delete', url, params, headers, options); return request('delete', url, params, options);
} }
function getApiUrl(url: string) { function getApiUrl(url: string) {
@@ -185,11 +209,22 @@ export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`; return `token=${getToken()}&clientId=${getClientId()}`;
} }
export default { function parseResult(result: Result) {
request, if (result.code === ResultEnum.SUCCESS) {
get, return result.data;
post, }
put,
del, // 如果提示没有权限则移除token使其重新登录
getApiUrl, 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,9 +1,77 @@
import openApi from './openApi'; import openApi from './openApi';
// 登录是否使用验证码配置key // 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity'; const AccountLoginSecurityKey = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha'; const MachineConfigKey = 'MachineConfig';
const UseWatermarkConfigKey = 'UseWatermark'; 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,67 +95,9 @@ export async function getBoolConfigValue(key: string, defaultValue: boolean): Pr
return convertBool(value, defaultValue); 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 useWatermark(): Promise<any> {
const value = await getConfigValue(UseWatermarkConfigKey);
const defaultValue = {
isUse: true,
};
if (!value) {
return defaultValue;
}
try {
const jsonValue = JSON.parse(value);
// 将字符串转为bool
jsonValue.isUse = convertBool(jsonValue.isUse, true);
return jsonValue;
} catch (e) {
return defaultValue;
}
}
function convertBool(value: string, defaultValue: boolean) { function convertBool(value: string, defaultValue: boolean) {
if (!value) { if (!value) {
return defaultValue; return defaultValue;
} }
return value == '1' || value == 'true'; return value == '1' || value == 'true';
} }
/**
* 获取LDAP登录配置
*
* @returns
*/
export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}

View File

@@ -41,7 +41,14 @@ class SysSocket {
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`; const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl) this.socket = SocketBuilder.builder(sysMsgUrl)
.message((event: { data: string }) => { .message((event: { data: string }) => {
const message = JSON.parse(event.data); let message;
try {
message = JSON.parse(event.data);
} catch (e) {
console.error('解析ws消息失败', e);
return;
}
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理 // 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
const handler = this.categoryHandlers.get(message.category); const handler = this.categoryHandlers.get(message.category);
if (handler) { if (handler) {
@@ -66,9 +73,9 @@ class SysSocket {
} }
destory() { destory() {
this.socket.close(); this.socket?.close();
this.socket = null; this.socket = null;
this.categoryHandlers.clear(); this.categoryHandlers?.clear();
} }
/** /**

View File

@@ -1,17 +0,0 @@
import { ref } from 'vue';
const vw = ref(document.documentElement.clientWidth);
const vh = ref(document.documentElement.clientHeight);
window.addEventListener('resize', () => {
vw.value = document.documentElement.clientWidth;
vh.value = document.documentElement.clientHeight;
});
/**
* 获取视图宽高
* @returns 视图宽高
*/
export function useViewport() {
return { vw, vh };
}

View File

@@ -1,27 +0,0 @@
export function dateFormat2(fmt: string, date: Date) {
let ret;
const opt = {
'y+': date.getFullYear().toString(), // 年
'M+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'm+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString(), // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
}
}
return fmt;
}
export function dateStrFormat(fmt: string, dateStr: string) {
return dateFormat2(fmt, new Date(dateStr));
}
export function dateFormat(dateStr: string) {
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
}

View File

@@ -7,24 +7,22 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
for (let column of columns) { for (let column of columns) {
let val: any = data[column]; let val: any = data[column];
if (val == null || val == undefined) { if (val == null || val == undefined) {
dataValueArr.push(''); val = '';
continue; } else if (val && typeof val == 'string') {
} // 替换换行符
val = val.replace(/[\r\n]/g, '\\n');
if (typeof val == 'string' && val) {
// csv格式如果有逗号整体用双引号括起来如果里面还有双引号就替换成两个双引号这样导出来的格式就不会有问题了 // csv格式如果有逗号整体用双引号括起来如果里面还有双引号就替换成两个双引号这样导出来的格式就不会有问题了
if (val.indexOf(',') != -1) { if (val.indexOf(',') != -1) {
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误 // 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
if (val.indexOf('"') != -1) { if (val.indexOf('"') != -1) {
val = val.replace(/\"/g, '""'); val = val.replace(/"/g, '""');
} }
// 再将逗号转义 // 再将逗号转义
val = `"${val}"`; val = `"${val}"`;
} }
dataValueArr.push(val + '\t');
} else {
dataValueArr.push(val + '\t');
} }
dataValueArr.push(String(val));
} }
cvsData.push(dataValueArr); cvsData.push(dataValueArr);
} }

View File

@@ -1,3 +1,18 @@
import dayjs from 'dayjs';
/**
* 格式化日期
* @param date 日期 字符串 Date 时间戳等
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
* @returns 格式化后内容
*/
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
if (!date) {
return '';
}
return dayjs(date).format(format);
}
/** /**
* 格式化字节单位 * 格式化字节单位
* @param size byte size * @param size byte size
@@ -16,161 +31,73 @@ export function formatByteSize(size: number, fixed = 2) {
} }
/** /**
* 格式化json字符串 * 容量转为对应的字节大小,如 1KB转为 1024
* @param txt json字符串 * @param sizeString 1kb 1gb等
* @param compress 是否压缩 * @returns
* @returns 格式化后的字符串
*/ */
export function formatJsonString(txt: string, compress: boolean) { export function convertToBytes(sizeStr: string) {
var indentChar = ' '; sizeStr = sizeStr.trim();
if (/^\s*$/.test(txt)) { const unit = sizeStr.slice(-2);
console.log('数据为空,无法格式化! ');
return txt;
}
try {
var data = JSON.parse(txt);
} catch (e: any) {
console.log('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err');
return txt;
}
var draw: any = [],
line = compress ? '' : '\n',
// eslint-disable-next-line no-unused-vars
nodeCount: number = 0,
// eslint-disable-next-line no-unused-vars
maxDepth: number = 0;
var notify = function (name: any, value: any, isLast: any, indent: any, formObj: any) { const valueStr = sizeStr.slice(0, -2);
nodeCount++; /*节点计数*/ const value = parseInt(valueStr, 10);
for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
tab = compress ? '' : tab; /*压缩模式忽略缩进*/
maxDepth = ++indent; /*缩进递增并记录*/
if (value && value.constructor == Array) {
/*处理数组*/
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
} else if (value && typeof value == 'object') {
/*处理对象*/
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
var len = 0,
i = 0;
for (var key in value) len++;
for (var key in value) notify(key, value[key], ++i == len, indent, true);
draw.push(tab + '}' + (isLast ? line : ',' + line)); /*缩进'}'换行,若非尾元素则添加逗号*/
} else {
if (typeof value == 'string') value = '"' + value + '"';
draw.push(tab + (formObj ? '"' + name + '": ' : '') + value + (isLast ? '' : ',') + line);
}
};
var isLast = true,
indent = 0;
notify('', data, isLast, indent, false);
return draw.join('');
}
/* let bytes = 0;
* 年(Y) 可用1-4个占位符
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符 switch (unit.toUpperCase()) {
* 星期(W) 可用1-3个占位符 case 'KB':
* 季度(q为阿拉伯数字Q为中文数字)可用1或4个占位符 bytes = value * 1024;
* break;
* let date = new Date() case 'MB':
* formatDate(date, "YYYY-mm-dd HH:MM:SS") // 2020-02-09 14:04:23 bytes = value * 1024 * 1024;
* formatDate(date, "YYYY-mm-dd HH:MM:SS Q") // 2020-02-09 14:09:03 一 break;
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW") // 2020-02-09 14:45:12 星期日 case 'GB':
* formatDate(date, "YYYY-mm-dd HH:MM:SS QQQQ") // 2020-02-09 14:09:36 第一季度 bytes = value * 1024 * 1024 * 1024;
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-02-09 14:46:12 星期日 第一季度 break;
*/ default:
export function formatDate(date: Date, format: string) { throw new Error('Invalid size unit');
let we = date.getDay(); // 星期
let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
const opt: any = {
'Y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始要+1)
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
'S+': date.getSeconds().toString(), // 秒
'q+': qut, // 季度
};
// 中文数字 (星期)
const week: any = {
'0': '日',
'1': '一',
'2': '二',
'3': '三',
'4': '四',
'5': '五',
'6': '六',
};
// 中文数字(季度)
const quarter: any = {
'1': '一',
'2': '二',
'3': '三',
'4': '四',
};
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
for (let k in opt) {
let r = new RegExp('(' + k + ')').exec(format);
// 若输入的长度不为1则前面补零
if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
} }
return format;
return bytes;
} }
/** /**
* 10秒 10 * 1000 * 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
* 1分 60 * 1000
* 1小时 60 * 60 * 1000
* 24小时60 * 60 * 24 * 1000
* 3天 60 * 60* 24 * 1000 * 3
* *
* let data = new Date() * @param time 时间数
* formatPast(data) // 刚刚 * @param unit time对应的单位
* formatPast(data - 11 * 1000) // 11秒前 * @returns
* formatPast(data - 2 * 60 * 1000) // 2分钟前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 71 * 1000) // 2天前
* formatPast("2020-06-01") // 2020-06-01
* formatPast("2020-06-01", "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-06-01 08:00:00 星期一 第二季度
*/ */
export function formatPast(param: any, format: string = 'YYYY-mm-dd') { export function formatTime(time: number, unit: string = 's') {
// 传入格式处理、存储转换值 const units = {
let t: any, s: any; y: 31536000,
// 获取js 时间戳 M: 2592000,
let time: any = new Date().getTime(); d: 86400,
// 是否是对象 h: 3600,
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param); m: 60,
// 当前时间戳 - 传入时间戳 s: 1,
time = Number.parseInt(`${time - t}`); };
if (time < 10000) {
// 10秒内 if (!units[unit]) {
return '刚刚'; return 'Invalid unit';
} else if (time < 60000 && time >= 10000) {
// 超过10秒少于1分钟内
s = Math.floor(time / 1000);
return `${s}秒前`;
} else if (time < 3600000 && time >= 60000) {
// 超过1分钟少于1小时
s = Math.floor(time / 60000);
return `${s}分钟前`;
} else if (time < 86400000 && time >= 3600000) {
// 超过1小时少于24小时
s = Math.floor(time / 3600000);
return `${s}小时前`;
} else if (time < 259200000 && time >= 86400000) {
// 超过1天少于3天内
s = Math.floor(time / 86400000);
return `${s}天前`;
} else {
// 超过3天
let date = typeof param === 'string' || 'object' ? new Date(param) : param;
return formatDate(date, format);
} }
let seconds = time * units[unit];
let result = '';
const timeUnits = Object.entries(units).map(([unit, duration]) => {
const value = Math.floor(seconds / duration);
seconds %= duration;
return { value, unit };
});
timeUnits.forEach(({ value, unit }) => {
if (value > 0) {
result += `${value}${unit} `;
}
});
return result;
} }
/** /**

View File

@@ -40,3 +40,7 @@ export const NextLoading = {
}); });
}, },
}; };
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,33 @@
/**
* 根据对象访问路径,获取对应的值
*
* @param obj 对象,如 {user: {name: 'xxx'}, orderNo: 1212211, products: [{id: 12}]}
* @param path 访问路径,如 orderNo 或者 user.name 或者product[0].id
* @returns 路径对应的值
*/
export function getValueByPath(obj: any, path: string) {
const keys = path.split('.');
let result = obj;
for (let key of keys) {
if (!result || typeof result !== 'object') {
return undefined;
}
if (key.includes('[') && key.includes(']')) {
// 处理包含数组索引的情况
const arrayKey = key.substring(0, key.indexOf('['));
const matchIndex = key.match(/\[(.*?)\]/);
if (!matchIndex) {
return undefined;
}
const index = parseInt(matchIndex[1]);
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
} else {
result = result[key];
}
}
return result;
}

View File

@@ -1,6 +1,7 @@
import { randomUuid } from './string'; import { randomUuid } from './string';
const TokenKey = 'm-token'; const TokenKey = 'm-token';
const RefreshTokenKey = 'm-refresh-token';
const UserKey = 'm-user'; const UserKey = 'm-user';
const TagViewsKey = 'm-tagViews'; const TagViewsKey = 'm-tagViews';
const ClientIdKey = 'm-clientId'; const ClientIdKey = 'm-clientId';
@@ -15,6 +16,14 @@ export function saveToken(token: string) {
setLocal(TokenKey, token); setLocal(TokenKey, token);
} }
export function getRefreshToken(): string {
return getLocal(RefreshTokenKey);
}
export function saveRefreshToken(refreshToken: string) {
return setLocal(RefreshTokenKey, refreshToken);
}
// 获取登录用户基础信息 // 获取登录用户基础信息
export function getUser() { export function getUser() {
return getLocal(UserKey); return getLocal(UserKey);
@@ -33,10 +42,13 @@ export function getThemeConfig() {
return getLocal('themeConfig'); return getLocal('themeConfig');
} }
// 清除用户相关的用户信息 /**
* 清除当前登录用户相关信息
*/
export function clearUser() { export function clearUser() {
removeLocal(TokenKey); removeLocal(TokenKey);
removeLocal(UserKey); removeLocal(UserKey);
removeLocal(RefreshTokenKey);
} }
export function getTagViews() { export function getTagViews() {

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

@@ -34,8 +34,8 @@
<script setup lang="ts" name="layoutTagsViewContextmenu"> <script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue'; import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { ContextmenuItem } from './index'; import { ContextmenuItem } from './index';
import { useViewport } from '@/common/use';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { useWindowSize } from '@vueuse/core';
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
@@ -57,7 +57,7 @@ const props = defineProps({
// 定义子组件向父组件传值/事件 // 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']); const emit = defineEmits(['currentContextmenuClick']);
const { vw, vh } = useViewport(); const { width: vw, height: vh } = useWindowSize();
// 定义变量内容 // 定义变量内容
const state = reactive({ const state = reactive({
@@ -184,6 +184,10 @@ defineExpose({
z-index: 2190; z-index: 2190;
position: fixed; position: fixed;
.el-dropdown-menu__item {
padding: 5px 12px;
}
.el-dropdown-menu__item { .el-dropdown-menu__item {
font-size: 12px !important; font-size: 12px !important;
white-space: nowrap; white-space: nowrap;

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

@@ -0,0 +1,33 @@
<template>
<el-page-header @back="props.back">
<template #content>
<span>{{ header }}</span>
<span v-if="resource && !hideResource">
-
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
</el-tooltip>
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
</span>
<el-divider v-if="slots.buttons" direction="vertical" />
<slot v-if="slots.buttons" name="buttons"></slot>
</template>
<template #extra>
<slot v-if="slots.extra" name="extra"></slot>
</template>
</el-page-header>
</template>
<script lang="ts" setup>
import { useSlots } from 'vue';
const slots = useSlots();
defineOptions({ name: 'DrawerHeader' });
const props = defineProps({
header: String,
back: Function,
resource: String,
hideResource: Boolean,
});
</script>

View File

@@ -1,212 +1,39 @@
<template> <template>
<div class="dynamic-form"> <div class="dynamic-form">
<el-form <el-form v-bind="$attrs" ref="formRef" :model="modelValue" label-width="auto">
:model="form" <el-form-item v-for="item in props.formItems as any" :key="item.name" :prop="item.model" :label="item.name" :required="item.required ?? true">
ref="dynamicForm" <el-input v-if="!item.options" v-model="modelValue[item.model]" :placeholder="item.placeholder" autocomplete="off" clearable></el-input>
: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
@change="item.change ? item.change(form) : ''" <el-select v-else v-model="modelValue[item.model]" :placeholder="item.placeholder" filterable autocomplete="off" clearable style="width: 100%">
></el-input> <el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
<!-- 普通文本信息可用于不可修改字段等 --> </el-form-item>
<span v-else-if="item.type === 'text'">{{ form[item.name] }}</span> </el-form>
</div>
<!-- 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>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue'; import { ref } from 'vue';
import { ElMessage } from 'element-plus';
export default defineComponent({ const props = defineProps({
name: 'DynamicForm', formItems: { type: Array },
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,
};
},
}); });
// @Component({
// name: 'DynamicForm'
// })
// export default class DynamicForm extends Vue {
// @Prop()
// formInfo: object
// @Prop()
// formData: [object,boolean]|undefined
// form = {} const formRef: any = ref();
// submitDisabled = false
// @Watch('formData', { deep: true }) const modelValue: any = defineModel();
// onRoleChange() {
// if (this.formData) {
// this.form = { ...this.formData }
// }
// }
// submit() { const validate = async (func: any) => {
// const dynamicForm: any = this.$refs['dynamicForm'] await formRef.value.validate(func);
// 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
// }
// })
// }
// reset() { const resetFields = () => {
// this.$emit('reset') formRef.value.resetFields();
// this.resetFieldsAndData() };
// }
// /** defineExpose({
// * 重置表单以及表单数据 validate,
// */ resetFields,
// resetFieldsAndData() { });
// // 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
// const df: any = this.$refs['dynamicForm']
// df.resetFields()
// // 重置表单数据
// this.form = {}
// }
// mounted() {
// // 组件可能还没有初始化第一次初始化的时候无法watch对象
// this.form = { ...this.formData }
// }
// }
</script> </script>
<style lang="scss"></style>

View File

@@ -1,60 +1,52 @@
<template> <template>
<div class="form-dialog"> <div class="form-dialog">
<el-dialog :title="title" v-model="visible" :width="dialogWidth ? dialogWidth : '500px'"> <el-dialog @close="close" v-bind="$attrs" :title="title" v-model="dialogVisible" :width="width">
<dynamic-form ref="df" :form-info="formInfo" :form-data="formData" @submitSuccess="submitSuccess"> <dynamic-form ref="df" :form-items="props.formItems" v-model="formData" />
<template #btns="props">
<slot name="btns"> <template #footer>
<el-button :disabled="props.submitDisabled" type="primary" @click="props.submit" size="small"> </el-button> <span>
<el-button :disabled="props.submitDisabled" @click="close()" size="small"> </el-button> <slot name="btns">
</slot> <el-button @click="dialogVisible = false"> </el-button>
</template> <el-button type="primary" @click="confirm"> </el-button>
</dynamic-form> </slot>
</el-dialog> </span>
</div> </template>
</el-dialog>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue'; import { ref } from 'vue';
import DynamicForm from './DynamicForm.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 emit = defineEmits(['close', 'confirm']);
const df: any = ref();
const close = () => { const props = defineProps({
// 更新父组件visible prop对应的值为false title: { type: String },
context.emit('update:visible', false); width: { type: [String, Number], default: '500px' },
// 关闭窗口则将表单数据置为null formItems: { type: Array },
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 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> </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 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

@@ -40,7 +40,7 @@ onMounted(() => {
}); });
const convert = (value: any) => { const convert = (value: any) => {
const enumValue = EnumValue.getEnumByValue(Object.values(props.enums as any) as any, value) as any; const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
if (!enumValue) { if (!enumValue) {
state.enumLabel = '-'; state.enumLabel = '-';
state.type = 'danger'; state.type = 'danger';

View File

@@ -1,7 +1,7 @@
<template> <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> <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-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select> </el-select>
</div> </div>
@@ -294,17 +294,19 @@ const registeShell = () => {
}; };
const format = () => { const format = () => {
/* // 触发自动格式化;
触发自动格式化;
*/
monacoEditorIns.trigger('', 'editor.action.formatDocument', ''); monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
}; };
const focus = () => {
monacoEditorIns.focus();
};
const getEditor = () => { const getEditor = () => {
return monacoEditorIns; return monacoEditorIns;
}; };
defineExpose({ getEditor, format }); defineExpose({ getEditor, format, focus });
</script> </script>
<style lang="scss"> <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?.focus();
editorRef.value?.format();
}, 300);
state.dialogVisible = true;
};
defineExpose({ open });
</script>
<style lang="scss" scoped>
.editor {
font-size: 9pt;
font-weight: 600;
}
</style>

View File

@@ -1,437 +1,467 @@
<template> <template>
<div class="page-table"> <div>
<!-- <transition name="el-zoom-in-top">
实现通过我们配置好的 查询条件 <!-- 查询表单 -->
首先去创建form表单根据我们配置的查询条件去做一个循环判断展示出不用类型所对应不同的输入框 <SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
比如text对应普通的输入框select对应下拉选择dateTime对应日期时间选择器 <!-- 遍历父组件传入的 solts 透传给子组件 -->
在使用时父组件会传来一个queryForm空的对象 <template v-for="(_, key) in useSlots()" v-slot:[key]>
循环出来的输入框会绑定表格配置中的prop字段绑定在queryForm对象中 <slot :name="key"></slot>
--> </template>
<el-card> </SearchForm>
<div class="query" ref="queryRef"> </transition>
<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>
<el-select-v2 <div class="card">
v-model="queryForm[item.prop]" <div class="table-main">
:options="item.options" <!-- 表格头部 操作按钮 -->
clearable <div class="table-header">
:placeholder="'选择' + item.label + '关键字'" <div class="header-button-lf">
v-else-if="item.type == 'select'" <slot name="tableHeader" />
/> </div>
<el-date-picker <div v-if="toolButton" class="header-button-ri">
v-model="queryForm[item.prop]" <slot name="toolButton">
clearable <div class="tool-button">
type="datetimerange" <!-- 简易单个搜索项 -->
format="YYYY-MM-DD hh:mm:ss" <div v-if="nowSearchItem" class="simple-search-form">
value-format="x" <el-dropdown v-if="searchItems?.length > 1">
range-separator="" <SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
start-placeholder="开始时间" <template #dropdown>
end-placeholder="结束时间" <el-dropdown-menu>
v-else-if="item.type == 'date'" <el-dropdown-item
/> v-for="searchItem in searchItems"
:key="searchItem.prop"
<template v-else-if="item.slot == 'queryBtns'"> @click="changeSimpleFormItem(searchItem)"
<template v-if="props.query?.length > defaultQueryCount"> >
<el-button {{ searchItem.label }}
@click="isOpenMoreQuery = !isOpenMoreQuery" </el-dropdown-item>
v-if="!isOpenMoreQuery" </el-dropdown-menu>
icon="ArrowDownBold"
circle
></el-button>
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-else icon="ArrowUpBold" circle></el-button>
</template> </template>
</el-dropdown>
<el-button @click="queryData()" type="primary" icon="search" plain>查询</el-button> <div class="simple-search-form-label mt5">
<el-button @click="reset()" icon="RefreshRight">重置</el-button> <el-text truncated tag="b">{{ `${nowSearchItem?.label} : ` }}</el-text>
</template> </div>
<slot :name="item.slot"></slot> <el-form-item style="width: 200px" :key="nowSearchItem.prop">
</el-form-item> <SearchFormItem
</el-row> @keyup.enter.native="searchFormItemKeyUpEnter"
</el-form> 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-value="true" :false-value="false" />
</div>
<template #reference>
<el-button icon="Operation" circle :size="props.size"></el-button>
</template>
</el-popover>
</div>
</div>
</slot>
</div> </div>
</div> </div>
<div class="slot"> <el-table
<!-- 查询栏右侧slot插槽用来添加表格其他操作比如新增数据删除数据等其他操作 --> ref="tableRef"
<slot name="queryRight"></slot> 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" />
<!-- <template v-for="(item, index) in tableColumns">
动态表头显示根据表格每条配置项中的show字段来决定改列是否显示或者隐藏 <el-table-column
columns 就是我们表格配置的数组对象 :key="index"
--> v-if="item.show"
<el-popover :prop="item.prop"
placement="bottom" :label="item.label"
title="表格配置" :fixed="item.fixed"
popper-style="max-height: 550px; overflow: auto; max-width: 450px" :align="item.align"
width="auto" :show-overflow-tooltip="item.showOverflowTooltip"
trigger="click" :min-width="item.minWidth"
> :sortable="item.sortable || false"
<div v-for="(item, index) in props.columns" :key="index"> :type="item.type"
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" /> :width="item.width"
</div> >
<template #reference> <!-- 插槽预留功能 -->
<!-- 一个Element Plus中的图标 --> <template #default="scope" v-if="item.slot">
<el-button icon="Operation" :size="props.size"></el-button> <slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
</template> </template>
</el-popover>
</div> <!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover
v-if="item.isBeautify && item.getValueByData(scope.row)?.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(item.getValueByData(scope.row))"
: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> </div>
<el-table <el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
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-pagination <el-pagination
:small="props.size == 'small'" :small="props.size == 'small'"
@current-change="handlePageChange" @current-change="handlePageNumChange"
@size-change="handleSizeChange" @size-change="handlePageSizeChange"
style="text-align: right" style="text-align: right"
layout="prev, pager, next, total, sizes, jumper" layout="prev, pager, next, total, sizes"
:total="props.total" :total="total"
v-model:current-page="state.pageNum" v-model:current-page="queryForm.pageNum"
v-model:page-size="state.pageSize" v-model:page-size="queryForm.pageSize"
:page-sizes="pageSizes" :page-sizes="pageSizes"
/> />
</el-row> </el-row>
</el-card> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue'; import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
import { TableColumn, TableQuery } from './index'; import { TableColumn } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue'; 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:selectionData', 'pageChange']);
const props = defineProps({ export interface PageTableProps {
size: { size?: string;
type: String, pageApi?: Api; // 请求表格数据的 api
default: '', 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, // table 实例
}, const tableRef = ref<InstanceType<typeof ElTable>>();
// 是否显示选择列
showSelection: { // 接收 columns 并设置为响应式
type: Boolean, const tableColumns = reactive<TableColumn[]>(props.columns);
default: false,
}, // 接收 searchItems 并设置为响应式
// 当前选择的数据 const tableSearchItems = reactive<SearchItem[]>(props.searchItems);
selectionData: {
type: Array<any> const { themeConfig } = storeToRefs(useThemeConfig());
},
// 列信息 // 是否显示搜索模块
columns: { const isShowSearch = ref(props.showSearch);
type: Array<TableColumn>,
default: function () { // 控制 ToolButton 显示
return []; const showToolButton = (key: 'setting' | 'search') => {
}, return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
required: true, };
},
// 表格数据 const nowSearchItem: Ref<SearchItem> = ref(null) as any;
data: {
type: Array, /**
required: true, * 改变当前的搜索项
}, * @param searchItem 当前点击的搜索项
total: { */
type: [Number], const changeSimpleFormItem = (searchItem: SearchItem) => {
default: 0, // 将之前的值置为空,避免因为只显示一个搜索项却搜索多个条件
}, queryForm.value[nowSearchItem.value.prop] = null;
pageNum: { nowSearchItem.value = searchItem;
type: Number, };
default: 1,
}, let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
pageSize: { props.pageable,
type: [Number], props.pageApi,
default: 10, queryForm,
}, props.beforeQueryFn,
// 查询条件配置 props.dataHandlerFn
query: { );
type: Array<TableQuery>,
default: function () {
return [];
}
},
// 绑定的查询表单
queryForm: {
type: Object,
default: function () {
return {};
}
},
})
const state = reactive({ const state = reactive({
pageSizes: [] as any, // 可选每页显示的数据量 pageSizes: [] as any, // 可选每页显示的数据量
pageSize: 10,
pageNum: 1,
isOpenMoreQuery: false,
defaultQueryCount: 2, // 默认显示的查询参数个数展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
queryForm: {} as any,
loadingData: false,
// 输入框宽度 // 输入框宽度
inputWidth: "200px" as any,
formatVal: '', // 格式化后的值 formatVal: '', // 格式化后的值
tableMaxHeight: window.innerHeight - 240 + 'px', tableMaxHeight: '500px',
}) });
const { const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
pageSizes,
isOpenMoreQuery,
defaultQueryCount,
queryForm,
loadingData,
inputWidth,
formatVal,
tableMaxHeight,
} = toRefs(state)
watch(() => props.queryForm, (newValue: any) => { watch(tableData, (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) => {
if (newValue && newValue.length > 0) { if (newValue && newValue.length > 0) {
props.columns.forEach(item => { props.columns.forEach((item) => {
if (item.autoWidth && item.show) { if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(props.data); item.autoCalculateMinWidth(tableData.value);
} }
}) });
} }
}) });
onMounted(() => { watch(isShowSearch, () => {
const pageSize = props.pageSize; calcuTableHeight();
});
state.pageNum = props.pageNum; watch(
state.pageSize = pageSize; () => props.data,
state.queryForm = props.queryForm; (newValue: any) => {
tableData = newValue;
}
);
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]; state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
// 如果没传输入框宽度则根据组件size设置默认宽度 if (!props.lazy) {
if (!props.inputWidth) { await getTableData();
state.inputWidth = props.size == 'small' ? '150px' : '200px';
} else {
state.inputWidth = props.inputWidth;
} }
});
window.addEventListener('resize', () => {
calcuTableHeight();
});
})
const 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 = ''; state.formatVal = '';
try { try {
state.formatVal = JSON.stringify(JSON.parse(data), null, 4); state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) { } catch (e) {
state.formatVal = data; 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) => { const handleSelectionChange = (val: any) => {
emit('update:selectionData', val); emit('update:selectionData', val);
} };
const handlePageChange = () => { const getData = () => {
emit('update:pageNum', state.pageNum); return toValue(tableData);
execQuery(); };
}
const handleSizeChange = () => { defineExpose({
changePageNum(1); tableRef: tableRef,
emit('update:pageSize', state.pageSize); search: getTableData,
execQuery(); getData,
} });
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 })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.page-table { .table-box,
.query { .table-main {
margin-bottom: 10px; display: flex;
overflow: hidden; flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
.is-open { // 表格 header 样式
// padding: 10px 0; .table-header {
max-height: 200px; width: 100%;
margin-top: 10px; .header-button-lf {
float: left;
} }
display: flex; .header-button-ri {
align-items: flex-start; float: right;
justify-content: space-between;
.slot { .tool-button {
display: flex; display: flex;
justify-content: flex-end; 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 { // el-table 表格样式
margin-top: 10px; .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) { ::v-deep(.el-form-item__label) {
font-weight: bold; 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;
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { getValueByPath } from '@/common/utils/object';
import { getTextWidth } from '@/common/utils/string'; import { getTextWidth } from '@/common/utils/string';
export class TableColumn { export class TableColumn {
@@ -29,10 +30,15 @@ export class TableColumn {
minWidth: number | string; minWidth: number | string;
/** /**
* 是否插槽,是的话插槽名为prop属性名 * 是否插槽,若slotName为空则插槽名为prop属性名
*/ */
slot: boolean = false; slot: boolean = false;
/**
* 插槽名,
*/
slotName: string = '';
showOverflowTooltip: boolean = true; showOverflowTooltip: boolean = true;
sortable: boolean = false; sortable: boolean = false;
@@ -87,7 +93,7 @@ export class TableColumn {
if (this.formatFunc) { if (this.formatFunc) {
return this.formatFunc(rowData, this.prop); return this.formatFunc(rowData, this.prop);
} }
return rowData[this.prop]; return getValueByPath(rowData, this.prop);
} }
static new(prop: string, label: string): TableColumn { static new(prop: string, label: string): TableColumn {
@@ -121,7 +127,7 @@ export class TableColumn {
/** /**
* 使用标签类型展示该列(用于枚举值友好展示) * 使用标签类型展示该列(用于枚举值友好展示)
* @param param 枚举对象 * @param param 枚举对象, 如AccountStatusEnum
* @returns this * @returns this
*/ */
typeTag(param: any): TableColumn { typeTag(param: any): TableColumn {
@@ -144,8 +150,9 @@ export class TableColumn {
* 标识该列为插槽 * 标识该列为插槽
* @returns this * @returns this
*/ */
isSlot(): TableColumn { isSlot(slotName: string = ''): TableColumn {
this.slot = true; this.slot = true;
this.slotName = slotName;
return this; return this;
} }
@@ -165,7 +172,7 @@ export class TableColumn {
*/ */
isTime(): TableColumn { isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => { this.setFormatFunc((data: any, prop: string) => {
return dateFormat(data[prop]); return formatDate(getValueByPath(data, prop));
}); });
return this; return this;
} }
@@ -176,7 +183,7 @@ export class TableColumn {
*/ */
isEnum(enums: any): TableColumn { isEnum(enums: any): TableColumn {
this.setFormatFunc((data: any, prop: string) => { this.setFormatFunc((data: any, prop: string) => {
return EnumValue.getLabelByValue(enums, data[prop]); return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
}); });
return this; return this;
} }
@@ -218,7 +225,7 @@ export class TableColumn {
// 获取该列中最长的数据(内容) // 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) { for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i]; let nowData = tableData[i];
let nowValue = nowData[prop]; let nowValue = getValueByPath(nowData, prop);
if (!nowValue) { if (!nowValue) {
continue; continue;
} }
@@ -242,68 +249,3 @@ export class TableColumn {
this.minWidth = (flexWidth > 400 ? 400 : flexWidth) + this.addWidth; 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,505 @@
<template>
<div>
<div ref="viewportRef" class="viewport" :style="{ width: state.size.width + 'px', height: state.size.height + 'px' }">
<div ref="displayRef" class="display" tabindex="0" />
<div class="btn-box">
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr10" title="剪贴板" />
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
<el-dropdown>
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65288'])"> Ctrl + Alt + Backspace </el-dropdown-item>
<el-dropdown-item @click="openSendKeyboard(['65515', '100'])"> Windows + D </el-dropdown-item>
<el-dropdown-item @click="openSendKeyboard(['65515', '101'])"> Windows + E </el-dropdown-item>
<el-dropdown-item @click="openSendKeyboard(['65515', '114'])"> Windows + R </el-dropdown-item>
<el-dropdown-item @click="openSendKeyboard(['65515'])"> Windows </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
</div>
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file
:machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName"
:protocol="state.filesystemDialog.protocol"
:file-id="state.filesystemDialog.fileId"
:path="state.filesystemDialog.path"
/>
</el-dialog>
</div>
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file
:machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName"
:protocol="state.filesystemDialog.protocol"
:file-id="state.filesystemDialog.fileId"
:path="state.filesystemDialog.path"
/>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import Guacamole from './guac/guacamole-common';
import { getMachineRdpSocketUrl } from '@/views/ops/machine/api';
import clipboard from './guac/clipboard';
import { reactive, ref } from 'vue';
import { TerminalStatus } from '@/components/terminal/common';
import ClipboardDialog from '@/components/terminal-rdp/guac/ClipboardDialog.vue';
import { TerminalExpose } from '@/components/terminal-rdp/index';
import SvgIcon from '@/components/svgIcon/index.vue';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
import { useEventListener } from '@vueuse/core';
import { debounce } from 'lodash';
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
import { ElMessage } from 'element-plus';
import { joinClientParams } from '@/common/request';
const viewportRef = ref({} as any);
const displayRef = ref({} as any);
const clipboardRef = ref({} as any);
const props = defineProps({
machineId: {
type: Number,
required: true,
},
authCert: {
type: String,
required: true,
},
clipboardList: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['statusChange']);
const state = reactive({
client: null as any,
display: null as any,
displayElm: {} as any,
clipboard: {} as any,
keyboard: {} as any,
mouse: null as any,
touchpad: null as any,
errorMessage: '',
arguments: {},
status: TerminalStatus.NoConnected,
size: {
height: 710,
width: 1024,
force: false,
},
enableClipboard: true,
clipboardDialog: {
visible: false,
},
filesystemDialog: {
visible: false,
authCertName: '',
machineId: 0,
protocol: 1,
title: '',
fileId: 0,
path: '',
},
fullscreen: false,
beforeFullSize: {
height: 710,
width: 1024,
},
});
const installKeyboard = () => {
state.keyboard = new Guacamole.Keyboard(state.displayElm);
uninstallKeyboard();
state.keyboard.onkeydown = (keysym: any) => {
state.client.sendKeyEvent(1, keysym);
};
state.keyboard.onkeyup = (keysym: any) => {
state.client.sendKeyEvent(0, keysym);
};
};
const uninstallKeyboard = () => {
state.keyboard!.onkeydown = state.keyboard!.onkeyup = () => {};
};
const installMouse = () => {
state.mouse = new Guacamole.Mouse(state.displayElm);
// Hide software cursor when mouse leaves display
state.mouse.onmouseout = () => {
if (!state.display) return;
state.display.showCursor(false);
};
state.mouse.onmousedown = state.mouse.onmouseup = state.mouse.onmousemove = handleMouseState;
};
const installTouchpad = () => {
state.touchpad = new Guacamole.Mouse.Touchpad(state.displayElm);
state.touchpad.onmousedown =
state.touchpad.onmouseup =
state.touchpad.onmousemove =
(st: any) => {
// 记录按下时,光标所在位置
console.log(st);
handleMouseState(st, true);
};
// 记录单指按压时候手在屏幕的位置
state.displayElm.ontouchend = (event: TouchEvent) => {
console.log('end', event);
state.displayElm.ontouchend = () => {};
};
};
const setClipboard = (data: string) => {
clipboardRef.value.setValue(data);
};
const installClipboard = () => {
state.enableClipboard = clipboard.install(state.client) as any;
clipboard.installWatcher(props.clipboardList, setClipboard);
state.client.onclipboard = clipboard.onClipboard;
};
const installResize = () => {
// 在resize事件结束后300毫秒执行
useEventListener('resize', debounce(resize, 300));
};
const installDisplay = () => {
let { width, height, force } = state.size;
state.display = state.client.getDisplay();
const displayElm = displayRef.value;
displayElm.appendChild(state.display.getElement());
displayElm.addEventListener('contextmenu', (e: any) => {
e.stopPropagation();
if (e.preventDefault) {
e.preventDefault();
}
e.returnValue = false;
});
state.client.connect('width=' + width + '&height=' + height + '&force=' + force + '&' + joinClientParams());
window.onunload = () => state.client.disconnect();
// allows focusing on the display div so that keyboard doesn't always go to session
displayElm.onclick = () => {
displayElm.focus();
};
displayElm.onfocus = () => {
displayElm.className = 'focus';
};
displayElm.onblur = () => {
displayElm.className = '';
};
state.displayElm = displayElm;
};
const installClient = () => {
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.authCert)) as any;
if (state.client) {
state.display?.scale(0);
uninstallKeyboard();
state.client.disconnect();
}
state.client = new Guacamole.Client(tunnel);
tunnel.onerror = (status: any) => {
// eslint-disable-next-line no-console
console.error(`Tunnel failed ${JSON.stringify(status)}`);
// state.connectionState = states.TUNNEL_ERROR;
};
tunnel.onstatechange = (st: any) => {
console.log('statechange', st);
state.status = st;
switch (st) {
case TunnelState.CONNECTING: // 'CONNECTING'
break;
case TunnelState.OPEN: // 'OPEN'
state.status = TerminalStatus.Connected;
emit('statusChange', TerminalStatus.Connected);
break;
case TunnelState.CLOSED: // 'CLOSED'
state.status = TerminalStatus.Disconnected;
emit('statusChange', TerminalStatus.Disconnected);
break;
case TunnelState.UNSTABLE: // 'UNSTABLE'
state.status = TerminalStatus.Error;
emit('statusChange', TerminalStatus.Error);
break;
}
};
state.client.onstatechange = (clientState: any) => {
console.log('clientState', clientState);
switch (clientState) {
case ClientState.IDLE:
console.log('连接空闲');
break;
case ClientState.CONNECTING:
console.log('连接中...');
break;
case ClientState.WAITING:
console.log('等待服务器响应...');
break;
case ClientState.CONNECTED:
console.log('连接成功...');
break;
// eslint-disable-next-line no-fallthrough
case ClientState.DISCONNECTING:
console.log('断开连接中...');
break;
case ClientState.DISCONNECTED:
console.log('已断开连接...');
break;
}
};
state.client.onerror = (error: any) => {
state.client.disconnect();
console.error(`Client error ${JSON.stringify(error)}`);
state.errorMessage = error.message;
// state.connectionState = states.CLIENT_ERROR;
};
state.client.onsync = () => {};
state.client.onargv = (stream: any, mimetype: any, name: any) => {
if (mimetype !== 'text/plain') return;
const reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
let value = '';
reader.ontext = (text: any) => {
value += text;
};
// Test mutability once stream is finished, storing the current value for the argument only if it is mutable
reader.onend = () => {
const stream = state.client.createArgumentValueStream('text/plain', name);
stream.onack = (status: any) => {
if (status.isError()) {
// ignore reject
return;
}
state.arguments[name] = value;
};
};
};
};
const resize = () => {
const elm = viewportRef.value;
if (!elm || !elm.offsetWidth) {
// resize is being called on the hidden window
return;
}
let box = elm.parentElement;
state.size.width = box.clientWidth;
state.size.height = box.clientHeight;
const width = parseInt(String(box.clientWidth));
const height = parseInt(String(box.clientHeight));
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
if (state.status !== TerminalStatus.Connected) {
connect(width, height);
} else {
state.client.sendSize(width, height);
}
}
// setting timeout so display has time to get the correct size
// setTimeout(() => {
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
// state.display.scale(scale);
// console.log(state.size, scale);
// }, 100);
};
const handleMouseState = (mouseState: any, showCursor = false) => {
state.client.getDisplay().showCursor(showCursor);
const scaledMouseState = Object.assign({}, mouseState, {
x: mouseState.x / state.display.getScale(),
y: mouseState.y / state.display.getScale(),
});
state.client.sendMouseState(scaledMouseState);
};
const connect = (width: number, height: number, force = false) => {
if (!width && !height) {
if (state.size && state.size.width && state.size.height) {
width = state.size.width;
height = state.size.height;
} else {
// 获取当前viewportRef宽高
width = viewportRef.value.clientWidth;
height = viewportRef.value.clientHeight;
}
}
state.size = { width, height, force };
installClient();
installDisplay();
installKeyboard();
installMouse();
installTouchpad();
installClipboard();
installResize();
};
const disconnect = () => {
uninstallKeyboard();
state.client?.disconnect();
};
const blur = () => {
uninstallKeyboard();
};
const focus = () => {};
const openPaste = async () => {
state.clipboardDialog.visible = true;
};
const closePaste = async () => {
installKeyboard();
};
const onsubmitClipboard = (val: string) => {
state.clipboardDialog.visible = false;
installKeyboard();
clipboard.sendRemoteClipboard(state.client, val);
};
const openFilesystem = async () => {
state.filesystemDialog.protocol = 2;
state.filesystemDialog.machineId = props.machineId;
state.filesystemDialog.authCertName = props.authCert;
state.filesystemDialog.fileId = props.machineId;
state.filesystemDialog.path = '/';
state.filesystemDialog.title = `远程桌面文件管理`;
state.filesystemDialog.visible = true;
};
const openFullScreen = function () {
launchIntoFullscreen(viewportRef.value);
state.fullscreen = true;
// 记录原始尺寸
state.beforeFullSize = {
width: state.size.width,
height: state.size.height,
};
// 使用新的宽高重新连接
setTimeout(() => {
connect(viewportRef.value.clientWidth, viewportRef.value.clientHeight, false);
}, 500);
watchFullscreenChange(watchFullscreen);
};
function watchFullscreen(event: Event, isFull: boolean) {
if (!isFull) {
closeFullScreen();
}
}
const closeFullScreen = function () {
exitFullscreen();
state.fullscreen = false;
// 使用新的宽高重新连接
setTimeout(() => {
connect(state.beforeFullSize.width, state.beforeFullSize.height, false);
}, 500);
// 取消注册esc事件退出全屏
unWatchFullscreenChange(watchFullscreen);
};
const openSendKeyboard = (keys: string[]) => {
if (!state.client) {
return;
}
for (let i = 0; i < keys.length; i++) {
state.client.sendKeyEvent(1, keys[i]);
}
for (let j = 0; j < keys.length; j++) {
state.client.sendKeyEvent(0, keys[j]);
}
ElMessage.success('发送组合键成功');
};
const exposes = {
connect,
disconnect,
init: connect,
close: disconnect,
fitTerminal: resize,
focus,
blur,
setRemoteClipboard: onsubmitClipboard,
} as TerminalExpose;
defineExpose(exposes);
</script>
<style lang="scss">
.viewport {
position: relative;
width: 1024px;
min-height: 710px;
z-index: 1;
}
.display {
overflow: hidden;
width: 100%;
height: 100%;
}
.btn-box {
position: absolute;
top: 20px;
right: 30px;
padding: 5px 0 5px 10px;
background: #dddddd4a;
color: #fff;
border-radius: 3px;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div class="rdpDialog" ref="dialogRef">
<el-dialog
v-model="dialogVisible"
:before-close="handleClose"
:close-on-click-modal="false"
:destroy-on-close="true"
:close-on-press-escape="false"
:show-close="false"
width="1024"
@open="connect()"
>
<template #header>
<div class="terminal-title-wrapper">
<!-- 左侧 -->
<div class="title-left-fixed">
<!-- title信息 -->
<div>
{{ title }}
</div>
</div>
<!-- 右侧 -->
<div class="title-right-fixed">
<el-popconfirm @confirm="connect(true)" title="确认重新连接?">
<template #reference>
<div class="mr10 pointer">
<el-tag v-if="state.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
<el-tag v-else type="danger" effect="light" round> 未连接点击重连 </el-tag>
</div>
</template>
</el-popconfirm>
<el-popconfirm @confirm="handleClose" title="确认关闭?">
<template #reference>
<SvgIcon name="Close" class="pointer-icon" title="关闭" :size="20" />
</template>
</el-popconfirm>
</div>
</div>
</template>
<machine-rdp ref="rdpRef" :machine-id="machineId" :auth-cert="authCert" @status-change="handleStatusChange" />
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
import { TerminalStatus } from '@/components/terminal/common';
import SvgIcon from '@/components/svgIcon/index.vue';
const rdpRef = ref({} as any);
const dialogRef = ref({} as any);
const props = defineProps({
visible: { type: Boolean },
machineId: {
type: Number,
required: true,
},
authCert: {
type: String,
required: true,
},
title: { type: String },
});
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const state = reactive({
dialogVisible: false,
title: '',
status: TerminalStatus.NoConnected,
});
const { dialogVisible } = toRefs(state);
watch(props, async (newValue: any) => {
const visible = newValue.visible;
state.dialogVisible = visible;
if (visible) {
state.title = newValue.title;
}
});
const connect = (force = false) => {
rdpRef.value?.disconnect();
let width = 1024;
let height = 710;
rdpRef.value?.connect(width, height, force);
};
const handleStatusChange = (status: TerminalStatus) => {
state.status = status;
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
emit('update:visible', false);
emit('update:machineId', null);
emit('cancel');
rdpRef.value?.disconnect();
};
</script>
<style lang="scss">
.rdpDialog {
.el-dialog {
padding: 0;
.el-dialog__header {
padding: 10px;
}
}
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
max-height: 100% !important;
padding: 0 !important;
}
.terminal-title-wrapper {
display: flex;
justify-content: space-between;
font-size: 16px;
.title-right-fixed {
display: flex;
align-items: center;
font-size: 20px;
text-align: end;
}
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="clipboard-dialog">
<el-dialog
v-model="dialogVisible"
title="请输入需要粘贴的文本"
:before-close="onclose"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="600"
>
<el-input v-model="state.modelValue" type="textarea" :rows="20" />
<template #footer>
<el-button type="primary" @click="onsubmit"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
visible: { type: Boolean },
});
const emits = defineEmits(['submit', 'close', 'update:visible']);
const state = reactive({
dialogVisible: false,
modelValue: '',
});
const { dialogVisible } = toRefs(state);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
});
const onclose = () => {
emits('update:visible', false);
emits('close');
};
const onsubmit = () => {
state.dialogVisible = false;
if (state.modelValue) {
ElMessage.success('发送剪贴板数据成功');
emits('submit', state.modelValue);
} else {
ElMessage.warning('请输入需要粘贴的文本');
}
};
const setValue = (val: string) => {
state.modelValue = val;
};
defineExpose({ setValue });
</script>
<style lang="scss">
.clipboard-dialog {
}
</style>

View File

@@ -0,0 +1,147 @@
import Guacamole from './guacamole-common';
import { ElMessage } from 'element-plus';
const clipboard = {};
clipboard.install = (client) => {
if (!navigator.clipboard) {
return false;
}
clipboard.getLocalClipboard().then((data) => (clipboard.cache = data));
window.addEventListener('load', clipboard.update(client), true);
window.addEventListener('copy', clipboard.update(client));
window.addEventListener('cut', clipboard.update(client));
window.addEventListener(
'focus',
(e) => {
if (e.target === window) {
clipboard.update(client)();
}
},
true
);
return true;
};
clipboard.update = (client) => {
return () => {
clipboard.getLocalClipboard().then((data) => {
clipboard.cache = data;
clipboard.setRemoteClipboard(client);
});
};
};
clipboard.sendRemoteClipboard = (client, text) => {
clipboard.cache = {
type: 'text/plain',
data: text,
};
clipboard.setRemoteClipboard(client);
};
clipboard.setRemoteClipboard = (client) => {
if (!clipboard.cache) {
return;
}
let writer;
const stream = client.createClipboardStream(clipboard.cache.type);
if (typeof clipboard.cache.data === 'string') {
writer = new Guacamole.StringWriter(stream);
writer.sendText(clipboard.cache.data);
writer.sendEnd();
clipboard.appendClipboardList('up', clipboard.cache.data);
} else {
writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
writer.sendBlob(clipboard.cache.data);
}
};
clipboard.getLocalClipboard = async () => {
// 获取本地剪贴板数据
if (navigator.clipboard && navigator.clipboard.readText) {
const text = await navigator.clipboard.readText();
return {
type: 'text/plain',
data: text,
};
} else {
ElMessage.warning('只有https才可以访问剪贴板');
}
};
clipboard.setLocalClipboard = async (data) => {
if (data.type === 'text/plain') {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(data.data);
}
}
};
// 获取到远程服务器剪贴板变动
clipboard.onClipboard = (stream, mimetype) => {
let reader;
if (/^text\//.exec(mimetype)) {
reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
let data = '';
reader.ontext = (text) => {
data += text;
};
// Set clipboard contents once stream is finished
reader.onend = () => {
clipboard.setLocalClipboard({
type: mimetype,
data: data,
});
clipboard.setClipboardFn && typeof clipboard.setClipboardFn === 'function' && clipboard.setClipboardFn(data);
clipboard.appendClipboardList('down', data);
};
} else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
clipboard.setLocalClipboard({
type: mimetype,
data: reader.getBlob(),
});
};
}
};
/***
* 注册剪贴板监听器,如果有本地或远程剪贴板变动,则会更新剪贴板列表
*/
clipboard.installWatcher = (clipboardList, setClipboardFn) => {
clipboard.clipboardList = clipboardList;
clipboard.setClipboardFn = setClipboardFn;
};
clipboard.appendClipboardList = (src, data) => {
clipboard.clipboardList = clipboard.clipboardList || [];
// 循环判断是否重复
for (let i = 0; i < clipboard.clipboardList.length; i++) {
if (clipboard.clipboardList[i].data === data) {
return;
}
}
clipboard.clipboardList.push({ type: 'text/plain', data, src });
};
export default clipboard;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
export function launchIntoFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
export function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
export function watchFullscreenChange(callback) {
function onFullscreenChange(e) {
let isFull = (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) != null;
callback(e, isFull);
}
document.addEventListener('fullscreenchange', onFullscreenChange);
document.addEventListener('mozfullscreenchange', onFullscreenChange);
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
document.addEventListener('msfullscreenchange', onFullscreenChange);
}
export function unWatchFullscreenChange(callback) {
document.removeEventListener('fullscreenchange', callback);
document.removeEventListener('mozfullscreenchange', callback);
document.removeEventListener('webkitfullscreenchange', callback);
document.removeEventListener('msfullscreenchange', callback);
}

View File

@@ -0,0 +1,78 @@
export const ClientState = {
/**
* The client is idle, with no active connection.
*
* @type number
*/
IDLE: 0,
/**
* The client is in the process of establishing a connection.
*
* @type {!number}
*/
CONNECTING: 1,
/**
* The client is waiting on further information or a remote server to
* establish the connection.
*
* @type {!number}
*/
WAITING: 2,
/**
* The client is actively connected to a remote server.
*
* @type {!number}
*/
CONNECTED: 3,
/**
* The client is in the process of disconnecting from the remote server.
*
* @type {!number}
*/
DISCONNECTING: 4,
/**
* The client has completed the connection and is no longer connected.
*
* @type {!number}
*/
DISCONNECTED: 5,
};
export const TunnelState = {
/**
* A connection is in pending. It is not yet known whether connection was
* successful.
*
* @type {!number}
*/
CONNECTING: 0,
/**
* Connection was successful, and data is being received.
*
* @type {!number}
*/
OPEN: 1,
/**
* The connection is closed. Connection may not have been successful, the
* tunnel may have been explicitly closed by either side, or an error may
* have occurred.
*
* @type {!number}
*/
CLOSED: 2,
/**
* The connection is open, but communication through the tunnel appears to
* be disrupted, and the connection may close as a result.
*
* @type {!number}
*/
UNSTABLE: 3,
};

View File

@@ -0,0 +1,11 @@
export interface TerminalExpose {
/** 连接 */
init(width: number, height: number, force: boolean): void;
/** 短开连接 */
close(): void;
blur(): void;
focus(): void;
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }"> <div id="terminal-body" :style="{ height }">
<div ref="terminalRef" class="terminal" /> <div ref="terminalRef" class="terminal" />
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" /> <TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
@@ -8,7 +8,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
import { Terminal } from 'xterm'; import { Terminal, ITheme } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search'; import { SearchAddon } from 'xterm-addon-search';
import { WebLinksAddon } from 'xterm-addon-web-links'; import { WebLinksAddon } from 'xterm-addon-web-links';
@@ -19,8 +19,16 @@ import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue'
import TerminalSearch from './TerminalSearch.vue'; import TerminalSearch from './TerminalSearch.vue';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { TerminalStatus } from './common'; import { TerminalStatus } from './common';
import { useEventListener } from '@vueuse/core';
import themes from './themes';
import { TrzszFilter } from 'trzsz';
const props = defineProps({ const props = defineProps({
// mounted时是否执行init方法
mountInit: {
type: Boolean,
default: true,
},
/** /**
* 初始化执行命令 * 初始化执行命令
*/ */
@@ -63,9 +71,9 @@ const state = reactive({
}); });
onMounted(() => { onMounted(() => {
nextTick(() => { if (props.mountInit) {
init(); init();
}); }
}); });
watch( watch(
@@ -75,6 +83,14 @@ watch(
} }
); );
// 监听 themeConfig terminalTheme配置的变化
watch(
() => themeConfig.value.terminalTheme,
() => {
term.options.theme = getTerminalTheme();
}
);
onBeforeUnmount(() => { onBeforeUnmount(() => {
close(); close();
}); });
@@ -84,6 +100,12 @@ function init() {
console.log('重新连接...'); console.log('重新连接...');
close(); close();
} }
nextTick(() => {
initTerm();
});
}
function initTerm() {
term = new Terminal({ term = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15, fontSize: themeConfig.value.terminalFontSize || 15,
fontWeight: themeConfig.value.terminalFontWeight || 'normal', fontWeight: themeConfig.value.terminalFontWeight || 'normal',
@@ -91,13 +113,10 @@ function init() {
cursorBlink: true, cursorBlink: true,
disableStdin: false, disableStdin: false,
allowProposedApi: true, allowProposedApi: true,
theme: { fastScrollModifier: 'ctrl',
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体 theme: getTerminalTheme(),
background: themeConfig.value.terminalBackground || '#002833', //背景色
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
// cursorAccent: "red", // 光标停止颜色
} as any,
}); });
term.open(terminalRef.value); term.open(terminalRef.value);
// 注册自适应组件 // 注册自适应组件
@@ -105,31 +124,12 @@ function init() {
state.addon.fit = fitAddon; state.addon.fit = fitAddon;
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
fitTerminal(); fitTerminal();
// 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
// 注册搜索组件
const searchAddon = new SearchAddon();
state.addon.search = searchAddon;
term.loadAddon(searchAddon);
// 注册 url link组件
const weblinks = new WebLinksAddon();
state.addon.weblinks = weblinks;
term.loadAddon(weblinks);
// 初始化websocket
initSocket(); initSocket();
} // 注册其他插件
loadAddon();
/**
* 连接成功
*/
const onConnected = () => {
// 注册心跳
pingInterval = setInterval(sendPing, 15000);
// 注册 terminal 事件
term.onResize((event) => sendResize(event.cols, event.rows));
term.onData((event) => sendCmd(event));
// 注册自定义快捷键 // 注册自定义快捷键
term.attachCustomKeyEventHandler((event: KeyboardEvent) => { term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
@@ -141,51 +141,25 @@ const onConnected = () => {
return true; return true;
}); });
}
state.status = TerminalStatus.Connected;
// resize
sendResize(term.cols, term.rows);
// 注册窗口大小监听器
window.addEventListener('resize', debounce(fitTerminal, 400));
focus();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendCmd(props.cmd + ' \r');
}
};
// 自适应终端
const fitTerminal = () => {
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
if (!dimensions) {
return;
}
if (dimensions?.cols && dimensions?.rows) {
term.resize(dimensions.cols, dimensions.rows);
}
};
const focus = () => {
setTimeout(() => term.focus(), 400);
};
const clear = () => {
term.clear();
term.clearSelection();
term.focus();
};
function initSocket() { function initSocket() {
if (props.socketUrl) { if (!props.socketUrl) {
socket = new WebSocket(props.socketUrl); return;
} }
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接 // 监听socket连接
socket.onopen = () => { socket.onopen = () => {
onConnected(); // 注册心跳
pingInterval = setInterval(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendCmd(props.cmd + ' \r');
}
}; };
// 监听socket错误信息 // 监听socket错误信息
@@ -197,22 +171,96 @@ function initSocket() {
socket.onclose = (e: CloseEvent) => { socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason); console.log('terminal socket close...', e.reason);
// 关闭窗口大小监听器
window.removeEventListener('resize', debounce(fitTerminal, 100));
// 清除 ping
pingInterval && clearInterval(pingInterval);
state.status = TerminalStatus.Disconnected; state.status = TerminalStatus.Disconnected;
}; };
// 监听socket消息
socket.onmessage = getMessage;
} }
function getMessage(msg: any) { function loadAddon() {
// msg.data是真正后端返回的数据 // 注册搜索组件
term.write(msg.data); const searchAddon = new SearchAddon();
state.addon.search = searchAddon;
term.loadAddon(searchAddon);
// 注册 url link组件
const weblinks = new WebLinksAddon();
state.addon.weblinks = weblinks;
term.loadAddon(weblinks);
// 注册 trzsz
// initialize trzsz filter
const trzsz = new TrzszFilter({
// write the server output to the terminal
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
// send the user input to the server
sendToServer: sendCmd,
// the terminal columns
terminalColumns: term.cols,
// there is a windows shell
isWindowsShell: false,
});
// let trzsz process the server output
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
// let trzsz process the user input
term.onData((data) => trzsz.processTerminalInput(data));
term.onBinary((data) => trzsz.processBinaryInput(data));
term.onResize((size) => {
sendResize(size.cols, size.rows);
// tell trzsz the terminal columns has been changed
trzsz.setTerminalColumns(size.cols);
});
window.addEventListener('resize', () => state.addon.fit.fit());
// enable drag files or directories to upload
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
terminalRef.value.addEventListener('drop', (event: any) => {
event.preventDefault();
trzsz
.uploadFiles(event.dataTransfer.items)
.then(() => console.log('upload success'))
.catch((err: any) => console.log(err));
});
} }
// 写入内容至终端
const write2Term = (data: any) => {
term.write(data);
};
const writeln2Term = (data: any) => {
term.writeln(data);
};
const getTerminalTheme = () => {
const terminalTheme = themeConfig.value.terminalTheme;
// 如果不是自定义主题,则返回内置主题
if (terminalTheme != 'custom') {
return themes[terminalTheme];
}
// 自定义主题
return {
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
background: themeConfig.value.terminalBackground || '#002833', //背景色
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
// cursorAccent: "red", // 光标停止颜色
} as ITheme;
};
// 自适应终端
const fitTerminal = () => {
state.addon.fit.fit();
};
const focus = () => {
setTimeout(() => term.focus(), 300);
};
const clear = () => {
term.clear();
term.clearSelection();
term.focus();
};
enum MsgType { enum MsgType {
Resize = 1, Resize = 1,
Data = 2, Data = 2,
@@ -220,29 +268,19 @@ enum MsgType {
} }
const send = (msg: any) => { const send = (msg: any) => {
state.status == TerminalStatus.Connected && socket.send(JSON.stringify(msg)); state.status == TerminalStatus.Connected && socket?.send(msg);
}; };
const sendResize = (cols: number, rows: number) => { const sendResize = (cols: number, rows: number) => {
send({ send(`${MsgType.Resize}|${rows}|${cols}`);
type: MsgType.Resize,
Cols: cols,
Rows: rows,
});
}; };
const sendPing = () => { const sendPing = () => {
send({ send(`${MsgType.Ping}|ping`);
type: MsgType.Ping,
msg: 'ping',
});
}; };
function sendCmd(key: any) { function sendCmd(key: any) {
send({ send(`${MsgType.Data}|${key}`);
type: MsgType.Data,
msg: key,
});
} }
function closeSocket() { function closeSocket() {
@@ -267,20 +305,19 @@ const getStatus = (): TerminalStatus => {
return state.status; return state.status;
}; };
defineExpose({ init, fitTerminal, focus, clear, close, getStatus }); defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
</script> </script>
<style lang="scss"> <style lang="scss">
#terminal-body { #terminal-body {
background: #212529;
width: 100%; width: 100%;
.terminal { .terminal {
width: 100%; width: 100%;
height: 100%; height: 100%;
.xterm .xterm-viewport { // .xterm .xterm-viewport {
overflow-y: hidden; // overflow-y: hidden;
} // }
} }
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<div> <div>
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId"> <div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
<el-dialog <el-dialog
title="终端" title="SSH终端"
v-model="openTerminal.visible" v-model="openTerminal.visible"
top="32px" top="32px"
class="terminal-dialog" class="terminal-dialog"
@@ -58,7 +58,7 @@
</div> </div>
</div> </div>
</template> </template>
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '47px' : '200px'})` }"> <div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
<TerminalBody <TerminalBody
@status-change="terminalStatusChange(openTerminal.terminalId, $event)" @status-change="terminalStatusChange(openTerminal.terminalId, $event)"
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)" :ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
@@ -92,7 +92,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive } from 'vue'; import { reactive, toRefs } from 'vue';
import TerminalBody from '@/components/terminal/TerminalBody.vue'; import TerminalBody from '@/components/terminal/TerminalBody.vue';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { TerminalStatus } from './common'; import { TerminalStatus } from './common';
@@ -259,6 +259,10 @@ defineExpose({
padding: 10px; padding: 10px;
} }
.el-dialog {
padding: 1px 1px;
}
// 取消body最大高度否则全屏有问题 // 取消body最大高度否则全屏有问题
.el-dialog__body { .el-dialog__body {
max-height: 100% !important; max-height: 100% !important;

View File

@@ -0,0 +1,113 @@
<template>
<div>
<el-drawer v-model="visible" :before-close="cancel" size="50%">
<template #header>
<DrawerHeader :header="props.title" :back="cancel">
<template #extra>
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr20" />
</template>
</DrawerHeader>
</template>
<el-descriptions class="mb10" :column="1" border v-if="extra">
<el-descriptions-item v-for="(value, key) in extra" :key="key" :span="1" :label="key">{{ value }}</el-descriptions-item>
</el-descriptions>
<TerminalBody class="mb10" ref="terminalRef" height="calc(100vh - 220px)" />
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TerminalBody from './TerminalBody.vue';
import { logApi } from '../../views/system/api';
import { LogTypeEnum } from '@/views/system/enums';
import { useIntervalFn } from '@vueuse/core';
import EnumTag from '@/components/enumtag/EnumTag.vue';
const props = defineProps({
title: {
type: String,
default: '日志',
},
});
const visible = defineModel<boolean>('visible', { default: false });
const logId = defineModel<number>('logId', { default: 0 });
const terminalRef: any = ref(null);
const nowLine = ref(0);
const log = ref({}) as any;
const extra = computed(() => {
if (log.value?.extra) {
return JSON.parse(log.value.extra);
}
return null;
});
// 定时获取最新日志
const { pause, resume } = useIntervalFn(() => {
writeLog();
}, 500);
watch(
() => logId.value,
(logId: number) => {
terminalRef.value?.clear();
if (!logId) {
return;
}
writeLog();
}
);
const cancel = () => {
visible.value = false;
logId.value = 0;
nowLine.value = 0;
pause();
};
const writeLog = async () => {
const log = await getLog();
if (!log) {
return;
}
writeLog2Term(log);
// 如果不是还在执行中的日志,则暂停轮询
if (log.type != LogTypeEnum.Running.value) {
pause();
return;
}
resume();
};
const writeLog2Term = (log: any) => {
if (!log) {
return;
}
const lines = log.resp.split('\n');
for (let line of lines.slice(nowLine.value)) {
nowLine.value += 1;
terminalRef.value?.writeln2Term(line);
}
terminalRef.value?.focus();
};
const getLog = async () => {
if (!logId.value) {
return;
}
const logRes = await logApi.detail.request({
id: logId.value,
});
log.value = logRes;
return logRes;
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,92 @@
export default {
dark: {
foreground: '#c7c7c7',
background: '#000000',
cursor: '#c7c7c7',
selectionBackground: '#686868',
black: '#000000',
brightBlack: '#676767',
red: '#c91b00',
brightRed: '#ff6d67',
green: '#00c200',
brightGreen: '#5ff967',
yellow: '#c7c400',
brightYellow: '#fefb67',
blue: '#0225c7',
brightBlue: '#6871ff',
magenta: '#c930c7',
brightMagenta: '#ff76ff',
cyan: '#00c5c7',
brightCyan: '#5ffdff',
white: '#c7c7c7',
brightWhite: '#fffefe',
},
light: {
foreground: '#000000',
background: '#fffefe',
cursor: '#000000',
selectionBackground: '#c7c7c7',
black: '#000000',
brightBlack: '#676767',
red: '#c91b00',
brightRed: '#ff6d67',
green: '#00c200',
brightGreen: '#5ff967',
yellow: '#c7c400',
brightYellow: '#fefb67',
blue: '#0225c7',
brightBlue: '#6871ff',
magenta: '#c930c7',
brightMagenta: '#ff76ff',
cyan: '#00c5c7',
brightCyan: '#5ffdff',
white: '#c7c7c7',
brightWhite: '#fffefe',
},
solarizedLight: {
foreground: '#657b83',
background: '#fdf6e3',
cursor: '#657b83',
selectionBackground: '#c7c7c7',
black: '#073642',
brightBlack: '#002b36',
red: '#dc322f',
brightRed: '#cb4b16',
green: '#859900',
brightGreen: '#586e75',
yellow: '#b58900',
brightYellow: '#657b83',
blue: '#268bd2',
brightBlue: '#839496',
magenta: '#d33682',
brightMagenta: '#6c71c4',
cyan: '#2aa198',
brightCyan: '#93a1a1',
white: '#eee8d5',
brightWhite: '#fdf6e3',
},
};

View File

@@ -0,0 +1,121 @@
import Api from '@/common/Api';
import { 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);
}
let res = await api.request(sp);
res.list = res.list || [];
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,192 @@
import router from '@/router';
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } 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';
import openApi from '@/common/openApi';
const baseUrl: string = config.baseApiUrl;
const useCustomFetch = createFetch({
baseUrl: baseUrl,
combination: 'chain',
options: {
immediate: false,
timeout: 600000,
// 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);
let apiUrl = url;
// 简单判断该url是否是restful风格
if (apiUrl.indexOf('{') != -1) {
apiUrl = templateResolve(apiUrl, paramsValue);
}
if (api.beforeHandler) {
paramsValue = api.beforeHandler(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 () {
return execUaf(uaf);
},
isFetching: uaf.isFetching,
data: uaf.data,
abort: uaf.abort,
};
}
let refreshingToken = false;
let queue: any[] = [];
async function execUaf(uaf: any) {
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);
}
const resultCode = result.code;
// 如果返回为成功结果则将结果的data赋值给响应式data
if (resultCode === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果是accessToken失效则使用refreshToken刷新token
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
if (refreshingToken) {
// 请求加入队列等待, 防止并发多次请求refreshToken
return new Promise((resolve) => {
queue.push(() => {
resolve(execUaf(uaf));
});
});
}
try {
refreshingToken = true;
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
saveToken(res.token);
saveRefreshToken(res.refresh_token);
// 重新缓存后端用户权限code
await openApi.getPermissions();
// 执行accessToken失效的请求
queue.forEach((resolve: any) => {
resolve();
});
} catch (e: any) {
clearUser();
} finally {
refreshingToken = false;
queue = [];
}
await execUaf(uaf);
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (resultCode === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
}

View File

@@ -1,26 +1,22 @@
<template> <template>
<el-main class="layout-main"> <el-main class="layout-main">
<el-scrollbar <el-scrollbar ref="layoutScrollbarRef" v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1">
class="layout-scrollbar"
ref="layoutScrollbarRef"
v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1"
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }"
>
<LayoutParentView /> <LayoutParentView />
<Footer v-if="themeConfig.isFooter" />
</el-scrollbar> </el-scrollbar>
<Link
:style="{ height: `calc(100vh - ${state.headerHeight}` }" <Link class="h100" :meta="state.currentRouteMeta" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2"
/>
<Iframes <Iframes
:style="{ height: `calc(100vh - ${state.headerHeight}` }" class="h100"
:meta="state.currentRouteMeta" :meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta" @getCurrentRouteMeta="onGetCurrentRouteMeta"
/> />
</el-main> </el-main>
<el-footer v-if="themeConfig.isFooter">
<Footer />
</el-footer>
</template> </template>
<script setup lang="ts" name="layoutMain"> <script setup lang="ts" name="layoutMain">
@@ -57,7 +53,7 @@ const initCurrentRouteMeta = (meta: object) => {
// 设置 main 的高度 // 设置 main 的高度
const initHeaderHeight = () => { const initHeaderHeight = () => {
let { isTagsview } = themeConfig.value; let { isTagsview } = themeConfig.value;
if (isTagsview) return (state.headerHeight = `84px`); if (isTagsview) return (state.headerHeight = `77px`);
else return (state.headerHeight = `50px`); else return (state.headerHeight = `50px`);
}; };
// 页面加载前 // 页面加载前
@@ -67,7 +63,7 @@ onBeforeMount(() => {
}); });
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度 // 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => { watch(themeConfig.value, (val) => {
state.headerHeight = val.isTagsview ? '84px' : '50px'; state.headerHeight = val.isTagsview ? '77px' : '50px';
if (val.isFixedHeaderChange !== val.isFixedHeader) { if (val.isFixedHeaderChange !== val.isFixedHeader) {
if (!proxy.$refs.layoutScrollbarRef) return false; if (!proxy.$refs.layoutScrollbarRef) return false;
proxy.$refs.layoutScrollbarRef.update(); proxy.$refs.layoutScrollbarRef.update();

View File

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange"> <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> <span>
{{ `${themeConfig.globalTitle}` }} {{ `${themeConfig.globalTitle}` }}
<sub <sub
@@ -9,7 +9,7 @@
</span> </span>
</div> </div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange"> <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> </div>
</template> </template>

View File

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

View File

@@ -12,32 +12,26 @@
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script lang="ts" setup name="layoutDefaults">
import { computed, getCurrentInstance, watch } from 'vue'; import { computed, getCurrentInstance, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import Aside from '@/layout/component/aside.vue'; import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue'; import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue'; import Main from '@/layout/component/main.vue';
import { useThemeConfig } from '@/store/themeConfig'; import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutDefaults', const { proxy } = getCurrentInstance() as any;
components: { Aside, Header, Main }, const route = useRoute();
setup() { const isFixedHeader = computed(() => {
const { proxy } = getCurrentInstance() as any; return useThemeConfig().themeConfig.isFixedHeader;
const route = useRoute(); });
const isFixedHeader = computed(() => { // 监听路由的变化
return useThemeConfig().themeConfig.isFixedHeader; watch(
}); () => route.path,
// 监听路由的变化 () => {
watch( try {
() => route.path, proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
() => { } catch (e) {}
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0; }
} );
);
return {
isFixedHeader,
};
},
};
</script> </script>

View File

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

View File

@@ -1,18 +1,15 @@
<template> <template>
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb"> <div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" <SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
@click="onThemeConfigChange" />
<el-breadcrumb class="layout-navbars-breadcrumb-hide"> <el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb" mode="out-in"> <transition-group name="breadcrumb" mode="out-in">
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title"> <el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span"> <span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
v-if="themeConfig.isBreadcrumbIcon" />
{{ v.meta.title }} {{ v.meta.title }}
</span> </span>
<a v-else @click.prevent="onBreadcrumbClick(v)"> <a v-else @click.prevent="onBreadcrumbClick(v)">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
v-if="themeConfig.isBreadcrumbIcon" />
{{ v.meta.title }} {{ v.meta.title }}
</a> </a>
</el-breadcrumb-item> </el-breadcrumb-item>

View File

@@ -1,17 +1,22 @@
<template> <template>
<div class="layout-search-dialog"> <div class="layout-search-dialog">
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false"> <el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索" <el-autocomplete
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur"> v-model="state.menuQuery"
:fetch-suggestions="menuSearch"
placeholder="菜单搜索"
prefix-icon="el-icon-search"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
@blur="onSearchBlur"
>
<template #prefix> <template #prefix>
<el-icon class="el-input__icon"> <el-icon class="el-input__icon">
<search /> <search />
</el-icon> </el-icon>
</template> </template>
<template #default="{ item }"> <template #default="{ item }">
<div> <div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
</div>
</template> </template>
</el-autocomplete> </el-autocomplete>
</el-dialog> </el-dialog>
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useRoutesList } from '@/store/routesList'; import { useRoutesList } from '@/store/routesList';
const layoutMenuAutocompleteRef: any = ref(null);; const layoutMenuAutocompleteRef: any = ref(null);
const router = useRouter(); const router = useRouter();
const state: any = reactive({ const state: any = reactive({
isShowSearch: false, isShowSearch: false,
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
const createFilter = (queryString: any) => { const createFilter = (queryString: any) => {
return (restaurant: any) => { return (restaurant: any) => {
return ( return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
); );
}; };
}; };
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
closeSearch(); closeSearch();
}; };
defineExpose({openSearch}) defineExpose({ openSearch });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -5,26 +5,39 @@
<!-- ssh终端主题 --> <!-- ssh终端主题 -->
<el-divider content-position="left">终端主题</el-divider> <el-divider content-position="left">终端主题</el-divider>
<div class="layout-breadcrumb-seting-bar-flex"> <div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div> <div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')"> <el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalTheme" size="small" style="width: 140px">
</el-color-picker> <el-option v-for="(_, k) in themes" :key="k" :label="k" :value="k"> </el-option>
<el-option label="自定义" value="custom"> </el-option>
</el-select>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex"> <template v-if="themeConfig.terminalTheme == 'custom'">
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div> <div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')"> <div class="layout-breadcrumb-seting-bar-flex-value">
</el-color-picker> <el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
</el-color-picker>
</div>
</div> </div>
</div> <div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex"> <div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div> <div class="layout-breadcrumb-seting-bar-flex-value">
<div class="layout-breadcrumb-seting-bar-flex-value"> <el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker> </el-color-picker>
</div>
</div> </div>
</div> <div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex mt15"> <div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
</el-color-picker>
</div>
</div>
</template>
<div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div> <div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-input-number <el-input-number
@@ -39,7 +52,7 @@
</el-input-number> </el-input-number>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex mt15"> <div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div> <div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px"> <el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
@@ -61,6 +74,24 @@
</div> </div>
</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> <el-divider content-position="left">全局主题</el-divider>
<div class="layout-breadcrumb-seting-bar-flex"> <div class="layout-breadcrumb-seting-bar-flex">
@@ -400,6 +431,7 @@ import { useThemeConfig } from '@/store/themeConfig';
import { getLightColor } from '@/common/utils/theme'; import { getLightColor } from '@/common/utils/theme';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage'; import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt'; import mittBus from '@/common/utils/mitt';
import themes from '@/components/terminal/themes';
const copyConfigBtnRef = ref(); const copyConfigBtnRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig()); const { themeConfig } = storeToRefs(useThemeConfig());
@@ -578,7 +610,7 @@ const openDrawer = () => {
themeConfig.value.isDrawer = true; themeConfig.value.isDrawer = true;
nextTick(() => { nextTick(() => {
// 初始化复制功能,防止点击两次才可以复制 // 初始化复制功能,防止点击两次才可以复制
onCopyConfigClick(copyConfigBtnRef.value.$el); onCopyConfigClick(copyConfigBtnRef.value?.$el);
}); });
}; };
// 触发 store 布局配置更新 // 触发 store 布局配置更新
@@ -597,6 +629,9 @@ const setLocalThemeConfigStyle = () => {
}; };
// 一键复制配置 // 一键复制配置
const onCopyConfigClick = (target: any) => { const onCopyConfigClick = (target: any) => {
if (!target) {
return;
}
let copyThemeConfig = getLocal('themeConfig'); let copyThemeConfig = getLocal('themeConfig');
copyThemeConfig.isDrawer = false; copyThemeConfig.isDrawer = false;
const clipboard = new ClipboardJS(target, { const clipboard = new ClipboardJS(target, {
@@ -672,6 +707,25 @@ defineExpose({ openDrawer });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
::v-deep(.el-drawer) {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
}
.layout-breadcrumb-seting-bar { .layout-breadcrumb-seting-bar {
height: calc(100vh - 50px); height: calc(100vh - 50px);
padding: 0 15px; padding: 0 15px;

View File

@@ -2,8 +2,8 @@
<div class="layout-navbars-breadcrumb-user" :style="{ flex: layoutUserFlexNum }"> <div class="layout-navbars-breadcrumb-user" :style="{ flex: layoutUserFlexNum }">
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-switch <el-switch
@change="switchDark(state.isDark)" @change="switchDark()"
v-model="state.isDark" v-model="isDark"
active-action-icon="Moon" active-action-icon="Moon"
inactive-action-icon="Sunny" inactive-action-icon="Sunny"
style="--el-switch-off-color: #c4c9c4; --el-switch-on-color: #2c2c2c" style="--el-switch-off-color: #c4c9c4; --el-switch-on-color: #2c2c2c"
@@ -75,7 +75,7 @@
</template> </template>
<script setup lang="ts" name="layoutBreadcrumbUser"> <script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted } from 'vue'; import { ref, computed, reactive, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
@@ -83,17 +83,17 @@ import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig'; import { useThemeConfig } from '@/store/themeConfig';
import { clearSession, removeLocal } from '@/common/utils/storage'; import { clearSession } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue'; import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue'; import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt'; import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi'; import openApi from '@/common/openApi';
import { saveThemeConfig, getThemeConfig } from '@/common/utils/storage'; import { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
import { useDark, usePreferredDark } from '@vueuse/core';
const router = useRouter(); const router = useRouter();
const searchRef = ref(); const searchRef = ref();
const state = reactive({ const state = reactive({
isDark: false,
isScreenfull: false, isScreenfull: false,
isShowUserNewsPopover: false, isShowUserNewsPopover: false,
disabledI18n: 'zh-cn', disabledI18n: 'zh-cn',
@@ -165,8 +165,16 @@ const onHandleCommandClick = (path: string) => {
} }
}; };
const switchDark = (isDark: boolean) => { const isDark = useDark();
themeConfigStore.switchDark(isDark); const preDark = usePreferredDark();
watch(preDark, (newValue) => {
isDark.value = newValue;
switchDark();
});
const switchDark = () => {
themeConfigStore.switchDark(isDark.value);
saveThemeConfig(themeConfig.value); saveThemeConfig(themeConfig.value);
}; };
@@ -176,14 +184,14 @@ const onSearchClick = () => {
}; };
// 组件大小改变 // 组件大小改变
const onComponentSizeChange = (size: string) => { // const onComponentSizeChange = (size: string) => {
removeLocal('themeConfig'); // removeLocal('themeConfig');
themeConfig.value.globalComponentSize = size; // themeConfig.value.globalComponentSize = size;
saveThemeConfig(themeConfig.value); // saveThemeConfig(themeConfig.value);
// proxy.$ELEMENT.size = size; // // proxy.$ELEMENT.size = size;
initComponentSize(); // initComponentSize();
window.location.reload(); // window.location.reload();
}; // };
// 初始化全局组件大小 // 初始化全局组件大小
const initComponentSize = () => { const initComponentSize = () => {
@@ -208,7 +216,7 @@ onMounted(() => {
const themeConfig = getThemeConfig(); const themeConfig = getThemeConfig();
if (themeConfig) { if (themeConfig) {
initComponentSize(); initComponentSize();
state.isDark = themeConfig.isDark; isDark.value = themeConfig.isDark;
} }
}); });
</script> </script>

View File

@@ -282,7 +282,11 @@ const onContextmenu = (v: any, e: any) => {
const onTagsClick = (v: any, k: number) => { const onTagsClick = (v: any, k: number) => {
state.routePath = decodeURI(v.path); state.routePath = decodeURI(v.path);
state.tagsRefsIndex = k; state.tagsRefsIndex = k;
router.push(v); try {
router.push(v);
} catch (e) {
// skip
}
}; };
// 更新滚动条显示 // 更新滚动条显示
const updateScrollbar = () => { const updateScrollbar = () => {

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
<template> <template>
<div class="h100"> <router-view v-slot="{ Component }">
<router-view v-slot="{ Component }"> <transition appear :name="setTransitionName" mode="out-in">
<transition :name="setTransitionName" mode="out-in"> <keep-alive :include="getKeepAliveNames">
<keep-alive :include="getKeepAliveNames"> <component :is="Component" :key="state.refreshRouterViewKey" />
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" /> </keep-alive>
</keep-alive> </transition>
</transition> </router-view>
</router-view>
</div>
</template> </template>
<script lang="ts" setup name="layoutParentView"> <script lang="ts" setup name="layoutParentView">

View File

@@ -13,6 +13,8 @@ import 'element-plus/theme-chalk/dark/css-vars.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn'; import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import 'splitpanes/dist/splitpanes.css';
import '@/theme/index.scss'; import '@/theme/index.scss';
import '@/assets/font/font.css'; import '@/assets/font/font.css';
import '@/assets/iconfont/iconfont.js'; import '@/assets/iconfont/iconfont.js';

View File

@@ -0,0 +1,164 @@
import 'nprogress/nprogress.css';
import { clearSession, getToken } from '@/common/utils/storage';
import openApi from '@/common/openApi';
import { useUserInfo } from '@/store/userInfo';
import { useRoutesList } from '@/store/routesList';
import { useKeepALiveNames } from '@/store/keepAliveNames';
import router from '.';
import { RouteRecordRaw } from 'vue-router';
import { LAYOUT_ROUTE_NAME } from './staticRouter';
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const viewsModules: Record<string, Function> = import.meta.glob(['../views/**/*.{vue,tsx}']);
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 后端控制路由:执行路由数据初始化
export async function initBackendRoutes() {
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
}
useUserInfo().setUserInfo({});
// 获取路由
let menuRoute = await getBackEndControlRoutes();
const cacheList: Array<string> = [];
// 处理路由component
const routes = backEndRouterConverter(menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false;
}
if (router.meta.isKeepAlive) {
cacheList.push(router.name);
}
});
routes.forEach((item: any) => {
if (item.meta.isFull) {
// 菜单为全屏展示 (示例:数据大屏页面等)
router.addRoute(item as RouteRecordRaw);
} else {
// 要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样
router.addRoute(LAYOUT_ROUTE_NAME, item as RouteRecordRaw);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
useRoutesList().setRoutesList(routes);
}
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
export async function getBackEndControlRoutes() {
try {
const menuAndPermission = await openApi.getPermissions();
// 赋值权限码,用于控制按钮等
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
return menuAndPermission.menus;
} catch (e: any) {
console.error('获取菜单权限信息失败', e);
clearSession();
throw e;
}
}
type RouterConvCallbackFunc = (router: any) => void;
/**
* 后端控制路由,后端返回路由 转换为vue route
*
* @description routes参数配置简介
* @param code(path) ==> route.path -> 路由菜单访问路径
* @param name ==> title路由标题 相当于route.meta.title
*
* @param meta ==> 路由菜单元信息
* @param meta.routeName ==> route.name -> 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选)
* @param meta.redirect ==> route.redirect -> 路由重定向地址
* @param meta.component ==> 文件路径
* @param meta.icon ==> 菜单和面包屑对应的图标
* @param meta.isHide ==> 是否在菜单中隐藏 (通常列表详情页需要隐藏)
* @param meta.isFull ==> 菜单是否全屏 (示例:数据大屏页面)
* @param meta.isAffix ==> 菜单是否固定在标签页中 (首页通常是固定项)
* @param meta.isKeepAlive ==> 当前路由是否缓存
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
* @param meta.link ==> 外链地址
* */
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) {
return [];
}
const routeItems = [];
for (let item of routes) {
if (!item.meta) {
return item;
}
// 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta);
// 将meta.comoponet 解析为route.component
if (item.meta.component) {
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
delete item.meta['component'];
}
let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith('/')) {
path = parentPath + '/' + path;
}
item.path = path;
delete item['code'];
// route.meta.title == resource.name
item.meta.title = item.name;
delete item['name'];
// route.name == resource.meta.routeName
item.name = item.meta.routeName;
delete item.meta['routeName'];
// route.redirect == resource.meta.redirect
if (item.meta.redirect) {
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
}
// 存在回调,则执行回调
callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
routeItems.push(item);
}
return routeItems;
}
/**
* 后端路由 component 转换函数
* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
* @param component 当前要处理项 component
* @returns 返回处理成函数后的 component
*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
});
if (matchKeys?.length === 1) {
return dynamicViewsModules[matchKeys[0]];
}
if (matchKeys?.length > 1) {
console.error('匹配到多个相似组件路径, 可添加后缀.vue或.tsx进行区分或者重命名组件名, 请调整...', matchKeys);
return null;
}
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
return null;
}

View File

@@ -1,30 +1,21 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHashHistory } from 'vue-router';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import { clearSession, getToken } from '@/common/utils/storage'; import { getToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string'; import { templateResolve } from '@/common/utils/string';
import { NextLoading } from '@/common/utils/loading'; import { NextLoading } from '@/common/utils/loading';
import { dynamicRoutes, staticRoutes, pathMatch } from './route'; import { staticRoutes, URL_LOGIN, URL_401, ROUTER_WHITE_LIST, errorRoutes } from './staticRouter';
import openApi from '@/common/openApi';
import syssocket from '@/common/syssocket'; import syssocket from '@/common/syssocket';
import pinia from '@/store/index'; import pinia from '@/store/index';
import { useThemeConfig } from '@/store/themeConfig'; import { useThemeConfig } from '@/store/themeConfig';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { useRoutesList } from '@/store/routesList'; import { useRoutesList } from '@/store/routesList';
import { useKeepALiveNames } from '@/store/keepAliveNames'; import { initBackendRoutes } from './dynamicRouter';
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}']);
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 添加静态路由 // 添加静态路由
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: staticRoutes, routes: [...staticRoutes, ...errorRoutes],
}); });
// 前端控制路由:初始化方法,防止刷新时丢失 // 前端控制路由:初始化方法,防止刷新时丢失
@@ -35,128 +26,15 @@ export function initAllFun() {
return false; return false;
} }
useUserInfo().setUserInfo({}); useUserInfo().setUserInfo({});
router.addRoute(pathMatch); // 添加404界面
resetRoute(); // 删除/重置路由 resetRoute(); // 删除/重置路由
router.addRoute(dynamicRoutes[0]); // router.addRoute(dynamicRoutes[0]);
// // 过滤权限菜单
// 过滤权限菜单 // useRoutesList().setRoutesList(dynamicRoutes[0].children);
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
// 后端控制路由:执行路由数据初始化
export async function initBackEndControlRoutesFun() {
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
}
useUserInfo().setUserInfo({});
// 获取路由
let menuRoute = await getBackEndControlRoutes();
const cacheList: Array<string> = [];
// 处理路由component
dynamicRoutes[0].children = backEndRouterConverter(menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false;
}
if (router.meta.isKeepAlive) {
cacheList.push(router.name);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
// 添加404界面
router.addRoute(pathMatch);
resetRoute(); // 删除/重置路由
router.addRoute(dynamicRoutes[0] as unknown as RouteRecordRaw);
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
export async function getBackEndControlRoutes() {
try {
const menuAndPermission = await openApi.getPermissions();
// 赋值权限码,用于控制按钮等
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
return menuAndPermission.menus;
} catch (e: any) {
console.error(e);
return [];
}
}
type RouterConvCallbackFunc = (router: any) => void;
// 后端控制路由,后端返回路由 转换为vue route
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) return;
return routes.map((item: any) => {
if (!item.meta) {
return item;
}
// 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta);
// 将meta.comoponet 解析为route.component
if (item.meta.component) {
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
delete item.meta['component'];
}
// route.path == resource.code
let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith('/')) {
path = parentPath + '/' + path;
}
item.path = path;
delete item['code'];
// route.meta.title == resource.name
item.meta.title = item.name;
delete item['name'];
// route.name == resource.meta.routeName
item.name = item.meta.routeName;
delete item.meta['routeName'];
// route.redirect == resource.meta.redirect
if (item.meta.redirect) {
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
}
// 存在回调,则执行回调
callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
return item;
});
}
/**
* 后端路由 component 转换函数
* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
* @param component 当前要处理项 component
* @returns 返回处理成函数后的 component
*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return dynamicViewsModules[matchKey];
}
if (matchKeys?.length > 1) {
return false;
}
} }
// 删除/重置路由 // 删除/重置路由
export function resetRoute() { export function resetRoute() {
useRoutesList().routesList.forEach((route: any) => { useRoutesList().routesList?.forEach((route: any) => {
const { name } = route; const { name } = route;
router.hasRoute(name) && router.removeRoute(name); router.hasRoute(name) && router.removeRoute(name);
}); });
@@ -172,19 +50,17 @@ export async function initRouter() {
initAllFun(); initAllFun();
} else if (isRequestRoutes) { } else if (isRequestRoutes) {
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由 // 后端控制路由isRequestRoutes 为 true则开启后端控制路由
await initBackEndControlRoutesFun(); await initBackendRoutes();
} }
} finally { } finally {
NextLoading.done(); NextLoading.done();
} }
} }
let loadRouter = false;
// 路由加载前 // 路由加载前
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.configure({ showSpinner: false }); NProgress.configure({ showSpinner: false });
if (to.meta.title) NProgress.start(); NProgress.start();
// 如果有标题参数,则再原标题后加上参数来区别 // 如果有标题参数,则再原标题后加上参数来区别
if (to.meta.titleRename && to.meta.title) { if (to.meta.titleRename && to.meta.title) {
@@ -192,39 +68,46 @@ router.beforeEach(async (to, from, next) => {
} }
const token = getToken(); const token = getToken();
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
next();
NProgress.done();
return;
}
if (!token) {
next(`/login?redirect=${to.path}`);
clearSession();
resetRoute();
NProgress.done();
const toPath = to.path;
// 判断是访问登陆页有token就在当前页面没有token重置路由与用户信息到登陆页
if (toPath === URL_LOGIN) {
if (token) {
return next(from.fullPath);
}
resetRoute();
syssocket.destory(); syssocket.destory();
return; return next();
} }
if (token && to.path === '/login') {
next('/'); // 判断访问页面是否在路由白名单地址(静态路由)中,如果存在直接放行
NProgress.done(); if (ROUTER_WHITE_LIST.includes(toPath)) {
return; return next();
}
// 判断是否有token没有重定向到 login 页面
if (!token) {
return next(`${URL_LOGIN}?redirect=${toPath}`);
} }
// 终端不需要连接系统websocket消息 // 终端不需要连接系统websocket消息
if (to.path != '/machine/terminal') { if (to.path != '/machine/terminal' && to.path != '/machine/terminal-rdp') {
syssocket.init(); syssocket.init();
} }
// 不存在路由(避免刷新页面找不到路由)并且未加载过避免token过期导致获取权限接口报权限不足无限获取,则重新初始化路由 // 不存在路由(避免刷新页面找不到路由),则重新初始化路由
if (useRoutesList().routesList.length == 0 && !loadRouter) { if (useRoutesList().routesList?.length == 0) {
await initRouter(); try {
loadRouter = true; // 可能token过期无法获取菜单权限信息等
next({ path: to.path, query: to.query }); await initRouter();
} else { } catch (e) {
next(); return next(`${URL_401}?redirect=${toPath}`);
}
return next({ path: toPath, query: to.query });
} }
next();
}); });
// 路由加载后 // 路由加载后
@@ -232,5 +115,13 @@ router.afterEach(() => {
NProgress.done(); NProgress.done();
}); });
/**
* @description 路由跳转错误
* */
router.onError((error) => {
NProgress.done();
console.warn('路由错误', error.message);
});
// 导出路由 // 导出路由
export default router; export default router;

View File

@@ -1,171 +0,0 @@
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue';
// 定义动态路由
export const dynamicRoutes = [
{
path: '/',
name: '/',
component: Layout,
redirect: '/home',
meta: {
isKeepAlive: true,
},
children: [],
// children: [
// {
// path: '/home',
// name: 'home',
// component: () => import('@/views/home/index.vue'),
// meta: {
// title: '首页',
// // iframe链接
// link: '',
// // 是否在菜单栏显示,默认显示
// isHide: false,
// isKeepAlive: true,
// // tag标签是否不可删除
// isAffix: true,
// // 是否为iframe
// isIframe: false,
// icon: 'el-icon-s-home',
// },
// },
// {
// path: '/sys',
// name: 'Resource',
// redirect: '/sys/resources',
// meta: {
// title: '系统管理',
// // 资源code用于校验用户是否拥有该资源权限
// code: 'sys',
// // isKeepAlive: true,
// icon: 'el-icon-monitor',
// },
// children: [
// {
// path: 'sys/resources',
// name: 'ResourceList',
// component: () => import('@/views/system/resource'),
// meta: {
// title: '资源管理',
// code: 'resource:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// {
// path: 'sys/roles',
// name: 'RoleList',
// component: () => import('@/views/system/role'),
// meta: {
// title: '角色管理',
// code: 'role:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// {
// path: 'sys/accounts',
// name: 'ResourceList',
// component: () => import('@/views/system/account'),
// meta: {
// title: '账号管理',
// code: 'account:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// ],
// },
// {
// path: '/machine',
// name: 'Machine',
// redirect: '/machine/list',
// meta: {
// title: '机器管理',
// // 资源code用于校验用户是否拥有该资源权限
// code: 'machine',
// // isKeepAlive: true,
// icon: 'el-icon-monitor',
// },
// children: [
// {
// path: '/list',
// name: 'MachineList',
// component: () => import('@/views/ops/machine'),
// meta: {
// title: '机器列表',
// code: 'machine:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// ],
// },
// {
// path: '/personal',
// name: 'personal',
// component: () => import('@/views/personal/index.vue'),
// meta: {
// title: '个人中心',
// isKeepAlive: true,
// icon: 'el-icon-user',
// },
// },
// ],
},
];
// 定义静态路由
export const staticRoutes: Array<RouteRecordRaw> = [
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
},
},
{
path: '/404',
name: 'notFound',
component: () => import('@/views/error/404.vue'),
meta: {
title: '找不到此页面',
},
},
{
path: '/401',
name: 'noPower',
component: () => import('@/views/error/401.vue'),
meta: {
title: '没有权限',
},
},
{
path: '/oauth2/callback',
name: 'oauth2Callback',
component: () => import('@/views/oauth/Oauth2Callback.vue'),
meta: {
title: 'oauth2回调',
},
},
{
path: '/machine/terminal',
name: 'machineTerminal',
component: () => import('@/views/ops/machine/SshTerminalPage.vue'),
meta: {
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
title: '终端 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true,
},
},
];
// 定义404界面
export const pathMatch = {
path: '/:path(.*)*',
redirect: '/404',
};

View File

@@ -0,0 +1,93 @@
import { RouteRecordRaw } from 'vue-router';
export const URL_HOME: string = '/home';
// 登录页地址(默认)
export const URL_LOGIN: string = '/login';
export const URL_401: string = '/401';
export const URL_404: string = '/404';
export const LAYOUT_ROUTE_NAME: string = 'layout';
// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
export const ROUTER_WHITE_LIST: string[] = [URL_404, URL_401, '/oauth2/callback'];
// 静态路由
export const staticRoutes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: URL_HOME,
},
{
path: URL_LOGIN,
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
},
},
{
path: '/layout',
name: LAYOUT_ROUTE_NAME,
component: () => import('@/layout/index.vue'),
redirect: URL_HOME,
children: [],
},
{
path: '/oauth2/callback',
name: 'oauth2Callback',
component: () => import('@/views/oauth/Oauth2Callback.vue'),
meta: {
title: 'oauth2回调',
},
},
{
path: '/machine/terminal',
name: 'machineTerminal',
component: () => import('@/views/ops/machine/SshTerminalPage.vue'),
meta: {
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
title: '终端 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true,
},
},
{
path: '/machine/terminal-rdp',
name: 'machineTerminalRdp',
component: () => import('@/views/ops/machine/RdpTerminalPage.vue'),
meta: {
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
title: '终端 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true,
},
},
];
// 错误页面路由
export const errorRoutes: Array<RouteRecordRaw> = [
{
path: URL_404,
name: 'notFound',
component: () => import('@/views/error/404.vue'),
meta: {
title: '找不到此页面',
},
},
{
path: URL_401,
name: 'noPower',
component: () => import('@/views/error/401.vue'),
meta: {
title: '没有权限',
},
},
// Resolve refresh page, route warnings
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/error/404.vue'),
},
];

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
/**
* 自动打开资源
*/
export const useAutoOpenResource = defineStore('autoOpenResource', {
state: () => ({
autoOpenResource: {
machineCodePath: '',
dbCodePath: '',
redisCodePath: '',
mongoCodePath: '',
},
}),
actions: {
setMachineCodePath(codePath: string) {
this.autoOpenResource.machineCodePath = codePath;
},
setDbCodePath(codePath: string) {
this.autoOpenResource.dbCodePath = codePath;
},
setRedisCodePath(codePath: string) {
this.autoOpenResource.redisCodePath = codePath;
},
setMongoCodePath(codePath: string) {
this.autoOpenResource.mongoCodePath = codePath;
},
},
});

View File

@@ -11,7 +11,7 @@ export const useRoutesList = defineStore('routesList', {
routesList: [], routesList: [],
}), }),
actions: { actions: {
async setRoutesList(data: Array<string>) { async setRoutesList(data: Array<any>) {
this.routesList = data; this.routesList = data;
}, },
}, },

View File

@@ -1,6 +1,12 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { dateFormat2 } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { getSysStyleConfig } from '@/common/sysconfig';
import { getLocal, getThemeConfig } from '@/common/utils/storage';
// 系统默认logo图标对应于@/assets/image/logo.svg
const logoIcon =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjIxODU5MDA5NjA1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijk3MDkiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNODIwLjIwMzkyMiA4MTIuMTcyNTQ5SDY4NC42NzQ1MXYtNDUuMTc2NDcxaDExMi40MzkyMTVWMjc5LjA5MDE5Nkg2MzMuNDc0NTFsLTg1LjMzMzMzNCAyNzcuMDgyMzUzYy0zLjAxMTc2NSAxMC4wMzkyMTYtMTIuMDQ3MDU5IDE2LjA2Mjc0NS0yMi4wODYyNzQgMTYuMDYyNzQ1LTEwLjAzOTIxNiAwLTE5LjA3NDUxLTcuMDI3NDUxLTIxLjA4MjM1My0xNy4wNjY2NjdsLTcxLjI3ODQzMS0yODAuMDk0MTE3aC0xODAuNzA1ODgzVjc2Mi45ODAzOTJoMTIwLjQ3MDU4OXY0NS4xNzY0NzFIMjI5Ljg5ODAzOWMtMTIuMDQ3MDU5IDAtMjIuMDg2Mjc1LTEwLjAzOTIxNi0yMi4wODYyNzQtMjIuMDg2Mjc1VjI1Mi45ODgyMzVjMC0xMi4wNDcwNTkgMTAuMDM5MjE2LTIyLjA4NjI3NSAyMi4wODYyNzQtMjIuMDg2Mjc0SDQ1MS43NjQ3MDZjMTAuMDM5MjE2IDAgMTkuMDc0NTEgNy4wMjc0NTEgMjIuMDg2Mjc0IDE3LjA2NjY2Nmw1NS4yMTU2ODcgMjE4Ljg1NDkwMkw1OTUuMzI1NDkgMjUwLjk4MDM5MmMzLjAxMTc2NS05LjAzNTI5NCAxMi4wNDcwNTktMTYuMDYyNzQ1IDIxLjA4MjM1My0xNi4wNjI3NDVoMjAyLjc5MjE1N2MxMi4wNDcwNTkgMCAyMi4wODYyNzUgMTAuMDM5MjE2IDIyLjA4NjI3NSAyMi4wODYyNzV2NTMzLjA4MjM1M2MxLjAwMzkyMiAxMi4wNDcwNTktOS4wMzUyOTQgMjIuMDg2Mjc1LTIxLjA4MjM1MyAyMi4wODYyNzR6IG0wIDAiIGZpbGw9IiNlMjU4MTMiIHAtaWQ9Ijk3MTAiPjwvcGF0aD48cGF0aCBkPSJNNzMxLjg1ODgyNCA0MjUuNjYyNzQ1YzQuMDE1Njg2LTEyLjA0NzA1OS0yLjAwNzg0My0yNS4wOTgwMzktMTQuMDU0OTAyLTI5LjExMzcyNS0xMi4wNDcwNTktNC4wMTU2ODYtMjUuMDk4MDM5IDIuMDA3ODQzLTI5LjExMzcyNiAxNC4wNTQ5MDJMNTYzLjIgNzY2Ljk5NjA3OGgtNzMuMjg2Mjc1TDM3MS40NTA5OCA0MTAuNjAzOTIyYy00LjAxNTY4Ni0xMi4wNDcwNTktMTcuMDY2NjY3LTE4LjA3MDU4OC0yOC4xMDk4MDQtMTQuMDU0OTAyLTEyLjA0NzA1OSA0LjAxNTY4Ni0xOC4wNzA1ODggMTcuMDY2NjY3LTE0LjA1NDkwMSAyOC4xMDk4MDRsMTIzLjQ4MjM1MiAzNzEuNDUwOThjMy4wMTE3NjUgOS4wMzUyOTQgMTIuMDQ3MDU5IDE1LjA1ODgyNCAyMS4wODIzNTMgMTUuMDU4ODIzaDcyLjI4MjM1M2wtNTMuMjA3ODQzIDE2MC42Mjc0NTEgNDYuMTgwMzkyIDIuMDA3ODQ0IDE5Mi43NTI5NDItNTQ4LjE0MTE3N3oiIGZpbGw9IiMyYzJjMmMiIHAtaWQ9Ijk3MTEiPjwvcGF0aD48L3N2Zz4=';
export const useThemeConfig = defineStore('themeConfig', { export const useThemeConfig = defineStore('themeConfig', {
state: (): ThemeConfigState => ({ state: (): ThemeConfigState => ({
@@ -108,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns // 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
layout: 'classic', layout: 'classic',
terminalTheme: 'light',
// ssh终端字体颜色 // ssh终端字体颜色
terminalForeground: '#C5C8C6', terminalForeground: '#C5C8C6',
// ssh终端背景色 // ssh终端背景色
@@ -130,39 +137,48 @@ export const useThemeConfig = defineStore('themeConfig', {
// 网站主标题(菜单导航、浏览器当前网页标题) // 网站主标题(菜单导航、浏览器当前网页标题)
globalTitle: 'mayfly', globalTitle: 'mayfly',
// 网站副标题(登录页顶部文字) // 网站副标题(登录页顶部文字)
globalViceTitle: 'mayfly', globalViceTitle: 'mayfly-go',
// 网站logo icon, base64编码内容
logoIcon: logoIcon,
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn // 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
globalI18n: 'zh-cn', globalI18n: 'zh-cn',
// 默认全局组件大小,可选值"<|large|default|small>",默认 '' // 默认全局组件大小,可选值"<|large|default|small>",默认 ''
globalComponentSize: '', globalComponentSize: '',
/** 全局设置 */
// 默认列表页的分页大小
defaultListPageSize: 10,
}, },
}), }),
actions: { actions: {
// 设置布局配置 initThemeConfig() {
setThemeConfig(data: ThemeConfigState) { // 获取缓存中的布局配置
this.themeConfig = data.themeConfig; const tc = getThemeConfig();
}, if (tc) {
// 切换暗模式 this.themeConfig = tc;
switchDark(isDark: boolean) { document.documentElement.style.cssText = getLocal('themeConfigStyle');
this.themeConfig.isDark = isDark;
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
this.themeConfig.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
this.themeConfig.editorTheme = 'vs';
} }
},
// 设置水印配置信息 // 根据后台系统配置初始化
setWatermarkConfig(useWatermarkConfig: any) { getSysStyleConfig().then((res) => {
this.themeConfig.watermarkText = []; if (res?.title) {
this.themeConfig.isWatermark = useWatermarkConfig.isUse; this.themeConfig.globalTitle = res.title;
if (!useWatermarkConfig.isUse) { }
return; if (res?.viceTitle) {
} this.themeConfig.globalViceTitle = res.viceTitle;
// 索引2为用户自定义水印信息 }
this.themeConfig.watermarkText[2] = useWatermarkConfig.content; if (res?.logoIcon) {
this.themeConfig.logoIcon = res.logoIcon;
}
this.themeConfig.watermarkText = [];
this.themeConfig.isWatermark = res?.useWatermark;
if (!res?.useWatermark) {
return;
}
// 索引2为用户自定义水印信息
this.themeConfig.watermarkText[2] = res.watermarkContent;
});
}, },
// 设置水印用户信息 // 设置水印用户信息
setWatermarkUser(del: boolean = false) { setWatermarkUser(del: boolean = false) {
@@ -175,7 +191,25 @@ export const useThemeConfig = defineStore('themeConfig', {
}, },
// 设置水印时间为当前时间 // 设置水印时间为当前时间
setWatermarkNowTime() { setWatermarkNowTime() {
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date()); this.themeConfig.watermarkText[1] = formatDate(new Date());
},
// 切换暗黑模式
switchDark(isDark: boolean) {
this.themeConfig.isDark = isDark;
// 切换编辑器主题
if (isDark) {
this.themeConfig.editorTheme = 'vs-dark';
} else {
this.themeConfig.editorTheme = 'vs';
}
// 如果终端主题不是自定义主题,则切换主题
if (this.themeConfig.terminalTheme != 'custom') {
if (isDark) {
this.themeConfig.terminalTheme = 'dark';
} else {
this.themeConfig.terminalTheme = 'light';
}
}
}, },
}, },
}); });

View File

@@ -8,21 +8,21 @@
} }
:root { :root {
--color-white: #ffffff; --color-white: #ffffff;
--bg-main-color: #f8f8f8; --bg-main-color: #f8f8f8;
--bg-color: #f5f5ff; --bg-color: #f5f5ff;
--bg-menuBarActiveColor: #0000000a; // 菜单栏激活时的背景色 --bg-menuBarActiveColor: #0000000a; // 菜单栏激活时的背景色
--border-color-light: #f1f2f3; --border-color-light: #f1f2f3;
--el-color-primary-lighter: #ecf5ff; --el-color-primary-lighter: #ecf5ff;
--color-success-lighter: #f0f9eb; --color-success-lighter: #f0f9eb;
--color-warning-lighter: #fdf6ec; --color-warning-lighter: #fdf6ec;
--color-danger-lighter: #fef0f0; --color-danger-lighter: #fef0f0;
--color-dark-hover: #0000001a; --color-dark-hover: #0000001a;
--color-menu-hover: rgba(0, 0, 0, 0.2); --color-menu-hover: rgba(0, 0, 0, 0.2);
--color-user-hover: rgba(0, 0, 0, 0.04); --color-user-hover: rgba(0, 0, 0, 0.04);
--color-seting-main: #e9eef3; --color-seting-main: #e9eef3;
--color-seting-aside: #d3dce6; --color-seting-aside: #d3dce6;
--color-seting-header: #b3c0d1; --color-seting-header: #b3c0d1;
--tagsview3-active-background-color: var(--el-color-primary-light-9); --tagsview3-active-background-color: var(--el-color-primary-light-9);
} }
@@ -35,7 +35,7 @@ body,
width: 100%; width: 100%;
height: 100%; height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450; font-weight: 500;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
background-color: var(--bg-main-color); background-color: var(--bg-main-color);
@@ -70,8 +70,9 @@ body,
} }
.layout-main { .layout-main {
padding: 0 !important; box-sizing: border-box;
overflow: hidden; padding: 10px 10px;
overflow-x: hidden;
width: 100%; width: 100%;
background-color: var(--bg-main-color); background-color: var(--bg-main-color);
} }
@@ -81,7 +82,7 @@ body,
} }
.layout-view-bg-white { .layout-view-bg-white {
background: white; background: var(--bg-main-color);
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 4px; border-radius: 4px;
@@ -107,11 +108,6 @@ body,
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 { .layout-mian-height-50 {
height: calc(100vh - 50px); height: calc(100vh - 50px);
} }
@@ -125,6 +121,11 @@ body,
.layout-hide { .layout-hide {
display: none; display: none;
} }
.el-footer {
height: auto;
padding: 0;
}
} }
/* element plus 全局样式 /* element plus 全局样式
@@ -190,6 +191,23 @@ body,
} }
} }
.flex-justify-between {
display: flex;
align-items: center; // 垂直方向水平居中
justify-content: space-between; // 使第一个子元素靠近父容器的起始位置,最后一个子元素靠近终止位置,而其他子元素均匀分布在它们之间
}
.flex-all-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-align-center {
display: flex;
align-items: center; // 垂直方向水平居中
}
/* 宽高 100% /* 宽高 100%
------------------------------- */ ------------------------------- */
.w100 { .w100 {
@@ -236,7 +254,7 @@ body,
/* 字体大小全局样式 /* 字体大小全局样式
------------------------------- */ ------------------------------- */
@for $i from 10 through 32 { @for $i from 8 through 32 {
.font#{$i} { .font#{$i} {
font-size: #{$i}px !important; font-size: #{$i}px !important;
} }
@@ -276,6 +294,10 @@ body,
.pl#{$i} { .pl#{$i} {
padding-left: #{$i}px !important; padding-left: #{$i}px !important;
} }
.pd#{$i} {
padding: #{$i}px !important;
}
} }
@@ -335,12 +357,23 @@ body,
user-select: none; user-select: none;
} }
.toolbar { /* custom card */
width: 100%; .card {
padding: 4px; box-sizing: border-box;
overflow: hidden; padding: 20px;
line-height: 24px; overflow-x: hidden;
border: 1px solid var(--el-border-color-light, #ebeef5); 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%);
}
/* content-box (常用内容盒子) */
.content-box {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
} }
.fl { .fl {
@@ -361,10 +394,6 @@ body,
z-index: inherit !important; z-index: inherit !important;
} }
.f12 {
font-size: 12px
}
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
@@ -373,6 +402,46 @@ body,
cursor: pointer; cursor: pointer;
transition: color 0.3s; transition: color 0.3s;
} }
.pointer-icon:hover { .pointer-icon:hover {
color: var(--el-color-primary); /* 鼠标移动到图标时的颜色 */ color: var(--el-color-primary);
/* 鼠标移动到图标时的颜色 */
}
/** splitpanes **/
.splitpanes.default-theme .splitpanes {
background: transparent !important;
}
.splitpanes.default-theme .splitpanes__pane {
background: transparent !important;
}
.default-theme.splitpanes--vertical>.splitpanes__splitter {
border-left: 1px solid var(--bg-main-color);
}
.default-theme.splitpanes--horizontal>.splitpanes__splitter {
border-top: 1px solid var(--bg-main-color);
}
// 竖线样式
.splitpanes.default-theme .splitpanes__splitter::before,
.splitpanes.default-theme .splitpanes__splitter::after {
background-color: var(--el-color-info-light-5);
}
.splitpanes.default-theme .splitpanes__splitter:hover::before,
.splitpanes.default-theme .splitpanes__splitter:hover::after {
background-color: var(--el-color-success);
}
.splitpanes.default-theme .splitpanes__splitter {
min-width: 6px;
background: var(--el-color-info-light-8) !important;
}
.splitpanes.default-theme .splitpanes__splitter:hover {
background: var(--el-color-success-light-8) !important;
} }

View File

@@ -7,335 +7,353 @@
------------------------------- */ ------------------------------- */
// 菜单搜索 // 菜单搜索
.el-autocomplete-suggestion__wrap { .el-autocomplete-suggestion__wrap {
max-height: 280px !important; max-height: 280px !important;
} }
/* Form 表单 /* Form 表单
------------------------------- */ ------------------------------- */
.el-form { .el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type { // 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
margin-bottom: 0 !important; .el-form-item:last-of-type {
} margin-bottom: 0 !important;
// 修复行内表单最后一个 el-form-item 位置下移问题 }
&.el-form--inline {
.el-form-item--large.el-form-item:last-of-type { // 修复行内表单最后一个 el-form-item 位置下移问题
margin-bottom: 22px !important; &.el-form--inline {
} .el-form-item--large.el-form-item:last-of-type {
.el-form-item--default.el-form-item:last-of-type, margin-bottom: 22px !important;
.el-form-item--small.el-form-item:last-of-type { }
margin-bottom: 18px !important;
} .el-form-item--default.el-form-item:last-of-type,
} .el-form-item--small.el-form-item:last-of-type {
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM margin-bottom: 18px !important;
.el-form-item .el-form-item__label .el-icon { }
margin-right: 0px; }
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
} }
/* Alert 警告 /* Alert 警告
------------------------------- */ ------------------------------- */
.el-alert { .el-alert {
border: 1px solid; border: 1px solid;
} }
.el-alert__title { .el-alert__title {
word-break: break-all; word-break: break-all;
} }
/* Message 消息提示 /* Message 消息提示
------------------------------- */ ------------------------------- */
.el-message { .el-message {
min-width: unset !important; min-width: unset !important;
padding: 15px !important; padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
} }
/* NavMenu 导航菜单 /* NavMenu 导航菜单
------------------------------- */ ------------------------------- */
// 鼠标 hover 时颜色 // 鼠标 hover 时颜色
.el-menu-hover-bg-color { .el-menu-hover-bg-color {
background-color: var(--bg-menuBarActiveColor) !important; background-color: var(--bg-menuBarActiveColor) !important;
} }
// 默认样式修改 // 默认样式修改
.el-menu { .el-menu {
border-right: none !important; border-right: none !important;
width: 220px; width: 220px;
} }
.el-menu-item { .el-menu-item {
height: 56px !important; height: 56px !important;
line-height: 56px !important; line-height: 56px !important;
} }
.el-menu-item, .el-menu-item,
.el-sub-menu__title { .el-sub-menu__title {
color: var(--bg-menuBarColor); color: var(--bg-menuBarColor);
} }
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题 // 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse { .el-menu--collapse {
width: 64px !important; width: 64px !important;
} }
// 外部链接时 // 外部链接时
.el-menu-item a, .el-menu-item a,
.el-menu-item a:hover, .el-menu-item a:hover,
.el-menu-item i, .el-menu-item i,
.el-sub-menu__title i { .el-sub-menu__title i {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
// 第三方图标字体间距/大小设置 // 第三方图标字体间距/大小设置
.el-menu-item .iconfont, .el-menu-item .iconfont,
.el-sub-menu .iconfont, .el-sub-menu .iconfont,
.el-menu-item .fa, .el-menu-item .fa,
.el-sub-menu .fa { .el-sub-menu .fa {
@include generalIcon; @include generalIcon;
} }
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色 // 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active, .el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title, .el-sub-menu.is-active .el-sub-menu__title,
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title { .el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
@extend .el-menu-hover-bg-color; @extend .el-menu-hover-bg-color;
} }
.el-menu-item:hover { .el-menu-item:hover {
@extend .el-menu-hover-bg-color; @extend .el-menu-hover-bg-color;
} }
.el-sub-menu.is-active.is-opened .el-sub-menu__title { .el-sub-menu.is-active.is-opened .el-sub-menu__title {
background-color: unset !important; background-color: unset !important;
} }
// 子级菜单背景颜色 // 子级菜单背景颜色
// .el-menu--inline { // .el-menu--inline {
// background: var(--next-bg-menuBar-light-1); // background: var(--next-bg-menuBar-light-1);
// } // }
// 水平菜单、横向菜单折叠 a 标签 // 水平菜单、横向菜单折叠 a 标签
.el-popper.is-dark a { .el-popper.is-dark a {
color: var(--el-color-white) !important; color: var(--el-color-white) !important;
text-decoration: none; text-decoration: none;
} }
// 水平菜单、横向菜单折叠背景色 // 水平菜单、横向菜单折叠背景色
.el-popper.is-pure.is-light { .el-popper.is-pure.is-light {
// 水平菜单
.el-menu--vertical { // 水平菜单
background: var(--bg-menuBar); .el-menu--vertical {
.el-sub-menu.is-active .el-sub-menu__title { background: var(--bg-menuBar);
color: var(--el-menu-active-color);
} .el-sub-menu.is-active .el-sub-menu__title {
.el-popper.is-pure.is-light { color: var(--el-menu-active-color);
.el-menu--vertical { }
.el-sub-menu .el-sub-menu__title {
background-color: unset !important; .el-popper.is-pure.is-light {
color: var(--bg-menuBarColor); .el-menu--vertical {
} .el-sub-menu .el-sub-menu__title {
.el-sub-menu.is-active .el-sub-menu__title { background-color: unset !important;
color: var(--el-menu-active-color); color: var(--bg-menuBarColor);
} }
}
} .el-sub-menu.is-active .el-sub-menu__title {
} color: var(--el-menu-active-color);
// 横向菜单 }
.el-menu--horizontal { }
background: var(--bg-topBar); }
.el-menu-item, }
.el-sub-menu {
height: 48px !important; // 横向菜单
line-height: 48px !important; .el-menu--horizontal {
color: var(--bg-topBarColor); background: var(--bg-topBar);
.el-sub-menu__title {
height: 48px !important; .el-menu-item,
line-height: 48px !important; .el-sub-menu {
color: var(--bg-topBarColor); height: 48px !important;
} line-height: 48px !important;
.el-popper.is-pure.is-light { color: var(--bg-topBarColor);
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title { .el-sub-menu__title {
background-color: unset !important; height: 48px !important;
color: var(--bg-topBarColor); line-height: 48px !important;
} color: var(--bg-topBarColor);
.el-sub-menu.is-active .el-sub-menu__title { }
color: var(--el-menu-active-color);
} .el-popper.is-pure.is-light {
} .el-menu--horizontal {
} .el-sub-menu .el-sub-menu__title {
} background-color: unset !important;
.el-menu-item.is-active, color: var(--bg-topBarColor);
.el-sub-menu.is-active .el-sub-menu__title { }
color: var(--el-menu-active-color);
} .el-sub-menu.is-active .el-sub-menu__title {
} color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
} }
// 横向菜单(经典、横向)布局 // 横向菜单(经典、横向)布局
.el-menu.el-menu--horizontal { .el-menu.el-menu--horizontal {
border-bottom: none !important; border-bottom: none !important;
width: 100% !important; width: 100% !important;
.el-menu-item,
.el-sub-menu__title { .el-menu-item,
height: 48px !important; .el-sub-menu__title {
color: var(--bg-topBarColor); height: 48px !important;
} color: var(--bg-topBarColor);
.el-menu-item:not(.is-active):hover, }
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor); .el-menu-item:not(.is-active):hover,
} .el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
}
} }
// 菜单收起时,图标不居中问题 // 菜单收起时,图标不居中问题
.el-menu--collapse { .el-menu--collapse {
.el-menu-item .iconfont,
.el-sub-menu .iconfont, .el-menu-item .iconfont,
.el-menu-item .fa, .el-sub-menu .iconfont,
.el-sub-menu .fa { .el-menu-item .fa,
margin-right: 0 !important; .el-sub-menu .fa {
} margin-right: 0 !important;
.el-sub-menu__title { }
padding-right: 0 !important;
} .el-sub-menu__title {
padding-right: 0 !important;
}
} }
/* Tabs 标签页 /* Tabs 标签页
------------------------------- */ ------------------------------- */
.el-tabs__nav-wrap::after { .el-tabs__nav-wrap::after {
height: 1px !important; height: 1px !important;
} }
/* Dropdown 下拉菜单 /* Dropdown 下拉菜单
------------------------------- */ ------------------------------- */
.el-dropdown-menu { .el-dropdown-menu {
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/ list-style: none !important;
} /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
.el-dropdown-menu .el-dropdown-menu__item {
white-space: nowrap;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
} }
/* Steps 步骤条 .el-dropdown-menu .el-dropdown-menu__item {
------------------------------- */ white-space: nowrap;
.el-step__icon-inner {
font-size: 30px !important; &:not(.is-disabled):hover {
font-weight: 400 !important; background-color: var(--el-dropdown-menuItem-hover-fill);
} color: var(--el-dropdown-menuItem-hover-color);
.el-step__title { }
font-size: 14px;
} }
/* Dialog 对话框 /* Dialog 对话框
------------------------------- */ ------------------------------- */
.el-overlay { .el-overlay {
overflow: hidden; overflow: hidden;
.el-overlay-dialog {
display: flex; .el-overlay-dialog {
align-items: center; display: flex;
justify-content: center; align-items: center;
position: unset !important; justify-content: center;
width: 100%; position: unset !important;
height: 100%; width: 100%;
.el-dialog { height: 100%;
margin: 0 auto !important;
position: absolute; .el-dialog {
.el-dialog__body { margin: 0 auto !important;
padding: 20px !important; position: absolute;
}
} .el-dialog__body {
} padding: 20px !important;
}
}
}
} }
.el-dialog__body { .el-dialog__body {
max-height: calc(90vh - 111px) !important; max-height: calc(90vh - 111px) !important;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
} }
/* Card 卡片 /* Card 卡片
------------------------------- */ ------------------------------- */
.el-card__header { .el-card__header {
padding: 15px 20px; padding: 15px 20px;
} }
/* Table 表格 element plus 2.2.0 版本 /* Table 表格 element plus 2.2.0 版本
------------------------------- */ ------------------------------- */
.el-table { .el-table {
.el-button.is-text { .el-button.is-text {
padding: 0; padding: 0;
} }
} }
/* scrollbar /* scrollbar
------------------------------- */ ------------------------------- */
.el-scrollbar__bar { .el-scrollbar__bar {
z-index: 4; z-index: 4;
} }
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/ /*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
.el-scrollbar__wrap { .el-scrollbar__wrap {
max-height: 100%; max-height: 100%;
} }
.el-select-dropdown .el-scrollbar__wrap { .el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important; overflow-x: scroll !important;
} }
/*修复Select 选择器高度问题*/ /*修复Select 选择器高度问题*/
.el-select-dropdown__wrap { .el-select-dropdown__wrap {
max-height: 274px !important; max-height: 274px !important;
} }
/*修复Cascader 级联选择器高度问题*/ /*修复Cascader 级联选择器高度问题*/
.el-cascader-menu__wrap.el-scrollbar__wrap { .el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important; height: 204px !important;
} }
/*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/ /*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/
.layout-container-view .el-scrollbar__view { .layout-container-view .el-scrollbar__view {
height: 100%; height: 100%;
} }
/*防止分栏布局二级菜单很多时,滚动条消失问题*/ /*防止分栏布局二级菜单很多时,滚动条消失问题*/
.layout-columns-warp .layout-aside .el-scrollbar__view { .layout-columns-warp .layout-aside .el-scrollbar__view {
height: unset !important; height: unset !important;
} }
/* Pagination 分页 /* Pagination 分页
------------------------------- */ ------------------------------- */
.el-pagination__editor { .el-pagination__editor {
margin-right: 8px; margin-right: 8px;
} }
/*深色模式时分页高亮问题*/ /*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active, .el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active, .el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active { .el-pagination.is-background .el-pager li.is-active {
background-color: var(--el-color-primary) !important; background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important; color: var(--el-color-white) !important;
}
/* Drawer 抽屉
------------------------------- */
.el-drawer {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
color: var(--el-text-color-primary);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
} }
/* Breadcrumb 面包屑 /* Breadcrumb 面包屑
------------------------------- */ ------------------------------- */
.el-breadcrumb__inner a:hover, .el-breadcrumb__inner a:hover,
.el-breadcrumb__inner.is-link:hover { .el-breadcrumb__inner.is-link:hover {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
.el-breadcrumb__inner a, .el-breadcrumb__inner a,
.el-breadcrumb__inner.is-link { .el-breadcrumb__inner.is-link {
color: var(--bg-topBarColor); color: var(--bg-topBarColor);
font-weight: normal; font-weight: normal;
} }
// el-tooltip使用自定义主题时的样式 // el-tooltip使用自定义主题时的样式
.el-popper.is-customized { .el-popper.is-customized {
/* Set padding to ensure the height is 32px */ /* Set padding to ensure the height is 32px */
// padding: 6px 12px; // padding: 6px 12px;
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129)); background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
} }
.el-popper.is-customized .el-popper__arrow::before { .el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689); background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0; right: 0;
@@ -343,7 +361,9 @@
.el-dialog { .el-dialog {
border-radius: 6px; /* 设置圆角 */ border-radius: 6px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */ /* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
/* 添加轻微阴影效果 */
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
} }

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