190 Commits

Author SHA1 Message Date
meilin.huang
e4d949a64b fix: machine file bug 2024-12-26 12:17:58 +08:00
zongyangleo
3f6fb5afef !129 fix: db 相关bug
* fix: db 相关bug
2024-12-26 04:11:28 +00:00
meilin.huang
68f553f4b0 refactor: remove router、ioc is adjusted to inject by type 2024-12-16 23:29:18 +08:00
meilin.huang
7f2a49ba3c fix: 机器文件内容写入导致内容清空、feat: ioc支持根据类型注入 2024-12-13 12:15:24 +08:00
meilin.huang
e56788af3e refactor: dbm 2024-12-08 13:04:23 +08:00
meilin.huang
ebc89e056f fix: fixed some minor issues 2024-11-28 20:11:16 +08:00
zongyangleo
d07cd74a8c !127 fix: 达梦特殊字段类型覆盖解析
* fix: 达梦特殊字段类型覆盖解析
2024-11-28 10:44:49 +00:00
meilin.huang
6cc15ebeda feat: tag refactor & other 2024-11-26 17:32:44 +08:00
zongyangleo
2b712cd548 !126 feat: 解析达梦特殊字段
* feat: 解析达梦特殊字段
2024-11-26 04:04:09 +00:00
meilin.huang
cda2963e1c fix: i18n & other optimizations 2024-11-23 17:23:18 +08:00
meilin.huang
bffa9c2676 fix: i18n 2024-11-21 20:12:16 +08:00
meilin.huang
9366a76b84 release: v1.9.1 2024-11-21 19:02:15 +08:00
meilin.huang
e03ceecd9a fix: redis set command causes ttl loss 2024-11-21 13:20:45 +08:00
meilin.huang
99a746085b feat: i18n 2024-11-20 22:43:53 +08:00
meilin.huang
74ae031853 refactor: dbm重构、调整metadata与dialect接口 2024-11-01 17:27:22 +08:00
meilin.huang
af14be9801 refactor: 前端文件夹名称调整 2024-10-31 17:24:56 +08:00
meilin.huang
df7413a9ea fix: base.app事务方法调整 2024-10-30 19:59:45 +08:00
meilin.huang
e967e02095 fix: 数据库实例删除,事务问题 2024-10-30 12:17:55 +08:00
meilin.huang
c1d09f447d fix: pgsql查询列信息解析问题修复等 2024-10-28 12:13:41 +08:00
meilin.huang
5e5afd49dc fix: sql缺少字段补充、dockefile调整等 2024-10-24 11:56:22 +08:00
meilin.huang
2118acf244 release: v1.9.0 2024-10-23 17:30:05 +08:00
meilin.huang
44a1bd626e feat: 数据库迁移至文件支持文件保存天数等 2024-10-22 20:39:44 +08:00
meilin.huang
ea3c70a8a8 feat: 新增统一文件模块,统一文件操作 2024-10-21 22:27:42 +08:00
zongyangleo
6343173cf8 !124 一些更新和bug
* fix: 代码合并
* feat:支持数据库版本兼容,目前兼容了oracle11g部分特性
* fix: 修改数据同步bug,数据sql里指定修改字段别,导致未正确记录修改字段值
* feat: 数据库迁移支持定时迁移和迁移到sql文件
2024-10-20 03:52:23 +00:00
meilin.huang
6837a9c867 fix: sql切割转义等问题处理 2024-10-18 17:15:58 +08:00
meilin.huang
a726927a28 feat: sql脚本执行调整 2024-10-18 12:32:53 +08:00
meilin.huang
e135e4ce64 feat: sql解析器替换、工单统一由‘我的流程’发起、流程定义支持自定义条件触发审批、资源隐藏编号、model支持物理删除等 2024-10-16 17:24:50 +08:00
zongyangleo
43edef412c !123 一些bug修复
* fix: 数据同步、数据迁移体验优化
* fix: des加密传输sql
* fix: 修复达梦字段注释显示问题
* fix: mysql timestamp 字段类型导出ddl错误修复
2024-08-22 00:43:39 +00:00
meilin.huang
2deb3109c2 feat: dbms表数据新增表单视图 2024-07-19 17:06:11 +08:00
meilin.huang
a80221a950 fix: 数据库实例删除等问题修复 2024-07-05 13:14:31 +08:00
meilin.huang
10630847df fix: 工单流程信息展示问题修复 2024-06-24 17:17:57 +08:00
meilin.huang
f43851698e fix: 资源关联多标签删除、数据库实例删除等问题修复与数据库等名称过滤优化 2024-06-07 12:31:40 +08:00
Coder慌
73884bb693 !122 fix: mysql导出修复
Merge pull request !122 from zongyangleo/dev_1.8.6_fix
2024-06-05 04:17:03 +00:00
zongyangleo
1b5bb1de8b fix: mysql导出修复 2024-06-01 13:35:31 +08:00
meilin.huang
4814793546 fix: 修复数据库表数据横向滚动后切换tab导致表头错位&数据取消居中显示 2024-05-31 12:12:40 +08:00
meilin.huang
d85bbff270 release v1.8.6 2024-05-23 17:18:22 +08:00
meilin.huang
bb1522f4dc refactor: 数据库管理迁移至数据库实例-库管理、机器管理-文件支持用户和组信息查看 2024-05-21 12:34:26 +08:00
zongyangleo
a7632fbf58 !121 fix: rdp ssh
* fix: rdp ssh
2024-05-21 04:06:13 +00:00
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
1065 changed files with 473939 additions and 23395 deletions

View File

@@ -1,41 +1,26 @@
# 构建前端资源
FROM node:18-alpine3.16 as fe-builder
ARG BASEIMAGES=m.daocloud.io/docker.io/alpine:3.20.2
WORKDIR /mayfly
FROM ${BASEIMAGES} AS builder
ARG TARGETARCH
COPY mayfly_go_web .
ARG MAYFLY_GO_VERSION
ARG MAYFLY_GO_DIR_NAME=mayfly-go-linux-${TARGETARCH}
ARG MAYFLY_GO_URL=https://gitee.com/dromara/mayfly-go/releases/download/${MAYFLY_GO_VERSION}/${MAYFLY_GO_DIR_NAME}.zip
RUN yarn
RUN wget -cO mayfly-go.zip ${MAYFLY_GO_URL} && \
unzip mayfly-go.zip && \
mv ${MAYFLY_GO_DIR_NAME}/* /opt
RUN yarn build
# 构建后端资源
FROM golang:1.21.0 as be-builder
FROM ${BASEIMAGES}
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
# Copy the go source for building server
COPY server .
RUN go mod tidy && go mod download
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \
-o mayfly-go main.go
FROM alpine:3.16
RUN apk add --no-cache ca-certificates bash expat
ENV TZ=Asia/Shanghai
ARG TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /mayfly
COPY --from=builder /opt/mayfly-go /usr/local/bin/mayfly-go
COPY --from=be-builder /mayfly/mayfly-go /usr/local/bin/mayfly-go
WORKDIR /mayfly-go
CMD ["mayfly-go"]
EXPOSE 18888
CMD ["mayfly-go"]

39
Dockerfile.sourcebuild Normal file
View File

@@ -0,0 +1,39 @@
# 构建前端资源
FROM m.daocloud.io/docker.io/node:18-bookworm-slim AS fe-builder
WORKDIR /mayfly
COPY frontend .
RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn install && \
yarn build
# 构建后端资源
FROM m.daocloud.io/docker.io/golang:1.23 AS be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
# Copy the go source for building server
COPY server .
RUN go mod tidy && go mod download
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a -ldflags=-w \
-o mayfly-go main.go
FROM m.daocloud.io/docker.io/alpine:3.20.2
ARG TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /mayfly-go
COPY --from=be-builder /mayfly/mayfly-go /usr/local/bin/mayfly-go
CMD ["mayfly-go"]

View File

@@ -1,4 +1,10 @@
# 🌈mayfly-go
# 🌈Dromara mayfly-go
<p align="center">
<a href="./README_EN.md">English</a> |
<a href="https://www.yuque.com/may-fly/mayfly-go">项目文档</a> |
<a href="https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854">操作视频</a> |
</p>
<p align="center">
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
@@ -13,91 +19,91 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
</a>
</p>
### 介绍
## 前言
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)数据库mysql postgres)、redis(单机 哨兵 集群)mongo 统一管理操作平台**
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据操作、数据同步、数据迁移。redis(单机 哨兵 集群)mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架
## 开发语言与主要框架
- 前端typescript、vue3、element-plus
- 后端golang、gin、gorm
### 交流及问题反馈加 QQ 群
## 交流及问题反馈加 QQ 群
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=IdJSHW0jTMhmWFHBUS9a83wxtrxDDhFj&jump_from=webapi">119699946</a>
### 系统相关资料
- 项目文档: https://www.yuque.com/may-fly/mayfly-go
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
### 演示环境
## 演示环境
http://go.mayfly.run
账号/密码test/test123.
### 系统核心功能截图
## 系统核心功能截图
##### 记录操作记录
#### 首页
![记录操作记录](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终端](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://objs.gitee.io/mayfly-go-docs/home/file-content-update.jpg "屏幕截图.png")
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### 数据库操作
##### 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 操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png")
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### 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
**其他更多功能&操作指南可查看上述项目文档**
#### 💌 支持作者
## 💌 支持作者
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。

105
README_EN.md Normal file
View File

@@ -0,0 +1,105 @@
# 🌈Dromara mayfly-go
<p align="center">
<a href="./README.md">中文介绍</a> |
<a href="https://www.yuque.com/may-fly/mayfly-go">Documentation</a> |
<a href="https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854">Operate Video</a>
</p>
<p align="center">
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
</a>
<a href="https://github.com/dromara/mayfly-go" target="_blank">
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
</a>
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
</a>
</p>
## Preface
Browser-based management platform. **linux(Terminal [terminal playback, command filtering], file, script, process, cronjob), database (mysql, postgres, oracle, sqlserver, Dameng, gauss, sqlite) data operation, data synchronization, data migration, redis(standlone, sentinel, cluster), mongo and other unified management and operation platforms that integrate work order process approval.**
## Development languages and major frameworks
- frontendtypescript、vue3、element-plus
- backendgolang、gin、gorm
## Demo
http://go.mayfly.run
account/passwordtest/test123.
## Screenshots of core features
#### Home page
![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
#### Machine Operation
##### Status
![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
##### SSH Terminal
![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### File Operation
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### Database Operation
##### SQL Editor
![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### Add, delete, update and check data online
![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis Operation
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo Operation
![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
#### Work order process approval
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")
#### System Management
##### Account
![账号管理](https://foruda.gitee.com/images/1714379179491881231/c6d802ae_1240250.png "屏幕截图")
##### Role
![角色管理](https://foruda.gitee.com/images/1714379269408676381/6ac1e85c_1240250.png "屏幕截图")
##### Menu & Permission
![菜单资源管理](https://foruda.gitee.com/images/1714379321338009940/a00d6a02_1240250.png "屏幕截图")
**Additional features & instructions can be found in the project documentation above.**
## 💌 Supporting Author
If you think the project is good, or you are already using it, I hope you can go to <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> to help me click ⭐ Star, which will be a great encouragement and support for me.

View File

@@ -8,7 +8,7 @@ project_path=`pwd`
# 构建后的二进制执行文件名
exec_file_name="mayfly-go"
# web项目目录
web_folder="${project_path}/mayfly_go_web"
web_folder="${project_path}/frontend"
# server目录
server_folder="${project_path}/server"
@@ -28,13 +28,13 @@ function buildWeb() {
cd ${web_folder}
copy2Server=$1
echo_yellow "-------------------打包前端开始-------------------"
echo_yellow "-------------------Start bundling frontends-------------------"
yarn run build
if [ "${copy2Server}" == "2" ] ; then
echo_green '将打包后的静态文件拷贝至server/static/static'
echo_green 'Copy the packaged static files to server/static/static'
rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>打包前端结束<<<<<<<<<<<<<<<<<<<<\n"
echo_yellow ">>>>>>>>>>>>>>>>>>>End of packaging frontend<<<<<<<<<<<<<<<<<<<<\n"
}
function build() {
@@ -46,26 +46,27 @@ function build() {
arch=$3
copyDocScript=$4
echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
echo_yellow "-------------------Start a bundle build - ${os}-${arch}-------------------"
cd ${server_folder}
echo_green "打包构建可执行文件..."
echo_green "Package build executables..."
execFileName=${exec_file_name}
# 如果是windows系统,可执行文件需要添加.exe结尾
if [ "${os}" == "windows" ];then
execFileName="${execFileName}.exe"
fi
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -o ${execFileName} main.go
go mod tidy
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
echo_green "目标文件夹已存在,清空文件夹"
echo_green "The desired folder already exists. Clear the folder"
sudo rm -rf ${toFolder}
fi
echo_green "创建'${toFolder}'目录"
echo_green "Create '${toFolder}' Directory"
mkdir ${toFolder}
echo_green "移动二进制文件至'${toFolder}'"
echo_green "Move binary to '${toFolder}'"
mv ${server_folder}/${execFileName} ${toFolder}
# if [ "${copy2Server}" == "1" ] ; then
@@ -74,16 +75,17 @@ function build() {
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
echo_green "Copy resources such as scripts [config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/readme_en.txt ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
echo_yellow ">>>>>>>>>>>>>>>>>>> ${os}-${arch} - Bundle build complete <<<<<<<<<<<<<<<<<<<<\n"
}
function buildLinuxAmd64() {
@@ -103,25 +105,25 @@ function buildMac() {
}
function buildDocker() {
echo_yellow "-------------------构建docker镜像开始-------------------"
echo_yellow "-------------------Start building the docker image-------------------"
imageVersion=$1
imageName="mayflygo/mayfly-go:${imageVersion}"
docker build --platform linux/amd64 -t "${imageName}" .
echo_green "docker镜像构建完成->[${imageName}]"
echo_yellow "-------------------构建docker镜像结束-------------------"
imageName="mayfly/mayfly-go:${imageVersion}"
docker build --no-cache --platform linux/amd64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
echo_green "The docker image is built -> [${imageName}]"
echo_yellow "-------------------Finished building the docker image-------------------"
}
function buildxDocker() {
echo_yellow "-------------------docker buildx构建镜像开始-------------------"
echo_yellow "-------------------The docker buildx build image starts-------------------"
imageVersion=$1
imageName="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${imageVersion}"
docker buildx build --push --platform linux/amd64,linux/arm64 -t "${imageName}" .
echo_green "docker多版本镜像构建完成->[${imageName}]"
echo_yellow "-------------------docker buildx构建镜像结束-------------------"
docker buildx build --no-cache --push --platform linux/amd64,linux/arm64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
echo_green "The docker multi-architecture version image is built -> [${imageName}]"
echo_yellow "-------------------The docker buildx image is finished-------------------"
}
function runBuild() {
read -p "请选择构建版本[0|其他->除docker镜像外其他 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
read -p "Select build version [0 | Other->Other than docker image 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
toPath="."
imageVersion="latest"
@@ -129,16 +131,16 @@ function runBuild() {
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
# 构建结果的目的路径
read -p "请输入构建产物输出目录[默认当前路径]: " toPath
read -p "Please enter the build product output directory [default current path]: " toPath
if [ ! -d ${toPath} ] ; then
echo_red "构建产物输出目录不存在!"
echo_red "Build product output directory does not exist!"
exit;
fi
if [ "${toPath}" == "" ] ; then
toPath="."
fi
read -p "是否拷贝文档&脚本[0-> 1->是][默认是]: " copyDocScript
read -p "Whether to copy documents & Scripts [0-> No 1-> Yes][Default yes]: " copyDocScript
if [ "${copyDocScript}" == "" ] ; then
copyDocScript="1"
fi
@@ -154,7 +156,7 @@ function runBuild() {
fi
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
read -p "请输入docker镜像版本号[默认latest]: " imageVersion
read -p "Please enter the docker image version (default latest) : " imageVersion
if [ "${imageVersion}" == "" ] ; then
imageVersion="latest"
@@ -189,7 +191,7 @@ function runBuild() {
esac
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
echo_green "删除['${server_folder}/static/static']下静态资源文件."
echo_green "Delete static assets under ['${server_folder}/static/static']."
# 删除静态资源文件保留一个favicon.ico否则后端启动会报错
rm -rf ${server_folder}/static/static/assets
rm -rf ${server_folder}/static/static/config.js

View File

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

View File

@@ -5,4 +5,6 @@ VITE_PORT = 8889
VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea

View File

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

View File

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

21
frontend/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="keywords" content="mayfly-go" />
<meta name="description" content="" />
<link type="favicon" rel="shortcut icon" href="favicon.ico" />
<title>mayfly-go</title>
</head>
<body>
<div id="app"></div>
<script type="application/javascript" src="./config.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

72
frontend/package.json Normal file
View File

@@ -0,0 +1,72 @@
{
"name": "mayfly-go-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-preview": "npm run build && npm run preview",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^12.0.0",
"asciinema-player": "^3.8.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.9.1",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.3.0",
"qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"splitpanes": "^3.1.5",
"sql-formatter": "^15.4.5",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.13",
"vue-i18n": "^10.0.5",
"vue-router": "^4.5.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-web-links": "^0.9.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.14.178",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/compiler-sfc": "^3.5.13",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.31.0",
"prettier": "^3.2.5",
"sass": "^1.82.0",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vue-eslint-parser": "^9.4.3"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,34 +1,37 @@
<template>
<div class="h100">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
:content="themeConfig.watermarkText"
class="h100"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<div class="h100">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="h100"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
</el-config-provider>
</template>
<script setup lang="ts" name="app">
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLocal } from '@/common/utils/storage';
import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { getThemeConfig } from './common/utils/storage';
import { useWatermark } from '@/common/sysconfig';
import { useIntervalFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import EnumValue from './common/Enum';
import { I18nEnum } from './common/commonEnum';
import { saveThemeConfig } from './common/utils/storage';
const setingsRef = ref();
const route = useRoute();
@@ -36,6 +39,9 @@ const route = useRoute();
const themeConfigStores = useThemeConfig();
const { themeConfig } = storeToRefs(themeConfigStores);
//
const { locale, t } = useI18n();
//
const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
@@ -49,17 +55,8 @@ onMounted(() => {
openSetingsDrawer();
});
//
const tc = getThemeConfig();
if (tc) {
themeConfigStores.setThemeConfig({ themeConfig: tc });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
}
//
useWatermark().then((res) => {
themeConfigStores.setWatermarkConfig(res);
});
//
themeConfigStores.initThemeConfig();
});
});
@@ -79,6 +76,31 @@ watch(
}
);
watch(
() => themeConfig.value.globalI18n,
(val) => {
locale.value = val;
}
);
watch(
themeConfig,
(val) => {
saveThemeConfig(val);
},
{ deep: true }
);
//
const getGlobalComponentSize = computed(() => {
return themeConfig.value.globalComponentSize;
});
// i18n
const getGlobalI18n = computed(() => {
return EnumValue.getEnumByValue(I18nEnum, locale.value)?.extra.el;
});
//
const { pause, resume } = useIntervalFn(() => {
if (!themeConfig.value.isWatermark) {
@@ -108,7 +130,7 @@ watch(
() => route.path,
() => {
nextTick(() => {
document.title = `${route.meta.title} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
document.title = `${t((route.meta.title as string) || '')} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
});
}
);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,121 @@
{
"id": "3953964",
"name": "mayfly-go",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "2967035",
"name": "符号-英文",
"font_class": "fuhao-yingwen",
"unicode": "e712",
"unicode_decimal": 59154
},
{
"icon_id": "26283783",
"name": "符号-中文",
"font_class": "fuhao-zhongwen",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "23957582",
"name": "MongoDB",
"font_class": "mongo",
"unicode": "e646",
"unicode_decimal": 58950
},
{
"icon_id": "4969649",
"name": "Redis",
"font_class": "op-redis",
"unicode": "e728",
"unicode_decimal": 59176
},
{
"icon_id": "22442993",
"name": "PostgreSQL",
"font_class": "op-postgres",
"unicode": "e8b7",
"unicode_decimal": 59575
},
{
"icon_id": "12295203",
"name": "达梦数据库",
"font_class": "db-dm",
"unicode": "e6f0",
"unicode_decimal": 59120
},
{
"icon_id": "10055634",
"name": "云数据库MongoDB",
"font_class": "op-mongo",
"unicode": "e7d7",
"unicode_decimal": 59351
},
{
"icon_id": "10055642",
"name": "云数据库 RDS MySQL",
"font_class": "op-mysql",
"unicode": "e7d8",
"unicode_decimal": 59352
},
{
"icon_id": "3876165",
"name": "redis",
"font_class": "redis",
"unicode": "e619",
"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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,5 +1,5 @@
import request from './request';
import { useApiFetch } from './useRequest';
import { useApiFetch } from '@/hooks/useRequest';
/**
* api请求
@@ -69,7 +69,7 @@ class Api {
*/
async xhrReq(param: any = null, options: any = {}): Promise<any> {
if (this.beforeHandler) {
this.beforeHandler(param);
await this.beforeHandler(param);
}
return request.xhrReq(this.method, this.url, param, options);
}

View File

@@ -22,6 +22,8 @@ export class EnumValue {
*/
tag: EnumValueTag;
extra: any;
constructor(value: any, label: string) {
this.value = value;
this.label = label;
@@ -53,6 +55,11 @@ export class EnumValue {
return this;
}
setExtra(extra: any): EnumValue {
this.extra = extra;
return this;
}
public static of(value: any, label: string): EnumValue {
return new EnumValue(value, label);
}
@@ -60,11 +67,12 @@ export class EnumValue {
/**
*
*
* @param enumValues
* @param enums
* @param value
* @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) {
if (enumValue.value == value) {
return enumValue;

View File

@@ -1,3 +1,5 @@
import { i18n } from '@/i18n';
/**
*
*/
@@ -28,7 +30,22 @@ export function isTrue(condition: boolean, msg: string) {
* @param msg
*/
export function notBlank(obj: any, msg: string) {
isTrue(obj, msg);
if (obj == null || obj == undefined || obj == '') {
throw new AssertError(msg);
}
if (Array.isArray(obj) && obj.length == 0) {
throw new AssertError(msg);
}
}
/**
* null,0,''
*
* @param obj
* @param field i18n msgKey
*/
export function notBlankI18n(obj: any, field: string) {
notBlank(obj, i18n.global.t('common.fieldNotEmpty', { field: i18n.global.t(field) }));
}
/**
@@ -65,3 +82,13 @@ export function notEmpty(str: string, msg: string) {
throw new AssertError(msg);
}
}
/**
*
*
* @param str
* @param field i18n msgKey
*/
export function notEmptyI18n(str: string, field: string) {
notEmpty(str, i18n.global.t('common.fieldNotEmpty', { field: i18n.global.t(field) }));
}

View File

@@ -0,0 +1,40 @@
import EnumValue from './Enum';
// element plus 自带国际化
import zhcnLocale from 'element-plus/es/locale/lang/zh-cn';
import enLocale from 'element-plus/es/locale/lang/en';
// i18n
export const I18nEnum = {
ZhCn: EnumValue.of('zh-cn', '简体中文').setExtra({ icon: 'iconfont icon-fuhao-zhongwen', el: zhcnLocale }),
En: EnumValue.of('en', 'English').setExtra({ icon: 'iconfont icon-fuhao-yingwen', el: enLocale }),
};
// 资源类型
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 = {
PublicAuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: ResourceTypeEnum.Machine,
DbInstance: ResourceTypeEnum.Db,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
};
// 标签关联的资源类型路径
export const TagResourceTypePath = {
MachineAuthCert: `${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`,
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
};

View File

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

View File

@@ -0,0 +1,38 @@
import CryptoJS from 'crypto-js';
import { getToken } from '@/common/utils/storage';
/**
* AES 加密数据
* @param word
* @param key
*/
export function AesEncrypt(word: string, key?: string) {
if (!key) {
key = getToken().substring(0, 24);
}
const sKey = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(word, sKey, {
iv: sKey,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
export function AesDecrypt(word: string, key?: string): string {
if (!key) {
key = getToken().substring(0, 24);
}
const sKey = CryptoJS.enc.Utf8.parse(key);
// key 和 iv 使用同一个值
const decrypted = CryptoJS.AES.decrypt(word, sKey, {
iv: sKey,
mode: CryptoJS.mode.CBC, // CBC算法
padding: CryptoJS.pad.Pkcs7, //使用pkcs7 进行padding 后端需要注意
});
return decrypted.toString(CryptoJS.enc.Base64);
}

View File

@@ -2,10 +2,12 @@ import request from './request';
export default {
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),
getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
oauth2LoginConfig: () => request.get('/auth/oauth2-config'),
getServerConf: () => request.get('/sys/configs/server'),
oauth2LoginConfig: () => request.get('/auth/oauth2/config'),
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
captcha: () => request.get('/sys/captcha'),
logout: () => request.post('/auth/accounts/logout'),
@@ -13,4 +15,5 @@ export default {
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
};

View File

@@ -0,0 +1,11 @@
import { i18n } from '@/i18n';
export const AccountUsernamePattern = {
pattern: /^[a-zA-Z0-9_]{5,16}$/g,
message: i18n.global.t('system.account.usernamePatternErrMsg'),
};
export const ResourceCodePattern = {
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
message: i18n.global.t('system.menu.resourceCodePatternErrMsg'),
};

View File

@@ -4,7 +4,7 @@ import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { useApiFetch } from './useRequest';
import { useApiFetch } from '../hooks/useRequest';
import Api from './Api';
export default {
@@ -38,6 +38,7 @@ export enum ResultEnum {
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501,
ACCESS_TOKEN_INVALID = 502, // accessToken失效
}
export const baseUrl: string = config.baseApiUrl;
@@ -208,6 +209,36 @@ export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;
}
/**
* url地址
* @param key key
* @returns url
*/
export function getFileUrl(key: string) {
return `${baseUrl}/sys/files/${key}`;
}
/**
* url
* @param key key
* @returns url
*/
export function getUploadFileUrl(key: string = '') {
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
}
/**
*
* @param key key
*/
export function downloadFile(key: string) {
const a = document.createElement('a');
a.setAttribute('href', `${getFileUrl(key)}`);
a.setAttribute('target', '_blank');
a.click();
a.remove();
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;

View File

@@ -1,9 +1,86 @@
import openApi from './openApi';
// 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseWatermarkConfigKey = 'UseWatermark';
const MachineConfig = 'MachineConfig';
const AccountLoginSecurityKey = 'AccountLoginSecurity';
const MachineConfigKey = 'MachineConfig';
const SysStyleConfigKey = 'SysStyleConfig';
/**
*
*
* @returns
*/
export async function getAccountLoginSecurity(): Promise<any> {
const value = await getConfigValue(AccountLoginSecurityKey);
if (!value) {
return null;
}
const jsonValue = JSON.parse(value);
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
return jsonValue;
}
/**
* logotitle等
*
* @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;
}
}
/**
*
*
* @returns
*/
export async function getServerConf(): Promise<any> {
return openApi.getServerConf();
}
/**
*
@@ -27,77 +104,6 @@ export async function getBoolConfigValue(key: string, defaultValue: boolean): Pr
return convertBool(value, defaultValue);
}
/**
*
*
* @returns
*/
export async function getAccountLoginSecurity(): Promise<any> {
const value = await getConfigValue(AccountLoginSecurity);
if (!value) {
return null;
}
const jsonValue = JSON.parse(value);
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
return jsonValue;
}
/**
*
*
* @returns
*/
export async function 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;
}
}
/**
* 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(MachineConfig);
const defaultValue = {
// 默认1gb
uploadMaxFileSize: '1GB',
};
if (!value) {
return defaultValue;
}
try {
const jsonValue = JSON.parse(value);
return jsonValue;
} catch (e) {
return defaultValue;
}
}
function convertBool(value: string, defaultValue: boolean) {
if (!value) {
return defaultValue;

View File

@@ -0,0 +1,47 @@
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
import syssocket from './syssocket';
import { h, reactive } from 'vue';
import { ElNotification } from 'element-plus';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
export function initSysMsgs() {
registerDbSqlExecProgress();
}
const sqlExecNotifyMap: Map<string, any> = new Map();
function registerDbSqlExecProgress() {
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
if (progress != undefined) {
progress.notification?.close();
sqlExecNotifyMap.delete(id);
progress = undefined;
}
return;
}
if (progress == undefined) {
progress = {
props: reactive(buildProgressProps()),
notification: undefined,
};
}
progress.props.progress.title = content.title;
progress.props.progress.executedStatements = content.executedStatements;
if (!sqlExecNotifyMap.has(id)) {
progress.notification = ElNotification({
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
showClose: false,
});
sqlExecNotifyMap.set(id, progress);
}
});
}

View File

@@ -1,9 +1,9 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import { joinClientParams } from './request';
import { ElNotification } from 'element-plus';
class SysSocket {
/**
@@ -19,7 +19,7 @@ class SysSocket {
/**
*
*/
messageTypes = {
messageTypes: any = {
0: 'error',
1: 'success',
2: 'info',
@@ -41,7 +41,14 @@ class SysSocket {
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl)
.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);
if (handler) {
@@ -49,11 +56,14 @@ class SysSocket {
return;
}
// 默认通知处理
const type = this.getMsgType(message.type);
let msg = message.msg;
let duration = 0;
ElNotification({
duration: 0,
duration: duration,
title: message.title,
message: message.msg,
message: msg,
type: type,
});
})
@@ -66,9 +76,9 @@ class SysSocket {
}
destory() {
this.socket.close();
this.socket?.close();
this.socket = null;
this.categoryHandlers.clear();
this.categoryHandlers?.clear();
}
/**

View File

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

View File

@@ -0,0 +1,116 @@
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
* @returns
*/
export function formatByteSize(size: number, fixed = 2) {
if (size === 0) {
return '0B';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const base = 1024;
const exponent = Math.floor(Math.log(size) / Math.log(base));
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
}
/**
* 容量转为对应的字节大小,如 1KB转为 1024
* @param sizeString 1kb 1gb等
* @returns
*/
export function convertToBytes(sizeStr: string) {
sizeStr = sizeStr.trim();
const unit = sizeStr.slice(-2);
const valueStr = sizeStr.slice(0, -2);
const value = parseInt(valueStr, 10);
let bytes = 0;
switch (unit.toUpperCase()) {
case 'KB':
bytes = value * 1024;
break;
case 'MB':
bytes = value * 1024 * 1024;
break;
case 'GB':
bytes = value * 1024 * 1024 * 1024;
break;
default:
throw new Error('Invalid size unit');
}
return bytes;
}
/**
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
*
* @param time 时间数
* @param unit time对应的单位
* @returns
*/
export function formatTime(time: number, unit: string = 's') {
const units: any = {
y: 31536000,
M: 2592000,
d: 86400,
h: 3600,
m: 60,
s: 1,
};
if (!units[unit]) {
return 'Invalid unit';
}
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;
}
/**
* formatAxis(new Date()) // 上午好
*/
export function formatAxis(param: any) {
let hour: number = new Date(param).getHours();
if (hour < 6) return '凌晨好';
else if (hour < 9) return '早上好';
else if (hour < 12) return '上午好';
else if (hour < 14) return '中午好';
else if (hour < 17) return '下午好';
else if (hour < 19) return '傍晚好';
else if (hour < 22) return '晚上好';
else return '夜里好';
}

View File

@@ -0,0 +1,56 @@
/**
* 根据对象访问路径,获取对应的值
*
* @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) {
return undefined;
}
// 如果是字符串则尝试使用json解析
if (typeof result == 'string') {
try {
result = JSON.parse(result);
} catch (e) {
console.error(e);
return undefined;
}
}
if (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]);
let arrValue = result[arrayKey];
if (typeof arrValue == 'string') {
try {
arrValue = JSON.parse(arrValue);
} catch (e) {
result = undefined;
break;
}
}
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
} else {
result = result[key];
}
}
return result;
}

View File

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

View File

@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
return width;
}
/**
*
*/
export function getContentWidth(content: any): number {
if (!content) {
return 50;
}
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 小写字母、数字字符
flexWidth += 9.3;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 20;
} else {
// 其他种类字符
flexWidth += 8;
}
}
// if (flexWidth > 450) {
// // 设置最大宽度
// flexWidth = 450;
// }
return flexWidth;
}
/**
*
* @returns uuid
@@ -179,3 +142,75 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
clipboard.destroy();
});
}
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
keyword = keyword?.toLowerCase();
return fields.filter((field) => {
for (let valueExtractFunc of valueExtractFuncs) {
const value = valueExtractFunc(field)?.toLowerCase();
if (isPrefixSubsequence(keyword, value)) {
return true;
}
}
return false;
});
}
/**
* targetTemplate=username prefix=uname -> trueprefix=uname2 -> false
* @param prefix ,
* @param targetTemplate
* @returns
*/
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
let i = 0; // 指向prefix的索引
let j = 0; // 指向targetTemplate的索引
while (i < prefix.length && j < targetTemplate.length) {
if (prefix[i] === targetTemplate[j]) {
// 字符匹配,两个指针都向前移动
i++;
}
j++; // 目标字符串指针始终向前移动
}
// 如果prefix的所有字符都被找到返回true
return i === prefix.length;
}
/**
*
* @param length
*/
export function randomPassword(length = 10) {
const lowerCase = 'abcdefghijklmnopqrstuvwxyz';
const upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
const specialChars = '!@#$%^&*()-_=+[]{}|;:,.<>?';
// 确保每个类别至少包含一个字符
let password = [getRandomChar(lowerCase), getRandomChar(upperCase), getRandomChar(numbers), getRandomChar(specialChars)];
// 剩余字符从所有字符集中随机选择
const allChars = lowerCase + upperCase + numbers + specialChars;
for (let i = 4; i < length; i++) {
password.push(getRandomChar(allChars));
}
// 打乱数组顺序以增加随机性
shuffleArray(password);
return password.join('');
}
function getRandomChar(charSet: string) {
const randomIndex = Math.floor(Math.random() * charSet.length);
return charSet[randomIndex];
}
function shuffleArray(array: string[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}

View File

@@ -4,6 +4,7 @@ export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {

View File

@@ -2,6 +2,7 @@
<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 : []"
@@ -15,7 +16,7 @@
:is="`el-option`"
v-for="(col, index) in item.options"
:key="index"
:label="col[fieldNames.label]"
:label="$t(col[fieldNames.label])"
:value="col[fieldNames.value]"
></component>
</template>
@@ -27,17 +28,16 @@
<script setup lang="ts" name="SearchFormItem">
import { computed } from 'vue';
import { SearchItem } from '../index';
import { useVModel } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
interface SearchFormItemProps {
modelValue: any;
item: SearchItem;
}
const props = defineProps<SearchFormItemProps>();
const emit = defineEmits(['update:modelValue']);
const itemValue = useVModel(props, 'modelValue', emit);
const itemValue = defineModel('modelValue');
// fieldNames label && value && children key
const fieldNames = computed(() => {
@@ -48,19 +48,6 @@ const fieldNames = computed(() => {
};
});
// enumMap (el select-v2 enumData)
// const enumMap = inject('enumMap', ref(new Map()));
// const columnEnum = computed(() => {
// let enumData = enumMap.value.get(props.item.prop);
// if (!enumData) return [];
// if (props.item?.type === 'select-v2' && props.item.fieldNames) {
// enumData = enumData.map((item: { [key: string]: any }) => {
// return { ...item, label: item[fieldNames.value.label], value: item[fieldNames.value.value] };
// });
// }
// return enumData;
// });
// searchProps (type tree-selectcascader label && value && children)
const handleSearchProps = computed(() => {
const label = fieldNames.value.label;
@@ -77,10 +64,16 @@ const handleSearchProps = computed(() => {
return searchProps;
});
//
const handleEvents = computed(() => {
let itemEvents = props.item?.events ?? {};
return itemEvents;
});
// placeholder
const placeholder = computed(() => {
const search = props.item;
const label = search.label;
const label = t(search.label);
if (['datetimerange', 'daterange', 'monthrange'].includes(search?.props?.type) || search?.props?.isRange) {
return {
rangeSeparator: search?.props?.rangeSeparator ?? '至',
@@ -88,7 +81,9 @@ const placeholder = computed(() => {
endPlaceholder: search?.props?.endPlaceholder ?? '结束时间',
};
}
const placeholder = search?.props?.placeholder ?? (search?.type?.includes('input') ? `请输入${label}` : `请选择${label}`);
return { placeholder };
const placeholder =
search?.props?.placeholder ?? (search?.type?.includes('input') ? t('common.pleaseInput', { label }) : t('common.pleaseSelect', { label }));
return { placeholder: t(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

@@ -6,7 +6,7 @@
<el-form-item>
<template #label>
<el-space :size="4">
<span>{{ `${item?.label}` }}</span>
<span>{{ `${$t(item?.label)}` }}</span>
<el-tooltip v-if="item.tooltip" :content="item?.tooltip" placement="top">
<SvgIcon name="QuestionFilled" />
</el-tooltip>
@@ -14,15 +14,15 @@
<span>:</span>
</template>
<SearchFormItem v-if="!item.slot" :item="item" v-model="searchParam[item.prop]" />
<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 type="primary" :icon="Search" @click="search" plain> {{ $t('common.search') }} </el-button>
<el-button :icon="Delete" @click="reset"> {{ $t('common.reset') }} </el-button>
<el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
{{ collapsed ? '展开' : '合并' }}
<el-icon class="el-icon--right">
@@ -44,11 +44,9 @@ 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';
import { useVModel } from '@vueuse/core';
interface ProTableProps {
items: SearchItem[]; //
modelValue?: { [key: string]: any }; //
searchCol: number | Record<BreakPoint, number>;
search: (params: any) => void; //
reset: (params: any) => void; //
@@ -60,9 +58,7 @@ const props = withDefaults(defineProps<ProTableProps>(), {
modelValue: () => ({}),
});
const emit = defineEmits(['update:modelValue']);
const searchParam = useVModel(props, 'modelValue', emit);
const searchParam: any = defineModel('modelValue');
//
const getResponsive = (item: SearchItem) => {
@@ -87,7 +83,7 @@ const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
// /
const showCollapse = computed(() => {
let show = false;
props.items.reduce((prev, current) => {
props.items.reduce((prev, current: any) => {
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;
@@ -98,6 +94,12 @@ const showCollapse = computed(() => {
}, 0);
return show;
});
const handleItemKeyupEnter = (item: SearchItem) => {
if (item.type == 'input') {
props.search(searchParam);
}
};
</script>
<style lang="scss">
.search-form {

View File

@@ -14,11 +14,11 @@ export function hasPerm(code: string) {
/**
* code
* @param perms { save: "xxx:save"}
* @returns {"xxx:save": true} key->permission code
* @param permCodes
*/
export function hasPerms(permCodes: any[]) {
const res = {};
const res = {} as { [key: string]: boolean };
for (let permCode of permCodes) {
if (hasPerm(permCode)) {
res[permCode] = true;

View File

@@ -22,7 +22,7 @@
@click="onCurrentContextmenuClick(v)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
<span>{{ $t(v.txt) }}</span>
</li>
</template>
</ul>
@@ -184,6 +184,10 @@ defineExpose({
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
padding: 5px 12px;
}
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;

View File

@@ -0,0 +1,298 @@
<template>
<div class="crontab">
<el-tabs v-model="state.activeName" @tab-change="changeTab(state.activeName)" type="border-card">
<el-tab-pane :label="$t('components.crontab.second')" name="second" v-if="shouldHide('second')">
<CrontabSecond :cron="crontabValueObj" ref="secondRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.minute')" name="min" v-if="shouldHide('min')">
<CrontabMin :cron="crontabValueObj" ref="minRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.hour')" name="hour" v-if="shouldHide('hour')">
<CrontabHour :cron="crontabValueObj" ref="hourRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.day')" name="day" v-if="shouldHide('day')">
<CrontabDay :cron="crontabValueObj" ref="dayRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.month')" name="mouth" v-if="shouldHide('mouth')">
<CrontabMouth :cron="crontabValueObj" ref="mouthRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.week')" name="week" v-if="shouldHide('week')">
<CrontabWeek :cron="crontabValueObj" ref="weekRef" />
</el-tab-pane>
<el-tab-pane :label="$t('components.crontab.year')" 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">{{ $t('components.crontab.timeExpression') }}</p>
<table>
<thead>
<tr>
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
<th>{{ $t('components.crontab.crontabCompleteExpression') }}</th>
</tr>
</thead>
<tbody>
<tr>
<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>{{ crontabValueString }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button size="small" @click="hidePopup">{{ $t('common.cancel') }}</el-button>
<el-button size="small" type="warning" @click="clearCron">{{ $t('common.reset') }}</el-button>
<el-button size="small" type="primary" @click="submitFill">{{ $t('common.confirm') }}</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', crontabValueString.value);
hidePopup();
};
const clearCron = () => {
// 还原选择项
state.crontabValueObj = {
second: '*',
min: '*',
hour: '*',
day: '*',
mouth: '*',
week: '?',
year: '',
};
changeTab(state.activeName);
};
const crontabValueString = 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,219 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.day') }}{{ $t('components.crontab.dayCrontype1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2"> {{ $t('components.crontab.crontype2') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontype3') }}
<el-input-number v-model="cycle01" :min="0" :max="31" /> - <el-input-number v-model="cycle02" :min="0" :max="31" />
{{ $t('components.crontab.day') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="0" :max="31" /> {{ $t('components.crontab.crontypeStartDay') }}
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="31" />
{{ $t('components.crontab.crontypeExecDay') }}
</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"> {{ $t('components.crontab.monthLastDay') }} </el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="7" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 7" class="w100" clearable v-model="checkboxList" 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,159 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.hour') }}{{ $t('components.crontab.hourCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ $t('components.crontab.crontype3') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
{{ $t('components.crontab.hour') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartHour') }}
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="60" />
{{ $t('components.crontab.crontypeExecHour') }}
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" 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="$t('components.crontab.crontabInputPlaceholder')">
<template #prepend>
<el-button @click="showCron = true" icon="Pointer"></el-button>
</template>
</el-input>
<el-dialog :title="$t('components.crontab.crontabTitle')" v-model="showCron" width="660px">
<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,155 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.minute') }}{{ $t('components.crontab.hourCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ $t('components.crontab.crontype3') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
{{ $t('components.crontab.minute') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartMin') }}
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="60" />
{{ $t('components.crontab.crontypeExecMin') }}
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" 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,166 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.month') }}{{ $t('components.crontab.hourCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ $t('components.crontab.crontype3') }}
<el-input-number v-model="cycle01" :min="1" :max="12" /> - <el-input-number v-model="cycle02" :min="1" :max="12" />
{{ $t('components.crontab.month') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="1" :max="12" /> {{ $t('components.crontab.crontypeStartMonth') }}
{{ $t('components.crontab.crontypeEvery') }}
<el-input-number v-model="average02" :min="1" :max="12" /> {{ $t('components.crontab.crontypeExecMonth') }}
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" 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">{{ $t('components.crontab.last5runTimes') }}</p>
<ul class="popup-result-scroll">
<template v-if="isShow">
<li v-for="item in resultList" :key="item">{{ item }}</li>
</template>
<li v-else>{{ $t('components.crontab.calculationing') }}...</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,152 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.second') }}{{ $t('components.crontab.hourCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ $t('components.crontab.crontype3') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
{{ $t('components.crontab.second') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartSecond') }}
{{ $t('components.crontab.crontypeEvery') }}<el-input-number v-model="average02" :min="0" :max="60" />
{{ $t('components.crontab.crontypeExecSecond') }}
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" 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,213 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.weekCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2"> {{ $t('components.crontab.crontype2') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ $t('components.crontab.crontype3') }}
<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"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 6" class="w100" clearable v-model="checkboxList" multiple>
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ $t(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: [
'components.crontab.monday',
'components.crontab.tuesday',
'components.crontab.wednesday',
'components.crontab.thursday',
'components.crontab.friday',
'components.crontab.saturday',
'components.crontab.sunday',
],
});
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,161 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio :label="1" v-model="radioValue"> {{ $t('components.crontab.hourCronType1') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="2" v-model="radioValue"> {{ $t('components.crontab.yearly') }} </el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="3" v-model="radioValue">
{{ $t('components.crontab.crontype3') }}
<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">
{{ $t('components.crontab.crontypeFrom') }}
<el-input-number v-model="average01" :min="fullYear" /> {{ $t('components.crontab.crontypeStartYear') }}
{{ $t('components.crontab.crontypeEvery') }}
<el-input-number v-model="average02" :min="fullYear" /> {{ $t('components.crontab.crontypeExecYear') }}
</el-radio>
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="5" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 5" class="w100" clearable v-model="checkboxList" 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

@@ -0,0 +1,47 @@
<template>
<div class="dynamic-form">
<el-form v-bind="$attrs" ref="formRef" :model="modelValue" label-width="auto">
<el-form-item v-for="item in props.formItems as any" :key="item.name" :prop="item.model" :label="$t(item.name)" :required="item.required ?? true">
<el-input v-if="!item.options" v-model="modelValue[item.model]" :placeholder="$t(item.placeholder)" autocomplete="off" clearable></el-input>
<el-select
v-else
v-model="modelValue[item.model]"
:placeholder="$t(item.placeholder)"
filterable
autocomplete="off"
clearable
style="width: 100%"
>
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const props = defineProps({
formItems: { type: Array },
});
const formRef: any = ref();
const modelValue: any = defineModel();
const validate = async (func: any) => {
await formRef.value.validate(func);
};
const resetFields = () => {
formRef.value.resetFields();
};
defineExpose({
validate,
resetFields,
});
</script>
<style lang="scss"></style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="form-dialog">
<el-dialog @close="close" v-bind="$attrs" :title="title" v-model="dialogVisible" :width="width">
<dynamic-form ref="df" :form-items="formItems" v-model="formData" />
<dynamic-form ref="df" :form-items="props.formItems" v-model="formData" />
<template #footer>
<span>
<slot name="btns">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="confirm"> </el-button>
<el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ $t('common.confirm') }}</el-button>
</slot>
</span>
</template>
@@ -18,22 +18,19 @@
<script lang="ts" setup>
import { ref } from 'vue';
import DynamicForm from './DynamicForm.vue';
import { useVModel } from '@vueuse/core';
const emit = defineEmits(['update:visible', 'update:modelValue', 'close', 'confirm']);
const emit = defineEmits(['close', 'confirm']);
const props = defineProps({
title: { type: String },
visible: { type: Boolean },
width: { type: [String, Number], default: '500px' },
formItems: { type: Array },
modelValue: { type: Object },
});
const df: any = ref();
const formData: any = useVModel(props, 'modelValue', emit);
const dialogVisible: any = useVModel(props, 'visible', emit);
const formData: any = defineModel('modelValue');
const dialogVisible = defineModel<boolean>('visible', { default: false });
const close = () => {
emit('close');

View File

@@ -1,35 +1,41 @@
<template>
<div class="dynamic-form-edit w100">
<el-table :data="formItems" stripe class="w100" empty-text="暂无表单项">
<el-table :data="formItems" stripe class="w100">
<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>
<span class="ml10">model field</span>
</template>
<template #default="scope">
<el-input v-model="scope.row['model']" placeholder="字段model" clearable> </el-input>
<el-input v-model="scope.row['model']" :placeholder="$t('components.df.fieldModelPlaceholder')" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="label" min-width="100px">
<el-table-column prop="name" :label="$t('components.df.fieldLabel')" min-width="100px">
<template #default="scope">
<el-input v-model="scope.row['name']" placeholder="字段title" clearable> </el-input>
<el-input v-model="scope.row['name']" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="placeholder" label="字段说明" min-width="140px">
<el-table-column prop="placeholder" :label="$t('components.df.fieldPlaceholder')" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['placeholder']" placeholder="字段说明" clearable> </el-input>
<el-input v-model="scope.row['placeholder']" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="options" label="可选值" min-width="140px">
<el-table-column prop="options" :label="$t('components.df.optionalValues')" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['options']" placeholder="可选值 ,分割" clearable> </el-input>
<el-input v-model="scope.row['options']" :placeholder="$t('components.df.optionalValuesPlaceholder')" clearable> </el-input>
</template>
</el-table-column>
<el-table-column label="操作" wdith="20px">
<el-table-column prop="required" :label="$t('components.df.required')" min-width="65px">
<template #default="scope">
<el-checkbox v-model="scope.row['required']" />
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" wdith="20px">
<template #default="scope">
<el-button type="danger" @click="deleteItem(scope.$index)" icon="delete" plain></el-button>
</template>
@@ -39,15 +45,7 @@
</template>
<script lang="ts" setup>
import { useVModel } from '@vueuse/core';
const props = defineProps({
modelValue: { type: Array },
});
const emit = defineEmits(['update:modelValue']);
const formItems: any = useVModel(props, 'modelValue', emit);
const formItems: any = defineModel('modelValue');
const addItem = () => {
formItems.value.push({});

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

@@ -0,0 +1,17 @@
<template>
<el-select v-bind="$attrs" v-model="modelValue">
<el-option v-for="item in props.enums" :key="item.value" :label="$t(item.label)" :value="item.value"> </el-option>
</el-select>
</template>
<script lang="ts" setup>
const props = defineProps({
enums: {
type: Object, // 需要为EnumValue类型
required: true,
},
});
const modelValue: any = defineModel('modelValue');
</script>
<style scoped lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-tag v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
<el-tag :disable-transitions="true" v-bind="$attrs" :type="type" :color="color" effect="plain">{{ $t(enumLabel) }}</el-tag>
</template>
<script lang="ts" setup>
@@ -40,7 +40,7 @@ onMounted(() => {
});
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) {
state.enumLabel = '-';
state.type = 'danger';

View File

@@ -0,0 +1,60 @@
<template>
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
</el-tooltip>
{{ fileDetail?.filename }}
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
fileKey: {
type: String,
required: true,
},
files: {
type: [Array],
},
canDownload: {
type: Boolean,
default: true,
},
});
onMounted(async () => {
setFileInfo();
});
watch(
() => props.fileKey,
async (val) => {
if (val) {
setFileInfo();
}
}
);
const fileDetail: any = ref({});
const setFileInfo = async () => {
if (!props.fileKey) {
return;
}
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -26,7 +26,7 @@
>
<template #default>
<div class="icon-selector-warp">
<div class="icon-selector-warp-title">{{ title }}</div>
<div class="ml10 mt10">{{ title }}</div>
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
<el-tab-pane lazy label="ele" name="ele">
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />

View File

@@ -1,7 +1,7 @@
<template>
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select>
</div>
@@ -196,6 +196,9 @@ watch(
(newValue: any) => {
if (!monacoEditorIns.hasTextFocus()) {
state.languageMode = props.language;
if (newValue == null) {
newValue = '';
}
monacoEditorIns?.setValue(newValue);
}
}
@@ -252,7 +255,9 @@ const changeLanguage = (value: any) => {
};
const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value);
if (value) {
monacoEditorIns.getModel()?.setValue(value);
}
};
/**
@@ -294,17 +299,19 @@ const registeShell = () => {
};
const format = () => {
/*
触发自动格式化;
*/
// ;
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
};
const focus = () => {
monacoEditorIns.focus();
};
const getEditor = () => {
return monacoEditorIns;
};
defineExpose({ getEditor, format });
defineExpose({ getEditor, format, focus });
</script>
<style lang="scss">

View File

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

View File

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

@@ -0,0 +1,439 @@
<template>
<div>
<transition name="el-zoom-in-top">
<!-- 查询表单 -->
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search"
:reset="reset" :search-col="searchCol">
<!-- 遍历父组件传入的 solts 透传给子组件 -->
<template v-for="(_, key) in useSlots()" v-slot:[key]>
<slot :name="key"></slot>
</template>
</SearchForm>
</transition>
<div class="card">
<div class="table-main">
<!-- 表格头部 操作按钮 -->
<div class="table-header">
<div class="header-button-lf">
<slot name="tableHeader" />
</div>
<div v-if="toolButton" class="header-button-ri">
<slot name="toolButton">
<div class="tool-button">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="simple-search-form">
<el-dropdown v-if="searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="searchItem in searchItems"
:key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
{{ $t(searchItem.label) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="simple-search-form-label mt5">
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
</div>
<el-form-item style="width: 200px" :key="nowSearchItem.prop">
<SearchFormItem @keyup.enter.native="searchFormItemKeyUpEnter"
v-if="!nowSearchItem.slot" :item="nowSearchItem"
v-model="queryForm[nowSearchItem.prop]" />
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else
:name="nowSearchItem.slot">
</slot>
</el-form-item>
</div>
<div>
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search"
circle @click="search" />
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button v-if="showToolButton('search') && searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'" circle
@click="isShowSearch = !isShowSearch" />
<el-popover placement="bottom" title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px" width="auto"
trigger="click">
<div v-for="(item, index) in tableColumns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-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>
<el-table ref="tableRef" v-bind="$attrs" :max-height="tableMaxHeight"
@selection-change="handleSelectionChange" :data="tableData" highlight-current-row
v-loading="loading" :size="props.size as any" :border="border">
<el-table-column v-if="props.showSelection" :selectable="selectable" type="selection" width="40" />
<template v-for="(item, index) in tableColumns">
<el-table-column :key="index" v-if="item.show" :prop="item.prop" :label="$t(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.slotName ? item.slotName : 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="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>
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
<el-pagination :small="props.size == 'small'" @current-change="pageNumChange"
@size-change="pageSizeChange" style="text-align: right" layout="prev, pager, next, total, sizes"
:total="total" v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize"
:page-sizes="pageSizes" />
</el-row>
</div>
</div>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
import { TableColumn } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import { useEventListener } from '@vueuse/core';
import Api from '@/common/Api';
import SearchForm from '@/components/SearchForm/index.vue';
import { SearchItem } from '../SearchForm/index';
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { usePageTable } from '@/hooks/usePageTable';
import { ElTable } from 'element-plus';
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
export interface PageTableProps {
size?: string;
pageApi?: Api; // 请求表格数据的 api
columns: TableColumn[]; // 列配置项 ==> 必传
showSelection?: boolean;
selectable?: (row: any) => boolean; // 是否可选
pageable?: boolean;
showSearch?: boolean; // 是否显示搜索表单
data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
lazy?: boolean; // 是否自动执行请求 api ==> 非必传默认为false
beforeQueryFn?: (params: any) => any; // 执行查询时对查询参数进行处理,调整等
dataHandlerFn?: (data: any) => any; // 数据处理回调函数,用于将请求回来的数据二次加工处理等
searchItems?: SearchItem[];
border?: boolean; // 是否带有纵向边框 ==> 非必传默认为false
toolButton?: ('setting' | 'search')[] | boolean; // 是否显示表格功能按钮 ==> 非必传默认为true
searchCol?: any; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | number 如 3
}
// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<PageTableProps>(), {
columns: () => [],
pageable: true,
showSelection: false,
lazy: false,
border: false,
toolButton: true,
showSearch: false,
searchItems: () => [],
searchCol: () => ({ xs: 1, sm: 3, md: 3, lg: 4, xl: 5 }),
});
// 查询表单参数 ==> 非必传(默认为{pageNum:1, pageSize: 10}
const queryForm: Ref<any> = defineModel('queryForm', {
default: {
pageNum: 1,
pageSize: 0,
},
});
// table 实例
const tableRef = ref<InstanceType<typeof ElTable>>();
// 接收 columns 并设置为响应式
const tableColumns = reactive<TableColumn[]>(props.columns);
// 接收 searchItems 并设置为响应式
const tableSearchItems = reactive<SearchItem[]>(props.searchItems);
const { themeConfig } = storeToRefs(useThemeConfig());
// 是否显示搜索模块
const isShowSearch = ref(props.showSearch);
// 控制 ToolButton 显示
const showToolButton = (key: 'setting' | 'search') => {
return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
};
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
/**
* 改变当前的搜索项
* @param searchItem 当前点击的搜索项
*/
const changeSimpleFormItem = (searchItem: SearchItem) => {
// 将之前的值置为空,避免因为只显示一个搜索项却搜索多个条件
queryForm.value[nowSearchItem.value.prop] = null;
nowSearchItem.value = searchItem;
};
const pageSizeChange = (val: number) => {
emit('pageSizeChange', val);
handlePageSizeChange(val);
};
const pageNumChange = (val: number) => {
emit('pageNumChange', val);
handlePageNumChange(val);
};
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
props.pageable,
props.pageApi,
queryForm,
props.beforeQueryFn,
props.dataHandlerFn
);
const state = reactive({
pageSizes: [] as any, // 可选每页显示的数据量
// 输入框宽度
formatVal: '', // 格式化后的值
tableMaxHeight: '500px',
});
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
watch(tableData, (newValue: any) => {
if (newValue && newValue.length > 0) {
props.columns.forEach((item) => {
if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(tableData.value);
}
});
}
});
watch(isShowSearch, () => {
calcuTableHeight();
});
watch(
() => props.data,
(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];
if (!props.lazy) {
await getTableData();
}
});
const calcuTableHeight = () => {
const headerHeight = isShowSearch.value ? 330 : 250;
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
};
const searchFormItemKeyUpEnter = (event: any) => {
event.preventDefault();
search();
};
const formatText = (data: any) => {
state.formatVal = '';
try {
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) {
state.formatVal = data;
}
};
const handleSelectionChange = (val: any) => {
emit('update:selectionData', val);
};
const getData = () => {
return toValue(tableData);
};
defineExpose({
tableRef: tableRef,
search: getTableData,
getData,
total,
});
</script>
<style scoped lang="scss">
.table-box,
.table-main {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
// 表格 header 样式
.table-header {
width: 100%;
.header-button-lf {
float: left;
}
.header-button-ri {
float: right;
.tool-button {
display: flex;
justify-content: space-between;
}
.simple-search-form {
margin-right: 10px;
display: flex;
justify-content: space-between;
::v-deep(.el-form-item__content > *) {
width: 100% !important;
}
.simple-search-form-label {
text-align: right;
margin-right: 5px;
}
.simple-search-form-btn:hover {
color: var(--el-color-primary);
}
}
}
.el-button {
margin-bottom: 10px;
}
}
// el-table 表格样式
.el-table {
flex: 1;
// 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
table {
width: 100%;
}
// .el-table__header th {
// height: 45px;
// font-size: 15px;
// font-weight: bold;
// color: var(--el-text-color-primary);
// background: var(--el-fill-color-light);
// }
// .el-table__row {
// height: 45px;
// font-size: 14px;
// .move {
// cursor: move;
// .el-icon {
// cursor: move;
// }
// }
// }
// 设置 el-table 中 header 文字不换行,并省略
.el-table__header .el-table__cell>.cell {
// white-space: nowrap;
white-space: wrap;
}
// 解决表格数据为空时样式不居中问题(仅在element-plus中)
// .el-table__empty-block {
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
// .table-empty {
// line-height: 30px;
// }
// }
// table 中 image 图片样式
.table-image {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
::v-deep(.el-form-item__label) {
font-weight: bold;
}
}
</style>

View File

@@ -1,6 +1,8 @@
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 { i18n } from '@/i18n';
export class TableColumn {
/**
@@ -29,10 +31,15 @@ export class TableColumn {
minWidth: number | string;
/**
* prop属性名
* slotName为空则插槽名为prop属性名
*/
slot: boolean = false;
/**
*
*/
slotName: string = '';
showOverflowTooltip: boolean = true;
sortable: boolean = false;
@@ -87,7 +94,7 @@ export class TableColumn {
if (this.formatFunc) {
return this.formatFunc(rowData, this.prop);
}
return rowData[this.prop];
return getValueByPath(rowData, this.prop);
}
static new(prop: string, label: string): TableColumn {
@@ -121,7 +128,7 @@ export class TableColumn {
/**
* 使
* @param param
* @param param , AccountStatusEnum
* @returns this
*/
typeTag(param: any): TableColumn {
@@ -144,8 +151,9 @@ export class TableColumn {
*
* @returns this
*/
isSlot(): TableColumn {
isSlot(slotName: string = ''): TableColumn {
this.slot = true;
this.slotName = slotName;
return this;
}
@@ -165,7 +173,7 @@ export class TableColumn {
*/
isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return dateFormat(data[prop]);
return formatDate(getValueByPath(data, prop));
});
return this;
}
@@ -176,7 +184,7 @@ export class TableColumn {
*/
isEnum(enums: any): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return EnumValue.getLabelByValue(enums, data[prop]);
return i18n.global.t(EnumValue.getLabelByValue(enums, getValueByPath(data, prop)));
});
return this;
}
@@ -218,7 +226,7 @@ export class TableColumn {
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i];
let nowValue = nowData[prop];
let nowValue = getValueByPath(nowData, prop);
if (!nowValue) {
continue;
}
@@ -236,7 +244,7 @@ export class TableColumn {
// 需要加上表格的内间距等,视情况加
const contentWidth: number = getTextWidth(maxWidthText) + 30;
// 获取label的宽度取较大的宽度
const columnWidth: number = getTextWidth(label) + 60;
const columnWidth: number = getTextWidth(i18n.global.t(label)) + 60;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
// 设置上限与累加需要额外增加的宽度
this.minWidth = (flexWidth > 400 ? 400 : flexWidth) + this.addWidth;

View File

@@ -1,5 +1,5 @@
<template>
<el-descriptions border size="small" :title="`${progress.title}`">
<el-descriptions border size="small" :title="`${props.progress.title}`">
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
</el-descriptions>

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