173 Commits

Author SHA1 Message Date
meilin.huang
0695ad9a85 feat: 新增机器授权凭证管理与其他优化 2023-03-06 16:59:57 +08:00
meilin.huang
7c086bbec8 fix: 问题修复 2023-02-22 15:54:53 +08:00
meilin.huang
c75fe7135a refactor: 图标优化等 2023-02-20 18:41:45 +08:00
meilin.huang
edf29976dd feat: 资源操作相关标签树调整为el-tree 2023-02-18 23:02:14 +08:00
meilin.huang
7b5f963ec4 fix: 构建脚本调整 2023-02-15 21:32:51 +08:00
meilin.huang
f22badb861 feat: 优化 2023-02-15 21:28:01 +08:00
meilin.huang
8c501c90cd feat: 标签路径展示优化,新增提示 2023-02-14 18:24:39 +08:00
Coder慌
11eebdfcf0 !39 feat: 表和schema超出20条后启用模糊高亮过滤
Merge pull request !39 from zongyangleo/dev_20230213_1
2023-02-14 06:36:41 +00:00
刘宗洋
46e331783f fix: sql提示bug修复 2023-02-14 14:03:18 +08:00
刘宗洋
7e33e64659 Merge remote-tracking branch 'upstream/dev' into dev_20230213_1 2023-02-14 12:24:48 +08:00
刘宗洋
7e4152ad6d fix: sql提示bug修复 2023-02-14 12:23:38 +08:00
meilin.huang
d189ee7c22 feat: 数据库表格重构 2023-02-14 11:44:48 +08:00
刘宗洋
641c2abb24 feat: 表名+schema模糊高亮过滤 2023-02-14 10:33:57 +08:00
meilin.huang
3ab4ac891b 表和schema超出20条后启用过滤 2023-02-13 21:36:18 +08:00
meilin.huang
70b586e45a refactor: sqlexec组件重构优化、新增数据库相关系统参数配置、相关问题修复 2023-02-13 21:11:16 +08:00
Coder慌
77aa724003 !37 fix: ref修复
Merge pull request !37 from zongyangleo/dev_20230206
2023-02-07 09:30:37 +00:00
刘宗洋
db7b50d96a fix: ref修复 2023-02-07 17:22:52 +08:00
meilin.huang
00fee24a85 feat: 代码小调整 2023-02-07 16:54:44 +08:00
Coder慌
fa0cb73ec9 !36 feat: redis新增实例树、各数据操作按钮跳转
Merge pull request !36 from zongyangleo/dev_20230206
2023-02-07 03:33:23 +00:00
刘宗洋
6ee815b71d debugger 2023-02-07 11:06:54 +08:00
刘宗洋
311a048af5 Merge remote-tracking branch 'upstream/dev' into dev_20230206
# Conflicts:
#	mayfly_go_web/src/views/ops/db/SqlExec.vue
#	mayfly_go_web/src/views/ops/db/component/InstanceTree.vue
#	mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue
2023-02-07 10:59:46 +08:00
刘宗洋
3da9ecfaa3 feat: 数据操作按钮跳转 2023-02-07 10:32:42 +08:00
刘宗洋
4c2c6f613e feat: redis新增实例树 2023-02-07 08:39:14 +08:00
meilin.huang
ba63736871 feat: 小功能优化 2023-02-06 17:14:16 +08:00
Coder慌
ed3dbafc35 !34 feat:mongo新增实例树
Merge pull request !34 from zongyangleo/dev_20230206
2023-02-06 08:54:31 +00:00
刘宗洋
fdeffbd495 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev_20230206
yes.
2023-02-06 16:46:52 +08:00
刘宗洋
9870812e6b feat:mongo新增实例树 2023-02-06 16:46:40 +08:00
Coder慌
46f35e14c4 !33 feat: 数据管理添加实例树,细节修改
Merge pull request !33 from zongyangleo/dev_20230206
2023-02-06 06:53:38 +00:00
刘宗洋
e89cf96ff4 feat:实例树优化:加宽菜单宽度、表加载中loading状态、新建查询等按钮自成一行、加宽实例详情宽度、实例树高度默认撑满 2023-02-06 14:50:57 +08:00
Coder慌
5cd19bf38d !32 feat: 数据管理添加实例树
Merge pull request !32 from zongyangleo/dev_20230206
2023-02-06 03:13:25 +00:00
刘宗洋
a6f69f2b62 修改团队bug 2023-02-06 11:08:18 +08:00
刘宗洋
e34b9adada feat: 数据管理添加实例树 2023-02-06 10:35:30 +08:00
meilin.huang
9f43f731b5 fix: 问题修复 2023-01-19 19:45:12 +08:00
meilin.huang
594ca43505 refactor: 包名变更ctx -> req 2023-01-14 16:29:52 +08:00
meilin.huang
2a91cdb67a fix: pgsql主键字段获取修复 2023-01-09 20:05:35 +08:00
Coder慌
11148e720b !30 feat: 执行sql,无数据时也提示一下
Merge pull request !30 from zongyangleo/dev_20221228
2023-01-01 06:43:54 +00:00
刘宗洋
c5835d6d8c feat: 执行sql,无数据时也提示一下 2022-12-28 10:23:38 +08:00
meilin.huang
4a26bb3ba5 feat: 支持redis缓存权限等信息 2022-12-26 20:53:07 +08:00
meilin.huang
4fec38724d fix: pgsql隧道连接问题修复 2022-12-22 18:41:34 +08:00
meilin.huang
85349df8a1 code review 2022-12-17 22:24:21 +08:00
meilin.huang
ffe250f8a9 feat: 构建文档调整&移除静态文件 2022-12-16 23:14:00 +08:00
Coder慌
eeb310a1d2 !28 redis监控信息面板重构
Merge pull request !28 from zongyangleo/dev_20221216
2022-12-16 03:40:02 +00:00
刘宗洋
a42c606d20 redis监控信息面板重构 2022-12-16 11:35:16 +08:00
Coder慌
4e64d46cd2 !27 redis监控信息面板重构
Merge pull request !27 from zongyangleo/dev_20221216
2022-12-16 03:17:50 +00:00
刘宗洋
9886ee6828 redis监控信息面板重构 2022-12-16 11:13:13 +08:00
Coder慌
0e1bde09c3 !26 fix: 修复双击编辑事件报错
Merge pull request !26 from zongyangleo/dev_20221213
2022-12-13 06:15:42 +00:00
刘宗洋
dda600709b fix: 修复双击编辑事件报错 2022-12-13 11:08:42 +08:00
meilin.huang
15f38491b2 feat: 新增docker部署 2022-12-09 22:02:37 +08:00
may-fly
4b140732f7 Merge pull request #23 from 1ch0/master
Feat: add Dockerfile
2022-12-08 19:14:41 +08:00
meilin.huang
4cb9ff3f14 Merge branch 'dev' 2022-12-05 21:50:23 +08:00
meilin.huang
e4f3e2c4c1 feat: redis scan优化 2022-12-05 21:45:35 +08:00
Coder慌
a80adb7dd8 !25 优化redis在scan查询时当前游标没有数据的场景
Merge pull request !25 from yechuankai/master
2022-12-05 09:56:09 +00:00
叶传开
195127a9d4 优化redis在scan查询时当前游标没有数据的场景 2022-12-05 13:21:28 +08:00
1ch0
f2119b2c52 Feat: add docker-compose 2022-12-05 11:27:05 +08:00
1ch0
f15c45793b Feat: add Dockerfile 2022-12-03 08:25:22 +08:00
meilin.huang
24f543e667 refactor: redis值格式化方式调整 2022-11-25 18:59:23 +08:00
meilin.huang
772995705f fix: 获取表主键逻辑完善 2022-11-23 20:48:37 +08:00
meilin.huang
3475c39fe6 fix: 小问题修复 2022-11-22 17:15:08 +08:00
Coder慌
e6e393379f !24 feat: 查询结果支持批量修改
Merge pull request !24 from zongyangleo/dev_20221121
2022-11-22 08:28:55 +00:00
刘宗洋
03cc91c3e5 fix: 表字段批量修改,查询tab:重复执行sql就取消修改,或者点击取消按钮
各表tab:独立计算修改字段,点击取消或者刷新取消修改
2022-11-22 15:54:11 +08:00
Coder慌
f82f7bec6a !23 feat: 查询结果支持批量修改
Merge pull request !23 from zongyangleo/dev_20221121
2022-11-22 05:52:45 +00:00
刘宗洋
4afd5bbd5e feat: 查询结果支持批量修改 2022-11-22 12:35:54 +08:00
meilin.huang
86aac2bf08 refactor: 列表字段精简 2022-11-21 19:49:50 +08:00
Coder慌
70c8b25a67 !22 bug修复
Merge pull request !22 from zongyangleo/dev_sqlexec_add
2022-11-19 09:24:00 +00:00
刘宗洋
231af72444 bug 2022-11-19 16:46:10 +08:00
meilin.huang
480e930385 refactor: 操作日志信息完善等 2022-11-18 17:52:30 +08:00
meilin.huang
debc34f0fb fix: 主键字段获取调整&其他小优化 2022-11-14 18:07:27 +08:00
meilin.huang
99ce3bd099 feat: 去除jsoneditor,统一使用monaco 2022-11-10 18:55:10 +08:00
Coder慌
99431cf9a2 !21 bug修复
Merge pull request !21 from zongyangleo/dev_20221109
2022-11-09 15:17:59 +00:00
刘宗洋
83711c69f9 fix: monaco bug, label加上description 2022-11-09 15:41:54 +08:00
meilin.huang
9e67032280 feat: 小调整 2022-11-07 21:57:51 +08:00
meilin.huang
fa37937410 feat: monaco主题移至全局配置 2022-11-05 22:18:59 +08:00
meilin.huang
1be2cad78e feat: monaco编辑器调整 2022-11-05 21:08:01 +08:00
meilin.huang
2b1e687ed4 feat: 初步移除codemirror 2022-11-05 15:13:40 +08:00
Coder慌
881009321b !20 feat: 新增monaco编辑器
Merge pull request !20 from zongyangleo/dev_upstream
2022-11-04 06:10:35 +00:00
刘宗洋
aed99b63b8 monaco bug 2022-11-04 11:32:06 +08:00
刘宗洋
dfa34ba371 Merge branch 'dev_monaco_20221104' into dev_upstream
# Conflicts:
#	mayfly_go_web/src/views/ops/db/DbList.vue
#	mayfly_go_web/src/views/ops/db/SqlExec.vue
#	mayfly_go_web/src/views/ops/db/component/SqlExecDialog.vue
2022-11-04 11:17:19 +08:00
meilin.huang
20beb30dd8 feat: 支持执行多条sql 2022-11-04 11:12:44 +08:00
刘宗洋
4475972af3 fix: sql执行器 获取sql bug 2022-11-04 11:12:44 +08:00
刘宗洋
a843c65783 index.html添加百秒级时间戳,防止被浏览器缓存 2022-11-04 11:12:44 +08:00
刘宗洋
c2de1d3fa2 refactor:
1.修改表后,刷新表数据
2.修改索引sql拼接bug
3.暂时只有mysql支持编辑表
2022-11-04 11:12:44 +08:00
meilin.huang
c8d091da06 feat: 小功能优化&前端基于setup语法糖重构 2022-11-04 11:12:42 +08:00
刘宗洋
553208ba57 fix: 子组件事件通知修复 2022-11-04 11:12:17 +08:00
刘宗洋
072028699a feat: monaco 2022-11-04 11:12:16 +08:00
Coder慌
9cdcf145a5 !18 bug修复
Merge pull request !18 from zongyangleo/dev_upstream_fix_20221102
2022-11-03 07:23:07 +00:00
meilin.huang
4df1c19e81 fix: 修复setup导致open失效问题 2022-11-03 14:05:04 +08:00
刘宗洋
ac26a214bc fix: 子组件事件通知修复 2022-11-02 22:45:39 +08:00
meilin.huang
ad616496d1 feat: 支持执行多条sql 2022-11-02 19:27:40 +08:00
刘宗洋
9870582779 refactor: sql解析失败逻辑重构 2022-11-01 21:25:00 +08:00
meilin.huang
20cc696b33 refactor: 标签列表选择调整 2022-11-01 21:25:00 +08:00
刘宗洋
d7263f2b3c fix: sql执行器 获取sql bug 2022-11-01 21:24:58 +08:00
meilin.huang
74e5ee41fb fix: sql字符串拼接改为占位符形式,防sql注入 2022-11-01 21:24:37 +08:00
刘宗洋
f936331dff refactor:
1.修改表后,刷新表数据
2.修改索引sql拼接bug
3.暂时只有mysql支持编辑表
2022-11-01 21:24:34 +08:00
meilin.huang
ba311c3504 fix: sql accout表字段修复 2022-11-01 21:23:51 +08:00
meilin.huang
03291594b1 feat: 小功能优化&前端基于setup语法糖重构 2022-11-01 21:23:49 +08:00
刘宗洋
a6d9a4b5ae refactor: 1.修改表后,刷新表数据;2.为了方便sqlmode,sql逻辑改为代码逻辑 3.修改索引sql拼接bug 2022-11-01 21:22:25 +08:00
刘宗洋
875de022c1 monaco 2022-11-01 21:22:23 +08:00
Coder慌
2c863a2774 !17 refactor: sql解析失败逻辑重构
Merge pull request !17 from zongyangleo/dev_reafactor_20221031
2022-11-01 07:48:59 +00:00
刘宗洋
f2f086a82c refactor: sql解析失败逻辑重构 2022-11-01 12:45:21 +08:00
meilin.huang
936ca61f94 refactor: 标签列表选择调整 2022-10-31 23:03:26 +08:00
Coder慌
87ae2f81fa !16 bug修复
Merge pull request !16 from zongyangleo/dev_reafactor_20221031
2022-10-31 14:30:16 +00:00
刘宗洋
ecf67db2b1 fix: sql执行器 获取sql bug 2022-10-31 22:27:21 +08:00
meilin.huang
2e5589e112 fix: sql字符串拼接改为占位符形式,防sql注入 2022-10-31 18:39:52 +08:00
Coder慌
2598a60898 !15 代码重构
Merge pull request !15 from zongyangleo/dev_reafactor_20221031
2022-10-31 07:53:32 +00:00
刘宗洋
0de226bbf3 refactor:
1.修改表后,刷新表数据
2.修改索引sql拼接bug
3.暂时只有mysql支持编辑表
2022-10-31 15:43:44 +08:00
meilin.huang
422f0d8491 fix: sql accout表字段修复 2022-10-30 22:56:57 +08:00
meilin.huang
b028708b94 feat: 小功能优化&前端基于setup语法糖重构 2022-10-29 20:08:15 +08:00
meilin.huang
812c0d0f6a feat: 微调 2022-10-27 19:33:21 +08:00
Coder慌
46df5293dd !13 支持编辑表
Merge pull request !13 from zongyangleo/dev_edit_table
2022-10-27 09:58:27 +00:00
刘宗洋
ab42b3e90b feat: mysql 支持编辑表结构、索引 2022-10-27 14:27:32 +08:00
Coder慌
1378259cc7 !12 新增了一些功能
Merge pull request !12 from zongyangleo/dev_link
2022-10-27 06:19:41 +00:00
刘宗洋
c8f0b0a83f 合并代码 2022-10-27 14:09:32 +08:00
meilin.huang
acec760ec1 fix: 字段补充 2022-10-27 14:06:50 +08:00
刘宗洋
2fe70d49f6 Merge remote-tracking branch 'upstream/dev' into dev_link_merge
# Conflicts:
#	mayfly_go_web/src/views/ops/db/DbList.vue
#	mayfly_go_web/src/views/ops/db/SqlExec.vue
#	mayfly_go_web/src/views/ops/mongo/MongoDataOp.vue
#	mayfly_go_web/src/views/ops/mongo/MongoList.vue
#	mayfly_go_web/src/views/ops/redis/DataOperation.vue
#	mayfly_go_web/src/views/ops/redis/RedisList.vue
2022-10-27 10:53:29 +08:00
刘宗洋
9013fff804 feat: DBMS 数据库管理、redis管理、mongo管理,新增【数据操作】快捷跳转 2022-10-27 10:35:11 +08:00
meilin.huang
e925a808c4 feat: 使用标签替代项目 2022-10-26 20:49:29 +08:00
meilin.huang
6c197edddd refactor: db代码review 2022-10-16 14:22:19 +08:00
meilin.huang
c35e91b7b6 refactor: db元信息获取调整 2022-10-16 11:14:13 +08:00
meilin.huang
575947795a refactor: db元信息获取调整 2022-10-16 11:01:45 +08:00
meilin.huang
51f116c7d2 feat: 小问题修复 2022-10-15 17:38:34 +08:00
meilin.huang
c28254855c feat: 菜单sql调整&其他小优化 2022-10-11 08:25:20 +08:00
meilin.huang
e8f3671ffb feat: redis支持设置多库操作 2022-09-29 13:14:50 +08:00
meilin.huang
ac62767a18 fix: 数据库查询-前端long类型精度丢失&bit类型无法展示 2022-09-28 21:40:59 +08:00
meilin.huang
2db4c20dd3 feat: linux文件排序 2022-09-26 18:08:12 +08:00
meilin.huang
cfb7fd5b29 feat: 数据库查询结果导出&其他小问题修复 2022-09-23 14:27:50 +08:00
meilin.huang
22c401f9d8 feat: redis支持list查看&其他小优化 2022-09-22 11:56:21 +08:00
meilin.huang
be00b90c1d refactor: 代码结构调整 2022-09-09 18:26:08 +08:00
meilin.huang
fb3f89c594 feat: 版本升级 2022-09-07 15:20:34 +08:00
meilin.huang
e7a66378ea feat: 终端勾选隧道保存报错问题修复&其他优化 2022-09-07 11:18:47 +08:00
meilin.huang
2f88b48973 feat: 新增终端回放记录&其他小优化 2022-08-29 21:43:24 +08:00
meilin.huang
7761fe0288 feat: 新增系统全局配置&修改账号密码 2022-08-26 20:15:36 +08:00
may-fly
09e6bdcf7e Merge pull request #9 from 1ch0/master
fix: store mongodb password incorrectly
2022-08-26 10:20:12 +08:00
1ch0
61a4d87f59 perf: hide mongodb passwords when printing logs 2022-08-26 10:01:08 +08:00
1ch0
c219ec33b0 fix: store mongodb password incorrectly 2022-08-26 09:58:01 +08:00
may-fly
fd86f36218 Merge pull request #8 from 1ch0/master
Perf: hide mongodb passwords when printing logs
2022-08-25 18:13:38 +08:00
Echo Cheng
efac41f392 Perf: hide mongodb passwords when printing logs 2022-08-25 17:58:07 +08:00
meilin.huang
52df61ae0d refactor: 构建发行版脚本优化 2022-08-24 21:36:16 +08:00
meilin.huang
cf2bc6785c feat: 使用embed将静态资源打包进二进制文件&其他小功能优化 2022-08-24 20:55:42 +08:00
meilin.huang
98a4c92576 feat: redis支持sentinel 2022-08-23 18:50:07 +08:00
meilin.huang
b1ee9b65ff fix: 小问题优化 2022-08-21 21:00:28 +08:00
meilin.huang
99cc4c5e5e fix: script type调整 2022-08-19 22:00:37 +08:00
meilin.huang
226bb8f089 fix: 终端断连提示 2022-08-19 21:42:26 +08:00
meilin.huang
37ed5134e8 feat: 机器脚本入参支持选择框 2022-08-15 20:14:02 +08:00
meilin.huang
0f54d4a472 refactor: code rewiew&功能小优化 2022-08-13 19:31:16 +08:00
Coder慌
64805360d6 update README.md. 2022-08-11 05:52:35 +00:00
Coder慌
7f69fe2ad9 update README.md. 2022-08-11 02:58:06 +00:00
meilin.huang
f913510d3c refactor: code review 2022-08-10 19:46:17 +08:00
meilin.huang
f2d9e7786d refactor: redis hash类型使用hscan获取数据 2022-08-05 21:41:21 +08:00
meilin.huang
e1afb1ed54 fix: sql脚本默认账号密码调整&终端默认配色调整 2022-08-04 20:47:13 +08:00
meilin.huang
12f8cf0111 feat: 资源密码加密处理&登录密码加密加强等 2022-08-02 21:44:01 +08:00
meilin.huang
daa2ef5203 feat: 数据库支持选中数据生成insert语句 2022-07-27 15:36:56 +08:00
meilin.huang
1e3e183930 feat: 优化机器脚本添加参数的前端交互 2022-07-26 18:32:45 +08:00
meilin.huang
366563a0fe fix: sql文件字段名调整 2022-07-24 18:54:23 +08:00
meilin.huang
577802e5ad fix: 定时任务问题修复 2022-07-24 15:37:13 +08:00
meilin.huang
76d6fc3ba5 feat: linux支持ssh隧道访问&其他优化 2022-07-23 16:41:04 +08:00
meilin.huang
f0540559bb feat: 数据库、redis、mongo支持ssh隧道等 2022-07-20 23:25:52 +08:00
Coder慌
802e379f60 !8 feat: 新增mysql ssh代理连接方式
Merge pull request !8 from das/N/A
2022-07-20 03:13:29 +00:00
大圣之家
8c9253da80 feat: 新增mysql ssh代理连接方式 2022-07-20 01:37:25 +00:00
meilin.huang
5271bd21e8 feat: 登录强制校验弱密码&关键信息加密传输 2022-07-18 20:36:31 +08:00
meilin.huang
db554ebdc9 feat: 新增系统操作日志&其他优化 2022-07-14 11:39:12 +08:00
meilin.huang
1c18a01bf6 feat: 新增pgsql数据操作&redis集群操作 2022-07-10 12:14:06 +08:00
meilin.huang
729a3d7028 feat: 新增linux文件夹创建&删除&其他优化 2022-07-04 20:21:24 +08:00
meilin.huang
b88923a128 feat: 数据库表数据支持分页查看 2022-07-02 18:59:46 +08:00
meilin.huang
fe8cd93c78 feat: 新增数据库导出功能&其他小优化 2022-06-30 16:42:25 +08:00
meilin.huang
64b49dae2e fix: 功能优化&小问题修复 2022-06-25 19:52:11 +08:00
meilin.huang
edbbbca5f9 fix: sql脚本导入参数修复 2022-06-21 17:54:07 +08:00
meilin.huang
5ad0d90038 feat: sql执行记录新增查看回滚sql 2022-06-17 17:58:35 +08:00
meilin.huang
9b9173dea7 feat: 新增数据库sql执行记录&执行前原值等信息 2022-06-16 15:55:18 +08:00
meilin.huang
f58331c1c1 feat: mongo新增json编辑器、其他优化 2022-06-08 10:21:02 +08:00
meilin.huang
b2dc9dff0b refactor: 后端包结构重构、去除无用的文件 2022-06-02 17:41:11 +08:00
meilin.huang
51d06ab206 fix: 数据库连接设置超时时间&界面优化 2022-05-27 15:45:12 +08:00
Coder慌
799e0ac9fc readme完善 2022-05-24 07:35:58 +00:00
meilin.huang
56e7a8843b feat: 新增mongo管理与数据操作 2022-05-17 20:23:08 +08:00
472 changed files with 32299 additions and 14030 deletions

4
.gitignore vendored
View File

@@ -15,4 +15,6 @@
*.sum
*/node_modules/
**/vendor/
**/vendor/
.idea
out

18
.vscode/launch.json vendored
View File

@@ -1,18 +0,0 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "mayfly-go",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}/main.go",
"env": {},
"args": []
},
]
}

View File

@@ -1,6 +1,6 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

View File

@@ -1,36 +0,0 @@
# mayfly-go
#### Description
golang实现linux运维等
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@@ -1,7 +1,26 @@
# mayfly-go
# 🌈mayfly-go
<p align="center">
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
</a>
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.18%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统一管理操作平台**
#### 介绍
简单基于DDD(领域驱动设计)分层架构实现web版mysql,redis,linux统一操作管理平台
### 开发语言与主要框架
- 前端typescript、vue3、element-plus
@@ -9,25 +28,57 @@
### 交流及问题反馈加 QQ 群
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?jump_from=webapi">119699946</a>
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=IdJSHW0jTMhmWFHBUS9a83wxtrxDDhFj&jump_from=webapi">119699946</a>
### 系统相关资料
- 项目文档: https://objs.gitee.io/mayfly-go-docs
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
- 部署文档https://objs.gitee.io/mayfly-go-docs/download
### 系统功能
记录操作记录
### 系统核心功能截图
##### 记录操作记录
![记录操作记录](https://images.gitee.com/uploads/images/2021/0508/204608_83ef7c33_1240250.png "屏幕截图.png")
#### 系统管理
账号管理
#### 机器操作
##### 状态查看
![状态查看](https://objs.gitee.io/mayfly-go-docs/home/machine-status.jpg "屏幕截图.png")
##### ssh终端
![ssh终端](https://objs.gitee.io/mayfly-go-docs/home/machine-ssh.jpg "屏幕截图.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")
#### 数据库操作
##### sql编辑器
![sql编辑器](https://objs.gitee.io/mayfly-go-docs/home/dbms-sql-editor.jpg "屏幕截图.png")
##### 在线增删改查数据
![选表查数据](https://objs.gitee.io/mayfly-go-docs/home/dbms-show-table-data.jpg "屏幕截图.png")
#### Redis操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png")
#### Mongo操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/mongo-op.jpg "屏幕截图.png")
##### 系统管理
##### 账号管理
![账号管理](https://images.gitee.com/uploads/images/2021/0607/173919_a8d7dc18_1240250.png "屏幕截图.png")
角色管理
##### 角色管理
![角色管理](https://images.gitee.com/uploads/images/2021/0607/174028_3654fb28_1240250.png "屏幕截图.png")
资源管理
##### 资源管理
![资源管理](https://images.gitee.com/uploads/images/2021/0607/174436_e9e1535c_1240250.png "屏幕截图.png")
**其他更多功能&操作指南可查看在线文档**: https://objs.gitee.io/mayfly-go-docs

View File

@@ -1,34 +0,0 @@
package biz
// 业务错误
type BizError struct {
code int16
err string
}
var (
Success *BizError = NewBizErrCode(200, "success")
BizErr *BizError = NewBizErrCode(400, "biz error")
ServerError *BizError = NewBizErrCode(500, "server error")
PermissionErr *BizError = NewBizErrCode(501, "token error")
)
// 错误消息
func (e *BizError) Error() string {
return e.err
}
// 错误码
func (e *BizError) Code() int16 {
return e.code
}
// 创建业务逻辑错误结构体,默认为业务逻辑错误
func NewBizErr(msg string) *BizError {
return &BizError{code: BizErr.code, err: msg}
}
// 创建业务逻辑错误结构体可设置指定错误code
func NewBizErrCode(code int16, msg string) *BizError {
return &BizError{code: code, err: msg}
}

View File

@@ -1,28 +0,0 @@
package captcha
import (
"mayfly-go/base/biz"
"github.com/mojocn/base64Captcha"
)
var store = base64Captcha.DefaultMemStore
var driver base64Captcha.Driver = base64Captcha.DefaultDriverDigit
// 生成验证码
func Generate() (string, string) {
c := base64Captcha.NewCaptcha(driver, store)
// 获取
id, b64s, err := c.Generate()
biz.ErrIsNilAppendErr(err, "获取验证码错误: %s")
return id, b64s
}
// 验证验证码
func Verify(id string, val string) bool {
if id == "" || val == "" {
return false
}
// 同时清理掉这个图片
return store.Verify(id, val, true)
}

View File

@@ -1,12 +0,0 @@
package config
import "fmt"
type App struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
}
func (a *App) GetAppInfo() string {
return fmt.Sprintf("[%s:%s]", a.Name, a.Version)
}

View File

@@ -1,32 +0,0 @@
package config
import "fmt"
type Server struct {
Port int `yaml:"port"`
Model string `yaml:"model"`
Cors bool `yaml:"cors"`
Tls *Tls `yaml:"tls"`
Static *[]*Static `yaml:"static"`
StaticFile *[]*StaticFile `yaml:"static-file"`
}
func (s *Server) GetPort() string {
return fmt.Sprintf(":%d", s.Port)
}
type Static struct {
RelativePath string `yaml:"relative-path"`
Root string `yaml:"root"`
}
type StaticFile struct {
RelativePath string `yaml:"relative-path"`
Filepath string `yaml:"filepath"`
}
type Tls struct {
Enable bool `yaml:"enable"` // 是否启用tls
KeyFile string `yaml:"key-file"` // 私钥文件路径
CertFile string `yaml:"cert-file"` // 证书文件路径
}

View File

@@ -1,13 +0,0 @@
package global
import (
"github.com/go-redis/redis"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
var (
Log *logrus.Logger // 日志
Db *gorm.DB // gorm
RedisCli *redis.Client // redis
)

View File

@@ -1,15 +0,0 @@
package model
type AppContext struct {
}
type LoginAccount struct {
Id uint64
Username string
}
type Permission struct {
CheckToken bool // 是否检查token
Code string // 权限码
Name string // 描述
}

View File

@@ -1,13 +0,0 @@
package model
// 分页参数
type PageParam struct {
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
}
// 分页结果
type PageResult struct {
Total int64 `json:"total"`
List interface{} `json:"list"`
}

View File

@@ -1,16 +0,0 @@
package starter
import (
"mayfly-go/base/global"
)
func PrintBanner() {
global.Log.Print(`
__ _
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) |
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|___/ |___/ |___/
`)
}

View File

@@ -1,34 +0,0 @@
package starter
import (
"mayfly-go/base/biz"
"mayfly-go/base/config"
"mayfly-go/base/ctx"
"mayfly-go/base/global"
"mayfly-go/server/initialize"
)
func RunWebServer() {
// 权限处理器
ctx.UseBeforeHandlerInterceptor(ctx.PermissionHandler)
// 日志处理器
ctx.UseAfterHandlerInterceptor(ctx.LogHandler)
// 注册路由
web := initialize.InitRouter()
server := config.Conf.Server
port := server.GetPort()
if app := config.Conf.App; app != nil {
global.Log.Infof("%s- Listening and serving HTTP on %s", app.GetAppInfo(), port)
} else {
global.Log.Infof("Listening and serving HTTP on %s", port)
}
var err error
if server.Tls != nil && server.Tls.Enable {
err = web.RunTLS(port, server.Tls.CertFile, server.Tls.KeyFile)
} else {
err = web.Run(port)
}
biz.ErrIsNilAppendErr(err, "服务启动失败: %s")
}

View File

@@ -1,13 +0,0 @@
package utils
import (
"crypto/md5"
"encoding/hex"
)
// md5
func Md5(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}

200
build_release.sh Executable file
View File

@@ -0,0 +1,200 @@
#bin/bash
#----------------------------------------------
# 前后端打包编译至指定目录,即快速制作发行版
#----------------------------------------------
project_path=`pwd`
# 构建后的二进制执行文件名
exec_file_name="mayfly-go"
# web项目目录
web_folder="${project_path}/mayfly_go_web"
# server目录
server_folder="${project_path}/server"
function echo_red() {
echo -e "\033[1;31m$1\033[0m"
}
function echo_green() {
echo -e "\033[1;32m$1\033[0m"
}
function echo_yellow() {
echo -e "\033[1;33m$1\033[0m"
}
function buildWeb() {
cd ${web_folder}
copy2Server=$1
echo_yellow "-------------------打包前端开始-------------------"
yarn run build
if [ "${copy2Server}" == "2" ] ; then
echo_green '将打包后的静态文件拷贝至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"
}
function build() {
cd ${project_path}
# 打包产物的输出目录
toFolder=$1
os=$2
arch=$3
copyDocScript=$4
echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
cd ${server_folder}
echo_green "打包构建可执行文件..."
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
if [ -d ${toFolder} ] ; then
echo_green "目标文件夹已存在,清空文件夹"
sudo rm -rf ${toFolder}
fi
echo_green "创建'${toFolder}'目录"
mkdir ${toFolder}
echo_green "移动二进制文件至'${toFolder}'"
mv ${server_folder}/${execFileName} ${toFolder}
# if [ "${copy2Server}" == "1" ] ; then
# echo_green "拷贝前端静态页面至'${toFolder}/static'"
# mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
}
function buildLinuxAmd64() {
build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
}
function buildLinuxArm64() {
build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
}
function buildWindows() {
build "$1/mayfly-go-windows" "windows" "amd64" $2
}
function buildMac() {
build "$1/mayfly-go-mac" "darwin" "amd64" $2
}
function buildDocker() {
echo_yellow "-------------------构建docker镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="mayflygo/mayfly-go:${imageVersion}"
docker build -t "${imageName}" .
echo_green "docker镜像构建完成->[${imageName}]"
echo_yellow "-------------------构建docker镜像结束-------------------"
}
function buildxDocker() {
echo_yellow "-------------------docker buildx构建镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="mayflygo/mayfly-go:${imageVersion}"
docker buildx build --push --platform linux/amd64,linux/arm64 -t "${imageName}" .
echo_green "docker多版本镜像构建完成->[${imageName}]"
echo_yellow "-------------------docker buildx构建镜像结束-------------------"
}
function runBuild() {
read -p "请选择构建版本[0|其他->除docker镜像外其他 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
toPath="."
imageVersion="latest"
copyDocScript="1"
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
# 构建结果的目的路径
read -p "请输入构建产物输出目录[默认当前路径]: " toPath
if [ ! -d ${toPath} ] ; then
echo_red "构建产物输出目录不存在!"
exit;
fi
if [ "${toPath}" == "" ] ; then
toPath="."
fi
read -p "是否拷贝文档&脚本[0->否 1->是][默认是]: " copyDocScript
if [ "${copyDocScript}" == "" ] ; then
copyDocScript="1"
fi
# 进入目标路径,并赋值全路径
cd ${toPath}
toPath=`pwd`
fi
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
read -p "请输入docker镜像版本号[默认latest]: " imageVersion
if [ "${imageVersion}" == "" ] ; then
imageVersion="latest"
fi
fi
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
runBuildWeb="2"
# 编译web前端
buildWeb ${runBuildWeb}
case ${buildType} in
"1")
buildLinuxAmd64 ${toPath} ${copyDocScript}
;;
"2")
buildLinuxArm64 ${toPath} ${copyDocScript}
;;
"3")
buildWindows ${toPath} ${copyDocScript}
;;
"4")
buildMac ${toPath} ${copyDocScript}
;;
"5")
buildDocker ${imageVersion}
;;
"6")
buildxDocker ${imageVersion}
;;
*)
buildLinuxAmd64 ${toPath} ${copyDocScript}
buildLinuxArm64 ${toPath} ${copyDocScript}
buildWindows ${toPath} ${copyDocScript}
buildMac ${toPath} ${copyDocScript}
;;
esac
echo_green "删除['${server_folder}/static/static']下静态资源文件."
# 删除静态资源文件保留一个favicon.ico否则后端启动会报错
rm -rf ${server_folder}/static/static/assets
rm -rf ${server_folder}/static/static/config.js
rm -rf ${server_folder}/static/static/index.html
}
runBuild

49
go.mod
View File

@@ -1,49 +0,0 @@
module mayfly-go
go 1.17
require (
// jwt
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis v6.15.9+incompatible
github.com/gorilla/websocket v1.5.0
//
github.com/mojocn/base64Captcha v1.3.5
github.com/pkg/sftp v1.13.4
//
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
// ssh
golang.org/x/crypto v0.0.0-20220314234724-5d542ad81a58
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
// gorm
gorm.io/driver/mysql v1.3.2
gorm.io/gorm v1.23.2
)
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

220
go.sum
View File

@@ -1,220 +0,0 @@
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig=
github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234724-5d542ad81a58 h1:L8CkJyVoa0/NslN3RUMLgasK5+KatNvyRGQ9QyCYAfc=
golang.org/x/crypto v0.0.0-20220314234724-5d542ad81a58/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.2 h1:QJryWiqQ91EvZ0jZL48NOpdlPdMjdip1hQ8bTgo4H7I=
gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.2 h1:xmq9QRMWL8HTJyhAUBXy8FqIIQCYESeKfJL4DoGKiWQ=
gorm.io/gorm v1.23.2/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
@@ -18,8 +18,7 @@
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./config.js"></script>
<script type="application/javascript" src="./config.js"></script>
<script type="module" src="/src/main.ts"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
</body>
</html>

5164
mayfly_go_web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,29 +4,34 @@
"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": {
"axios": "^0.26.1",
"codemirror": "^5.65.2",
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.1.0",
"axios": "^1.3.4",
"countup.js": "^2.0.7",
"cropperjs": "^1.5.11",
"echarts": "^5.3.2",
"element-plus": "^2.1.11",
"@element-plus/icons-vue": "^1.1.3",
"jsonlint": "^1.6.3",
"echarts": "^5.4.0",
"element-plus": "^2.2.33",
"jsencrypt": "^3.3.1",
"lodash": "^4.17.21",
"mitt": "^3.0.0",
"monaco-editor": "^0.36.1",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.2",
"nprogress": "^0.2.0",
"screenfull": "^5.1.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sql-formatter": "^12.1.2",
"vue": "^3.2.47",
"vue-clipboard3": "^1.0.1",
"sql-formatter": "^4.0.2",
"vue": "^3.2.30",
"vue-router": "^4.0.12",
"vue-router": "^4.1.6",
"vuex": "^4.0.2",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0"
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@types/lodash": "^4.14.178",
@@ -35,16 +40,16 @@
"@types/sortablejs": "^1.10.6",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-vue": "^1.2.2",
"@vitejs/plugin-vue": "^2.3.3",
"@vue/compiler-sfc": "^3.0.11",
"dotenv": "^10.0.0",
"eslint": "^8.5.0",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.3.0",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"typescript": "^4.2.4",
"vite": "^2.8.6",
"sass": "^1.58.0",
"sass-loader": "^13.2.0",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vue-eslint-parser": "^8.0.1"
},
"browserslist": [

View File

@@ -1,4 +1,24 @@
window.globalConfig = {
"BaseApiUrl": "http://localhost:8888",
"BaseWsUrl": "ws://localhost:8888"
}
// 默认为空以访问根目录为api请求地址。若前后端分离部署可单独配置该后端api请求地址
"BaseApiUrl": "",
"BaseWsUrl": ""
}
// index.html添加百秒级时间戳防止被浏览器缓存
!function () {
let t = "t=" + new Date().getTime().toString().substring(0, 8)
let search = location.search;
let m = search && search.match(/t=\d*/g)
if (m[0]) {
if (m[0] !== t) {
location.search = search.replace(m[0], t)
}
} else {
if (search.indexOf('?') > -1) {
location.search = search + '&' + t
} else {
location.search = t
}
}
}()

View File

@@ -1,8 +1,21 @@
/* eslint-disable */
import {IDisposable} from 'monaco-editor';
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module 'codemirror';
declare module 'sql-formatter';
declare global {
interface Window {
completionItemProvider?: IDisposable | undefined;
}
}
declare module 'sql-formatter';
declare module 'jsoneditor';
declare module 'asciinema-player';
declare module 'monaco-editor';

View File

@@ -11,6 +11,8 @@ import { useStore } from '@/store/index.ts';
import { getLocal } from '@/common/utils/storage.ts';
import LockScreen from '@/views/layout/lockScreen/index.vue';
import Setings from '@/views/layout/navBars/breadcrumb/setings.vue';
import Watermark from '@/common/utils/wartermark.ts';
export default defineComponent({
name: 'app',
components: { LockScreen, Setings },
@@ -57,6 +59,8 @@ export default defineComponent({
() => route.path,
() => {
nextTick(() => {
// 路由变化更新水印
Watermark.use();
document.title = `${route.meta.title} - ${getThemeConfig.value.globalTitle}` || getThemeConfig.value.globalTitle;
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -19,24 +19,6 @@ class Api {
this.method = method;
}
/**
* 设置rl
* @param {String} uri 请求url
*/
setUrl(url: string) {
this.url = url;
return this;
}
/**
* url的请求方法
* @param {String} method 请求方法
*/
setMethod(method: string) {
this.method = method;
return this;
}
/**
* 获取权限的完整url
*/

View File

@@ -1,6 +1,17 @@
function getBaseApiUrl() {
let path = window.location.pathname;
if (path == '/') {
return window.location.host;
}
return window.location.host + path;
}
const config = {
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl}/api`
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.4.1'
}
export default config

View File

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

View File

@@ -1,8 +1,11 @@
import request from './request'
export default {
login: (param: any) => request.request('POST', '/sys/accounts/login', param, null),
captcha: () => request.request('GET', '/sys/captcha', null, null),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param, null),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param, null)
login: (param: any) => request.request('POST', '/sys/accounts/login', param),
changePwd: (param: any) => request.request('POST', '/sys/accounts/change-pwd', param),
getPublicKey: () => request.request('GET', '/common/public-key'),
getConfigValue: (param: any) => request.request('GET', '/sys/configs/value', param),
captcha: () => request.request('GET', '/sys/captcha'),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param)
}

View File

@@ -0,0 +1,36 @@
import openApi from './openApi';
import JSEncrypt from 'jsencrypt'
import { notBlank } from './assert';
var encryptor: any = null
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey')
if (publicKey) {
return publicKey
}
publicKey = await openApi.getPublicKey() as string
sessionStorage.setItem('RsaPublicKey', publicKey)
return publicKey
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return ""
}
if (encryptor != null) {
return encryptor.encrypt(value)
}
encryptor = new JSEncrypt()
const publicKey = await getRsaPublicKey() as string;
notBlank(publicKey, "获取公钥失败")
encryptor.setPublicKey(publicKey)//设置公钥
return encryptor.encrypt(value)
}

View File

@@ -0,0 +1,48 @@
import openApi from './openApi';
// 登录是否使用验证码配置key
const UseLoginCaptchaConfigKey = "UseLoginCaptcha"
const UseWartermarkConfigKey = "UseWartermark"
/**
* 获取系统配置值
*
* @param key 配置key
* @returns 配置值
*/
export async function getConfigValue(key: string) : Promise<string> {
return await openApi.getConfigValue({key}) as string
}
/**
* 获取bool类型系统配置值
*
* @param key 配置key
* @param defaultValue 默认值
* @returns 是否为ture1: true其他: false
*/
export async function getBoolConfigValue(key :string, defaultValue :boolean) : Promise<boolean> {
const value = await getConfigValue(key)
if (!value) {
return defaultValue;
}
return value == "1";
}
/**
* 是否使用登录验证码
*
* @returns
*/
export async function useLoginCaptcha() : Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true)
}
/**
* 是否启用水印
*
* @returns
*/
export async function useWartermark() : Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true)
}

View File

@@ -1,4 +1,4 @@
export function dateFormat(fmt: string, date: Date) {
export function dateFormat2(fmt: string, date: Date) {
let ret;
const opt = {
"y+": date.getFullYear().toString(), // 年
@@ -19,5 +19,9 @@ export function dateFormat(fmt: string, date: Date) {
}
export function dateStrFormat(fmt: string, dateStr: string) {
return dateFormat(fmt, new Date(dateStr))
return dateFormat2(fmt, new Date(dateStr))
}
export function dateFormat(dateStr: string) {
return dateFormat2('yyyy-MM-dd HH:mm:ss',new Date(dateStr))
}

View File

@@ -0,0 +1,39 @@
export function exportCsv(filename: string, columns: string[], datas: []) {
// 二维数组
const cvsData = [columns];
for (let data of datas) {
// 数据值组成的一维数组
let dataValueArr: any = [];
for (let column of columns) {
let val: any = data[column];
if (typeof val == 'string' && val) {
// csv格式如果有逗号整体用双引号括起来如果里面还有双引号就替换成两个双引号这样导出来的格式就不会有问题了
if (val.indexOf(',') != -1) {
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
if (val.indexOf('"') != -1) {
val = val.replace(/\"/g, "\"\"");
}
// 再将逗号转义
val = `"${val}"`;
}
dataValueArr.push(val);
} else {
dataValueArr.push(val);
}
}
cvsData.push(dataValueArr);
}
const csvString = cvsData.map((e) => e.join(',')).join('\n');
// 导出
let link = document.createElement('a');
let exportContent = '\uFEFF';
let blob = new Blob([exportContent + csvString], {
type: 'text/plain;charset=utrf-8',
});
link.id = 'download-csv';
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', `${filename}.csv`);
document.body.appendChild(link);
link.click();
}

View File

@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
import loadingCss from '@/theme/loading.scss';
import loadingCss from "@/theme/loading.scss?inline"
// 定义方法
export const NextLoading = {

View File

@@ -35,3 +35,22 @@ export function removeSession(key: string) {
export function clearSession() {
window.sessionStorage.clear();
}
export function getUserInfo4Session() {
return getSession("userInfo")
}
export function setUserInfo2Session(userinfo: any) {
setSession("userInfo", userinfo)
}
// 获取是否开启水印
export function getUseWatermark4Session() {
return getSession("useWatermark")
}
export function setUseWatermark2Session(useWatermark: boolean) {
setSession("useWatermark", useWatermark)
}

View File

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

View File

@@ -1,339 +0,0 @@
<template>
<div class="in-coder-panel">
<textarea ref="textarea"></textarea>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="mode" @change="changeMode">
<el-option v-for="mode in modes" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select>
</div>
</template>
<script lang="ts">
import { ref, nextTick, toRefs, reactive, watch, onMounted, defineComponent } from 'vue';
// 引入全局实例
import _CodeMirror from 'codemirror';
// 核心样式
import 'codemirror/lib/codemirror.css';
// 引入主题后还需要在 options 中指定主题才会生效
import 'codemirror/theme/cobalt.css';
import 'codemirror/addon/selection/active-line.js';
// 匹配括号
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/comment/comment';
// 需要引入具体的语法高亮库才会有对应的语法高亮效果
// codemirror 官方其实支持通过 /addon/mode/loadmode.js 和 /mode/meta.js 来实现动态加载对应语法高亮库
// 但 vue 貌似没有无法在实例初始化后再动态加载对应 JS ,所以此处才把对应的 JS 提前引入
import 'codemirror/mode/yaml/yaml.js';
import 'codemirror/mode/dockerfile/dockerfile.js';
import 'codemirror/mode/nginx/nginx.js';
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/css/css.js';
import 'codemirror/mode/xml/xml.js';
import 'codemirror/mode/markdown/markdown.js';
import 'codemirror/mode/python/python.js';
import 'codemirror/mode/shell/shell.js';
import 'codemirror/mode/sql/sql.js';
import 'codemirror/mode/vue/vue.js';
import 'codemirror/mode/textile/textile.js';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint.js';
import { ElOption, ElSelect } from 'element-plus';
// 尝试获取全局实例
const CodeMirror = (window as any).CodeMirror || _CodeMirror;
export default defineComponent({
name: 'CodeMirror',
components: {
ElOption,
ElSelect,
},
props: {
modelValue: {
type: String,
},
language: {
type: String,
default: null,
},
height: {
type: String,
default: "500px",
},
width: {
type: String,
default: "auto",
},
canChangeMode: {
type: Boolean,
default: false,
},
options: {
type: Object,
default: null,
},
},
setup(props: any, { emit }) {
let { modelValue, language } = toRefs(props);
const textarea: any = ref(null);
// 编辑器实例
let coder = null as any;
const state = reactive({
coder: null as any,
content: '',
// 默认的语法类型
mode: 'x-sh',
// 默认配置
options: {
// 缩进格式
tabSize: 2,
// 主题,对应主题库 JS 需要提前引入
theme: 'cobalt',
// 显示行号
lineNumbers: true,
line: true,
indentWithTabs: true,
smartIndent: true,
matchBrackets: true,
autofocus: true,
styleSelectedText: true,
styleActiveLine: true, // 高亮选中行
foldGutter: true, // 块槽
// extraKeys: { Tab: 'autocomplete' }, // 自定义快捷键
hintOptions: {
// 当匹配只有一项的时候是否自动补全
completeSingle: false,
},
},
// 支持切换的语法高亮类型,对应 JS 已经提前引入
// 使用的是 MIME-TYPE ,不过作为前缀的 text/ 在后面指定时写死了
modes: [
{
value: 'x-sh',
label: 'Shell',
},
{
value: 'x-yaml',
label: 'Yaml',
},
{
value: 'x-dockerfile',
label: 'Dockerfile',
},
{
value: 'x-nginx-conf',
label: 'Nginx',
},
{
value: 'html',
label: 'XML/HTML',
},
{
value: 'x-python',
label: 'Python',
},
{
value: 'x-sql',
label: 'SQL',
},
{
value: 'css',
label: 'CSS',
},
{
value: 'javascript',
label: 'Javascript',
},
{
value: 'x-java',
label: 'Java',
},
{
value: 'x-vue',
label: 'Vue',
},
{
value: 'markdown',
label: 'Markdown',
},
{
value: 'text/x-textile',
label: 'text',
},
],
});
onMounted(() => {
init();
});
watch(
() => props.modelValue,
(newValue) => {
handerCodeChange(newValue);
}
);
// watch(
// () => props.options,
// (newValue, oldValue) => {
// for (const key in newValue) {
// coder.setOption(key, newValue[key]);
// }
// }
// );
const init = () => {
if (props.options) {
state.options = props.options;
}
// 初始化编辑器实例,传入需要被实例化的文本域对象和默认配置
coder = CodeMirror.fromTextArea(textarea.value, state.options);
coder.setValue(modelValue.value || state.content);
// 支持双向绑定
coder.on('change', (coder: any) => {
state.content = coder.getDoc().getValue();
emit('update:modelValue', state.content);
});
coder.on('inputRead', (instance: any, changeObj: any) => {
if (/^[a-zA-Z]/.test(changeObj.text[0])) {
instance.showHint();
}
});
coder.setSize(props.width, props.height);
// editor.setSize('width','height');
// 修改编辑器的语法配置
setMode(language.value);
[
'scroll',
'changes',
'beforeChange',
'cursorActivity',
'keyHandled',
'inputRead',
'electricInput',
'beforeSelectionChange',
'viewportChange',
'swapDoc',
'gutterClick',
'gutterContextMenu',
'focus',
'blur',
'refresh',
'optionChange',
'scrollCursorIntoView',
'update',
].forEach((event) => {
// 循环事件,并兼容 run-time 事件命名
coder.on(event, (...args: any) => {
// console.log('当有事件触发了', event, args);
emit(event, ...args);
const lowerCaseEvent = event.replace(/([A-Z])/g, '-$1').toLowerCase();
if (lowerCaseEvent !== event) {
emit(lowerCaseEvent, ...args);
}
});
});
state.coder = coder;
// 不加无法显示内容,需点击后才可显示
refresh();
};
const refresh = () => {
nextTick(() => {
coder.refresh();
});
};
// 设置模式
const setMode = (val: string) => {
if (val) {
// 获取具体的语法类型对象
let modeObj = getLanguage(val);
// 判断父容器传入的语法是否被支持
if (modeObj) {
state.mode = modeObj.value;
}
}
// 修改编辑器的语法配置
coder.setOption('mode', `text/${state.mode}`);
};
// 获取当前语法类型
const getLanguage = (language: string) => {
// 在支持的语法类型列表中寻找传入的语法类型
return state.modes.find((mode: any) => {
// 所有的值都忽略大小写,方便比较
let currentLanguage = language.toLowerCase();
let currentLabel = mode.label.toLowerCase();
let currentValue = mode.value.toLowerCase();
// 由于真实值可能不规范,例如 java 的真实值是 x-java ,所以讲 value 和 label 同时和传入语法进行比较
return currentLabel === currentLanguage || currentValue === currentLanguage;
});
};
// 更改模式
const changeMode = (val: string) => {
setMode(val);
// 获取修改后的语法
let label = (getLanguage(val) as any).label.toLowerCase();
// 允许父容器通过以下函数监听当前的语法值
emit('language-change', label);
};
const handerCodeChange = (newVal: string) => {
const cm_value = coder.getValue();
if (newVal !== cm_value) {
const scrollInfo = coder.getScrollInfo();
coder.setValue(newVal);
state.content = newVal;
coder.scrollTo(scrollInfo.left, scrollInfo.top);
refresh()
}
};
return {
...toRefs(state),
textarea,
changeMode,
refresh,
};
},
});
</script>
<style lang="scss">
.in-coder-panel {
flex-grow: 1;
display: flex;
position: relative;
.CodeMirror {
flex-grow: 1;
z-index: 1;
.CodeMirror-code {
line-height: 19px;
}
font-family: 'JetBrainsMono';
}
.code-mode-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
}
}
</style>

View File

@@ -1,20 +0,0 @@
import _CodeMirror from 'codemirror'
import codemirror from './codemirror.vue'
const CodeMirror = window.CodeMirror || _CodeMirror
const install = (Vue, config) => {
if (config) {
if (config.options) {
codemirror.props.globalOptions.default = () => config.options
}
if (config.events) {
codemirror.props.globalEvents.default = () => config.events
}
}
Vue.component(codemirror.name, codemirror)
}
const VueCodemirror = { CodeMirror, codemirror, install }
export default VueCodemirror
export { CodeMirror, codemirror, install }

View File

@@ -0,0 +1,280 @@
<template>
<div class="monaco-editor" style="border: 1px solid #ccc;">
<div ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-option v-for="mode in languages" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import * as monaco from 'monaco-editor';
import { editor, languages } from 'monaco-editor';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
// import Monokai from 'monaco-themes/themes/Monokai.json'
// import Active4D from 'monaco-themes/themes/Active4D.json'
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
// import krTheme from 'monaco-themes/themes/krTheme.json'
// import Dracula from 'monaco-themes/themes/Dracula.json'
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json'
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
import { ElOption, ElSelect } from 'element-plus';
const props = defineProps({
modelValue: {
type: String,
},
language: {
type: String,
default: null,
},
height: {
type: String,
default: '500px',
},
width: {
type: String,
default: 'auto',
},
canChangeMode: {
type: Boolean,
default: false,
},
options: {
type: Object,
default: null,
},
})
//定义事件
const emit = defineEmits(['update:modelValue'])
const languages = [
{
value: 'shell',
label: 'Shell',
},
{
value: 'json',
label: 'JSON',
},
{
value: 'yaml',
label: 'Yaml',
},
{
value: 'dockerfile',
label: 'Dockerfile',
},
{
value: 'html',
label: 'XML/HTML',
},
{
value: 'python',
label: 'Python',
},
{
value: 'sql',
label: 'SQL',
},
{
value: 'css',
label: 'CSS',
},
{
value: 'javascript',
label: 'Javascript',
},
{
value: 'java',
label: 'Java',
},
{
value: 'markdown',
label: 'Markdown',
},
{
value: 'text',
label: 'text',
},
];
const options = {
language: 'shell',
theme: 'SolarizedLight',
automaticLayout: true, //自适应宽高布局
foldingStrategy: 'indentation',//代码可分小段折叠
roundedSelection: false, // 禁用选择文本背景的圆角
matchBrackets: 'near',
linkedEditing: true,
cursorBlinking: 'smooth',// 光标闪烁样式
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时在编辑器中缩放字体
overviewRulerBorder: false, // 不要滚动条的边框
tabSize: 4, // tab 缩进长度
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
fontWeight: 'bold',
// fontSize: 12,
// letterSpacing: 1, 字符间距
// quickSuggestions:false, // 禁用代码提示
minimap: {
enabled: false, // 不要小地图
},
}
const state = reactive({
languageMode: 'shell',
})
const {
languageMode,
} = toRefs(state)
onMounted(() => {
state.languageMode = props.language;
initMonacoEditorIns();
setEditorValue(props.modelValue);
registerCompletionItemProvider();
});
onBeforeUnmount(() => {
if (monacoEditorIns) {
monacoEditorIns.dispose();
}
if (completionItemProvider) {
completionItemProvider.dispose();
}
})
watch(() => props.modelValue, (newValue: any) => {
if (!monacoEditorIns.hasTextFocus()) {
state.languageMode = props.language;
monacoEditorIns?.setValue(newValue);
}
})
watch(() => props.language, (newValue: any) => {
changeLanguage(newValue);
})
const monacoTextarea: any = ref(null);
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker()
}
return new EditorWorker();
}
};
const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器')
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
options.language = state.languageMode;
// 从localStorage中获取通过store可能存在父子组件都使用store报错
options.theme = JSON.parse(localStorage.getItem('themeConfig') as string).editorTheme || 'vs';
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(options, props.options as any));
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
emit('update:modelValue', monacoEditorIns.getModel()?.getValue());
})
// 动态设置主题
// monaco.editor.setTheme('hc-black');
};
const changeLanguage = (value: any) => {
console.log('change lan');
// 获取当前的文档模型
let oldModel = monacoEditorIns.getModel()
if (!oldModel) {
return;
}
// 创建一个新的文档模型
let newModel = monaco.editor.createModel(oldModel.getValue(), value)
// 设置成新的
monacoEditorIns.setModel(newModel)
// 销毁旧的模型
if (oldModel) {
oldModel.dispose()
}
registerCompletionItemProvider();
}
const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value)
}
/**
* 注册联想补全提示
*/
const registerCompletionItemProvider = () => {
if (completionItemProvider) {
completionItemProvider.dispose();
}
if (state.languageMode == 'shell') {
registeShell()
}
}
const registeShell = () => {
completionItemProvider = monaco.languages.registerCompletionItemProvider('shell', {
provideCompletionItems: async () => {
let suggestions: languages.CompletionItem[] = []
shellLan.keywords.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
} as any);
})
shellLan.builtins.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Property,
insertText: item,
} as any);
})
return {
suggestions: suggestions
};
}
})
};
const format = () => {
/*
触发自动格式化;
*/
monacoEditorIns.trigger('', 'editor.action.formatDocument', '')
}
defineExpose({ format })
</script>
<style lang="scss">
.monaco-editor {
.code-mode-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
}
}
</style>

View File

@@ -18,6 +18,8 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import '@/assets/font/font.css'
const app = createApp(App);
// 屏蔽警告信息
app.config.warnHandler = () => null;
/**
* 导出全局注册 element plus svg 图标

View File

@@ -2,20 +2,29 @@ import RouterParent from '@/views/layout/routerView/parent.vue';
export const imports = {
'RouterParent': RouterParent,
"Home": () => import('@/views/home/index.vue'),
"Home": () => import('@/views/home/Home.vue'),
'Personal': () => import('@/views/personal/index.vue'),
// machine
"MachineList": () => import('@/views/ops/machine'),
"MachineList": () => import('@/views/ops/machine/MachineList.vue'),
"AuthCertList": () => import('@/views/ops/machine/authcert/AuthCertList.vue'),
// sys
"ResourceList": () => import('@/views/system/resource'),
"RoleList": () => import('@/views/system/role'),
"AccountList": () => import('@/views/system/account'),
// project
"ProjectList": () => import('@/views/ops/project/ProjectList.vue'),
"SyslogList": () => import('@/views/system/syslog/SyslogList.vue'),
"ConfigList": () => import('@/views/system/config/ConfigList.vue'),
// tag
"TagTreeList": () => import('@/views/ops/tag/TagTreeList.vue'),
"TeamList": () => import('@/views/ops/tag/TeamList.vue'),
// db
"DbList": () => import('@/views/ops/db/DbList.vue'),
"SqlExec": () => import('@/views/ops/db'),
// redis
"RedisList": () => import('@/views/ops/redis'),
"DataOperation": () => import('@/views/ops/redis/DataOperation.vue'),
// mongo
"MongoDataOp": () => import('@/views/ops/mongo/MongoDataOp.vue'),
// redis
"MongoList": () => import('@/views/ops/mongo/MongoList.vue'),
}

View File

@@ -221,26 +221,32 @@ router.beforeEach((to, from, next) => {
if (to.path === '/login' && !token) {
next();
NProgress.done();
} else {
if (!token) {
next(`/login?redirect=${to.path}`);
clearSession();
resetRoute();
NProgress.done();
return;
}
if (!token) {
next(`/login?redirect=${to.path}`);
clearSession();
resetRoute();
NProgress.done();
if (SysWs) {
SysWs.close();
SysWs = null;
}
} else if (token && to.path === '/login') {
next('/');
NProgress.done();
} else {
if (!SysWs) {
SysWs = sockets.sysMsgSocket();
}
if (store.state.routesList.routesList.length > 0) next();
if (SysWs) {
SysWs.close();
SysWs = null;
}
return;
}
if (token && to.path === '/login') {
next('/');
NProgress.done();
return;
}
// 终端不需要连接系统websocket消息
if (!SysWs && to.path != '/machine/terminal') {
SysWs = sockets.sysMsgSocket();
}
if (store.state.routesList.routesList.length > 0) {
next();
}
});

View File

@@ -1,6 +1,6 @@
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/views/layout/index.vue'
import RouterParent from '@/views/layout/routerView/parent.vue';
// import RouterParent from '@/views/layout/routerView/parent.vue';
// 定义动态路由
export const dynamicRoutes = [
@@ -12,119 +12,108 @@ export const dynamicRoutes = [
meta: {
isKeepAlive: true,
},
children: [
{
path: '/home',
name: 'home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
// iframe链接
link: '',
// 是否在菜单栏显示,默认显示
isHide: false,
isKeepAlive: true,
// tag标签是否不可删除
isAffix: true,
// 是否为iframe
isIframe: false,
icon: 'el-icon-s-home',
},
},
{
path: '/sys',
name: 'Resource',
redirect: '/sys/resources',
meta: {
title: '系统管理',
// 资源code用于校验用户是否拥有该资源权限
code: 'sys',
// isKeepAlive: true,
icon: 'el-icon-monitor',
},
children: [
{
path: 'sys/resources',
name: 'ResourceList',
component: () => import('@/views/system/resource'),
meta: {
title: '资源管理',
code: 'resource:list',
isKeepAlive: true,
icon: 'el-icon-menu',
},
},
{
path: 'sys/roles',
name: 'RoleList',
component: () => import('@/views/system/role'),
meta: {
title: '角色管理',
code: 'role:list',
isKeepAlive: true,
icon: 'el-icon-menu',
},
},
{
path: 'sys/accounts',
name: 'ResourceList',
component: () => import('@/views/system/account'),
meta: {
title: '账号管理',
code: 'account:list',
isKeepAlive: true,
icon: 'el-icon-menu',
},
},
],
},
{
path: '/machine',
name: 'Machine',
redirect: '/machine/list',
meta: {
title: '机器管理',
// 资源code用于校验用户是否拥有该资源权限
code: 'machine',
// isKeepAlive: true,
icon: 'el-icon-monitor',
},
children: [
{
path: '/list',
name: 'MachineList',
component: () => import('@/views/ops/machine'),
meta: {
title: '机器列表',
code: 'machine:list',
isKeepAlive: true,
icon: 'el-icon-menu',
},
},
],
},
{
path: '/personal',
name: 'personal',
component: () => import('@/views/personal/index.vue'),
meta: {
title: '个人中心',
isKeepAlive: true,
icon: 'el-icon-user',
},
},
{
path: '/iframes',
name: 'layoutIfameView',
component: RouterParent,
meta: {
title: 'iframe',
link: 'https://gitee.com/lyt-top/vue-next-admin',
isIframe: true,
icon: 'el-icon-menu',
},
},
],
// children: [
// {
// path: '/home',
// name: 'home',
// component: () => import('@/views/home/index.vue'),
// meta: {
// title: '首页',
// // iframe链接
// link: '',
// // 是否在菜单栏显示,默认显示
// isHide: false,
// isKeepAlive: true,
// // tag标签是否不可删除
// isAffix: true,
// // 是否为iframe
// isIframe: false,
// icon: 'el-icon-s-home',
// },
// },
// {
// path: '/sys',
// name: 'Resource',
// redirect: '/sys/resources',
// meta: {
// title: '系统管理',
// // 资源code用于校验用户是否拥有该资源权限
// code: 'sys',
// // isKeepAlive: true,
// icon: 'el-icon-monitor',
// },
// children: [
// {
// path: 'sys/resources',
// name: 'ResourceList',
// component: () => import('@/views/system/resource'),
// meta: {
// title: '资源管理',
// code: 'resource:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// {
// path: 'sys/roles',
// name: 'RoleList',
// component: () => import('@/views/system/role'),
// meta: {
// title: '角色管理',
// code: 'role:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// {
// path: 'sys/accounts',
// name: 'ResourceList',
// component: () => import('@/views/system/account'),
// meta: {
// title: '账号管理',
// code: 'account:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// ],
// },
// {
// path: '/machine',
// name: 'Machine',
// redirect: '/machine/list',
// meta: {
// title: '机器管理',
// // 资源code用于校验用户是否拥有该资源权限
// code: 'machine',
// // isKeepAlive: true,
// icon: 'el-icon-monitor',
// },
// children: [
// {
// path: '/list',
// name: 'MachineList',
// component: () => import('@/views/ops/machine'),
// meta: {
// title: '机器列表',
// code: 'machine:list',
// isKeepAlive: true,
// icon: 'el-icon-menu',
// },
// },
// ],
// },
// {
// path: '/personal',
// name: 'personal',
// component: () => import('@/views/personal/index.vue'),
// meta: {
// title: '个人中心',
// isKeepAlive: true,
// icon: 'el-icon-user',
// },
// },
// ],
},
];
@@ -135,7 +124,7 @@ export const staticRoutes: Array<RouteRecordRaw> = [
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登',
title: '登',
},
},
{
@@ -163,7 +152,6 @@ export const staticRoutes: Array<RouteRecordRaw> = [
title: '终端 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true,
icon: 'iconfont icon-caidan',
},
},
];

View File

@@ -52,6 +52,8 @@ export interface ThemeConfigState {
terminalBackground: string;
terminalCursor: string;
terminalFontSize: number;
terminalFontWeight: string;
editorTheme: string;
};
}
@@ -70,6 +72,7 @@ export interface UserInfosState {
userInfos: object;
}
// 后端返回原始路由(未处理时)
// export interface RequestOldRoutesState {
// requestOldRoutes: Array<object>;

View File

@@ -107,12 +107,16 @@ const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
layout: 'classic',
// ssh终端字体颜色
terminalForeground: '#7e9192',
terminalForeground: '#C5C8C6',
// ssh终端背景色
terminalBackground: '#002833',
terminalBackground: '#121212',
// ssh终端cursor色
terminalCursor: '#268F81',
terminalFontSize: 15,
terminalCursor: '#F0CC09',
terminalFontSize: 14,
terminalFontWeight: 'bold',
// 编辑器主题
editorTheme: 'vs',
/* 后端控制路由

View File

@@ -1,225 +1,261 @@
/* 初始化样式
------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
}
html,
body,
#app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: Microsoft YaHei, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, SimSun, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
}
/* 主布局样式
------------------------------- */
.layout-container {
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
}
/* element plus 全局样式
------------------------------- */
.layout-breadcrumb-seting {
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
}
/* nprogress 进度条跟随主题颜色
------------------------------- */
#nprogress {
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
}
/* flex 弹性布局
------------------------------- */
.flex {
display: flex;
display: flex;
}
.flex-auto {
flex: 1;
flex: 1;
}
.flex-center {
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
}
.flex-margin {
margin: auto;
margin: auto;
}
.flex-warp {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
}
/* 宽高 100%
------------------------------- */
.w100 {
width: 100% !important;
width: 100% !important;
}
.h100 {
height: 100% !important;
height: 100% !important;
}
.vh100 {
height: 100vh !important;
height: 100vh !important;
}
.max100vh {
max-height: 100vh !important;
max-height: 100vh !important;
}
.min100vh {
min-height: 100vh !important;
min-height: 100vh !important;
}
/* 颜色值
------------------------------- */
.color-primary {
color: var(--color-primary);
color: var(--color-primary);
}
.color-success {
color: var(--color-success);
color: var(--color-success);
}
.color-warning {
color: var(--color-warning);
color: var(--color-warning);
}
.color-danger {
color: var(--color-danger);
color: var(--color-danger);
}
.color-info {
color: var(--color-info);
color: var(--color-info);
}
/* 字体大小全局样式
------------------------------- */
@for $i from 10 through 32 {
.font#{$i} {
font-size: #{$i}px !important;
}
.font#{$i} {
font-size: #{$i}px !important;
}
}
/* 外边距、内边距全局样式
------------------------------- */
@for $i from 1 through 35 {
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
}
@@ -249,15 +285,22 @@ body,
.el-menu .fa:not(.is-children) {
font-size: 14px;
}
.gray-mode{
.gray-mode {
filter: grayscale(100%);
}
.fade-enter-active, .fade-leave-active {
.fade-enter-active,
.fade-leave-active {
transition: opacity .2s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
.fade-enter,
.fade-leave-to
/* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
@@ -270,7 +313,7 @@ body,
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
.toolbar {
width: 100%;
@@ -289,4 +332,32 @@ body,
.el-form-item {
margin-bottom: 3px;
}
}
.el-table-z-index-inherit .el-table .el-table__cell {
z-index: inherit !important;
}
.f12 {
font-size: 12px
}
// 图标垂直居中
.icon-middle {
.el-icon {
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
vertical-align: middle;
}
}
.img-icon {
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
vertical-align: middle;
height: 16px;
width: 16px;
}

View File

@@ -239,16 +239,6 @@
color: set-color(primary);
}
/* Switch 开关
------------------------------- */
.el-switch.is-checked .el-switch__core {
border-color: set-color(primary);
background-color: set-color(primary);
}
.el-switch__label.is-active {
color: set-color(primary);
}
/* Slider 滑块
------------------------------- */
.el-slider__bar {
@@ -957,12 +947,6 @@
.el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important;
}
.el-select-dropdown__wrap {
max-height: 274px !important; /*修复Select 选择器高度问题*/
}
.el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important; /*修复Cascader 级联选择器高度问题*/
}
/* Drawer 抽屉
------------------------------- */
@@ -993,4 +977,15 @@
}
.el-drawer-fade-leave-active .el-drawer.ltr {
animation: ltr-drawer-animation 0.3s ease !important;
}
}
// el-tooltip使用自定义主题时的样式
.el-popper.is-customized {
/* Set padding to ensure the height is 32px */
// padding: 6px 12px;
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
}
.el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0;
}

View File

@@ -2,6 +2,6 @@
@import './base.scss';
@import './other.scss';
@import './element.scss';
@import './iconSelector.scss';
@import './media/media.scss';
@import './waves.scss';
@import './iconSelector.scss';

View File

@@ -7,13 +7,14 @@
<img :src="getUserInfos.photo" />
<div class="home-card-first-right ml15">
<div class="flex-margin">
<div class="home-card-first-right-title">{{ `${currentTime}, ${getUserInfos.username}` }}</div>
<div class="home-card-first-right-title">{{ `${currentTime}, ${getUserInfos.username}`
}}</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList" :key="k">
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList as any" :key="k">
<div @click="toPage(v)" class="home-card-item home-card-item-box" :style="{ background: v.color }">
<div class="home-card-item-flex">
<div class="home-card-item-title pb3">{{ v.title }}</div>
@@ -26,7 +27,7 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
import { useStore } from '@/store/index.ts';
// import * as echarts from 'echarts';
@@ -34,103 +35,96 @@ import { CountUp } from 'countup.js';
import { formatAxis } from '@/common/utils/formatTime.ts';
import { indexApi } from './api';
import { useRouter } from 'vue-router';
export default {
name: 'HomePage',
setup() {
// const { proxy } = getCurrentInstance() as any;
const router = useRouter();
const store = useStore();
const state = reactive({
topCardItemList: [
{
title: '项目数',
id: 'projectNum',
color: '#FEBB50',
},
{
title: 'Linux机器数',
id: 'machineNum',
color: '#F95959',
},
{
title: '数据库总数',
id: 'dbNum',
color: '#8595F4',
},
{
title: 'redis总数',
id: 'redisNum',
color: '#1abc9c',
},
],
});
//
const currentTime = computed(() => {
return formatAxis(new Date());
});
const router = useRouter();
const store = useStore();
const state = reactive({
topCardItemList: [
{
title: 'Linux机器',
id: 'machineNum',
color: '#F95959',
},
{
title: '数据库',
id: 'dbNum',
color: '#8595F4',
},
{
title: 'redis',
id: 'redisNum',
color: '#1abc9c',
},
{
title: 'Mongo',
id: 'mongoNum',
color: '#FEBB50',
},
],
});
//
const initNumCountUp = async () => {
const res: any = await indexApi.getIndexCount.request();
nextTick(() => {
new CountUp('projectNum', res.projectNum).start();
new CountUp('machineNum', res.machineNum).start();
new CountUp('dbNum', res.dbNum).start();
new CountUp('redisNum', res.redisNum).start();
});
};
const {
topCardItemList,
} = toRefs(state)
const toPage = (item: any) => {
switch (item.id) {
case 'personal': {
router.push('/personal');
break;
}
case 'projectNum': {
router.push('/ops/projects');
break;
}
case 'machineNum': {
router.push('/ops/machines');
break;
}
case 'dbNum': {
router.push('/ops/dbms/dbs');
break;
}
case 'redisNum': {
router.push('/ops/redis/manage');
break;
}
}
};
//
const currentTime = computed(() => {
return formatAxis(new Date());
});
//
onMounted(() => {
initNumCountUp();
// initHomeLaboratory();
// initHomeOvertime();
});
// vuex
const getUserInfos = computed(() => {
return store.state.userInfos.userInfos;
});
return {
getUserInfos,
currentTime,
toPage,
...toRefs(state),
};
},
//
const initNumCountUp = async () => {
const res: any = await indexApi.getIndexCount.request();
nextTick(() => {
new CountUp('mongoNum', res.mongoNum).start();
new CountUp('machineNum', res.machineNum).start();
new CountUp('dbNum', res.dbNum).start();
new CountUp('redisNum', res.redisNum).start();
});
};
const toPage = (item: any) => {
switch (item.id) {
case 'personal': {
router.push('/personal');
break;
}
case 'mongoNum': {
router.push('/mongo/mongo-data-operation');
break;
}
case 'machineNum': {
router.push('/machine/machines');
break;
}
case 'dbNum': {
router.push('/dbms/sql-exec');
break;
}
case 'redisNum': {
router.push('/redis/data-operation');
break;
}
}
};
//
onMounted(() => {
initNumCountUp();
// initHomeLaboratory();
// initHomeOvertime();
});
// vuex
const getUserInfos = computed(() => {
return store.state.userInfos.userInfos;
});
</script>
<style scoped lang="scss">
.home-container {
overflow-x: hidden;
.home-card-item {
width: 100%;
height: 103px;
@@ -138,16 +132,19 @@ export default {
border-radius: 4px;
transition: all ease 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
transition: all ease 0.3s;
}
}
.home-card-item-box {
display: flex;
align-items: center;
position: relative;
overflow: hidden;
&:hover {
i {
right: 0px !important;
@@ -155,6 +152,7 @@ export default {
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
@@ -163,48 +161,59 @@ export default {
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.home-card-item-flex {
padding: 0 20px;
color: white;
.home-card-item-title,
.home-card-item-tip {
font-size: 13px;
}
.home-card-item-title-num {
font-size: 18px;
}
.home-card-item-tip-num {
font-size: 13px;
}
}
}
.home-card-first {
background: white;
border: 1px solid #ebeef5;
display: flex;
align-items: center;
img {
width: 60px;
height: 60px;
border-radius: 100%;
border: 2px solid var(--color-primary-light-5);
}
.home-card-first-right {
flex: 1;
display: flex;
flex-direction: column;
.home-card-first-right-msg {
font-size: 13px;
color: gray;
}
}
}
.home-monitor {
height: 200px;
.flex-warp-item {
width: 50%;
height: 100px;
display: flex;
.flex-warp-item-box {
margin: auto;
height: auto;
@@ -212,19 +221,24 @@ export default {
}
}
}
.home-warning-card {
height: 292px;
::v-deep(.el-card) {
height: 100%;
}
}
.home-dynamic {
height: 200px;
.home-dynamic-item {
display: flex;
width: 100%;
height: 60px;
overflow: hidden;
&:first-of-type {
.home-dynamic-item-line {
i {
@@ -232,20 +246,24 @@ export default {
}
}
}
.home-dynamic-item-left {
text-align: right;
.home-dynamic-item-left-time1 {
}
.home-dynamic-item-left-time1 {}
.home-dynamic-item-left-time2 {
font-size: 13px;
color: gray;
}
}
.home-dynamic-item-line {
height: 60px;
border-right: 2px dashed #dfdfdf;
margin: 0 20px;
position: relative;
i {
color: var(--color-primary);
font-size: 12px;
@@ -256,8 +274,10 @@ export default {
background: white;
}
}
.home-dynamic-item-right {
flex: 1;
.home-dynamic-item-right-title {
i {
margin-right: 5px;
@@ -270,6 +290,7 @@ export default {
color: var(--color-primary);
}
}
.home-dynamic-item-right-label {
font-size: 13px;
color: gray;

View File

@@ -83,7 +83,7 @@ export default defineComponent({
() => route.path,
() => {
initCurrentRouteMeta(route.meta);
proxy.$refs.layoutScrollbarRef.wrap$.scrollTop = 0;
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
}
);
return {

View File

@@ -1,7 +1,10 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img src="@/assets/image/logo.svg" class="layout-logo-medium-img" />
<span>{{ getThemeConfig.globalTitle }}</span>
<span>
{{ `${getThemeConfig.globalTitle}` }}
<sub><span style="font-size: 10px;color:goldenrod">{{ ` ${config.version}` }}</span></sub>
</span>
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img src="@/assets/image/logo.svg" class="layout-logo-size-img" />
@@ -11,6 +14,7 @@
<script lang="ts">
import { computed, getCurrentInstance } from 'vue';
import { useStore } from '@/store/index.ts';
import config from '@/common/config.ts';
export default {
name: 'layoutLogo',
setup() {
@@ -32,6 +36,7 @@ export default {
store.state.themeConfig.themeConfig.isCollapse = !store.state.themeConfig.themeConfig.isCollapse;
};
return {
config,
setShowLogo,
getThemeConfig,
onThemeConfigChange,
@@ -52,26 +57,31 @@ export default {
font-size: 16px;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&:hover {
span {
color: var(--color-primary-light-2);
}
}
&-medium-img {
width: 20px;
margin-right: 5px;
}
}
.layout-logo-size {
width: 100%;
height: 50px;
display: flex;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&-img {
width: 20px;
margin: auto;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;

View File

@@ -30,7 +30,7 @@ import { useStore } from '@/store/index.ts';
export default defineComponent({
name: 'layoutBreadcrumbSearch',
setup() {
const layoutMenuAutocompleteRef = ref();
const layoutMenuAutocompleteRef: any = ref(null);
const store = useStore();
const router = useRouter();
const state: any = reactive({
@@ -44,7 +44,9 @@ export default defineComponent({
state.isShowSearch = true;
initTageView();
nextTick(() => {
layoutMenuAutocompleteRef.value.focus();
setTimeout(() => {
layoutMenuAutocompleteRef.value.focus();
});
});
};
// 搜索弹窗关闭
@@ -68,7 +70,6 @@ export default defineComponent({
// 初始化菜单数据
const initTageView = () => {
if (state.tagsViewList.length > 0) return false;
console.log(getRoutes(store.state.routesList.routesList));
getRoutes(store.state.routesList.routesList).map((v: any) => {
if (!v.meta.isHide) {
state.tagsViewList.push({ ...v });

View File

@@ -40,7 +40,7 @@
</el-input-number>
</div>
</div>
<!-- <div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select @change="setLocalThemeConfig" v-model="getThemeConfig.terminalFontWeight" size="small" style="width: 90px">
@@ -48,7 +48,19 @@
<el-option label="bold" value="bold"> </el-option>
</el-select>
</div>
</div> -->
</div>
<el-divider content-position="left">editor 设置</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select @change="setLocalThemeConfig" v-model="getThemeConfig.editorTheme" size="small" style="width: 130px">
<el-option label="vs" value="vs"> </el-option>
<el-option label="vs-dark" value="vs-dark"> </el-option>
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
</el-select>
</div>
</div>
<!-- 全局主题 -->
<el-divider content-position="left">全局主题</el-divider>
@@ -273,23 +285,6 @@
<el-switch v-model="getThemeConfig.isInvert" @change="onAddFilterChange('invert')"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启水印</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isWartermark" @change="onWartermarkChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">水印文案</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input
v-model="getThemeConfig.wartermarkText"
size="small"
style="width: 90px"
@input="onWartermarkTextInput($event)"
></el-input>
</div>
</div>
<!-- 其它设置 -->
<el-divider content-position="left">其他设置</el-divider>
@@ -440,8 +435,6 @@ import { ElMessage } from 'element-plus';
import ClipboardJS from 'clipboard';
import { useStore } from '@/store/index.ts';
import { getLightColor } from '@/common/utils/theme.ts';
import Watermark from '@/common/utils/wartermark.ts';
import { verifyAndSpace } from '@/common/utils/toolsValidate.ts';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage.ts';
export default defineComponent({
name: 'layoutBreadcrumbSeting',
@@ -572,18 +565,6 @@ export default defineComponent({
setLocalThemeConfig();
setLocal('appFilterStyle', appEle.style.cssText);
};
// 4、界面显示 --> 开启水印
const onWartermarkChange = () => {
getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
setLocalThemeConfig();
};
// 4、界面显示 --> 水印文案
const onWartermarkTextInput = (val: string) => {
getThemeConfig.value.wartermarkText = verifyAndSpace(val);
if (getThemeConfig.value.wartermarkText === '') return false;
if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
setLocalThemeConfig();
};
// 5、布局切换
const onSetLayout = (layout: string) => {
setLocal('oldLayout', layout);
@@ -735,8 +716,6 @@ export default defineComponent({
const appEl: any = document.querySelector('#app');
appEl.style.cssText = getLocal('appFilterStyle');
}
// 开启水印
onWartermarkChange();
// // 语言国际化
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
}, 1100);
@@ -762,8 +741,6 @@ export default defineComponent({
getThemeConfig,
onDrawerClose,
onAddFilterChange,
onWartermarkChange,
onWartermarkTextInput,
onSetLayout,
setLocalThemeConfig,
onClassicSplitMenuChange,

View File

@@ -14,11 +14,11 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<!-- <div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon title="菜单搜索">
<search />
</el-icon>
</div>
</div> -->
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<el-icon title="布局设置">
<setting />
@@ -28,12 +28,12 @@
<el-popover
placement="bottom"
trigger="click"
v-model:visible="isShowUserNewsPopover"
:visible="isShowUserNewsPopover"
:width="300"
popper-class="el-popover-pupop-user-news"
>
<template #reference>
<el-badge :is-dot="true" @click="isShowUserNewsPopover = !isShowUserNewsPopover">
<el-badge :is-dot="false" @click="isShowUserNewsPopover = !isShowUserNewsPopover">
<el-icon title="消息">
<bell />
</el-icon>
@@ -55,7 +55,7 @@
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link" style="cursor: pointer">
<img :src="getUserInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ getUserInfos.username === '' ? 'test' : getUserInfos.username }}
{{ getUserInfos.name || getUserInfos.username }}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>

View File

@@ -40,7 +40,7 @@ export default {
};
// 前往通知中心点击
const toMsgCenter = () => {
// window.open('https://gitee.com/lyt-top/vue-next-admin');
};
return {
onAllReadClick,

View File

@@ -234,7 +234,7 @@ export default {
};
// 鼠标滚轮滚动
const onHandleScroll = (e: any) => {
proxy.$refs.scrollbarRef.$refs.wrap.scrollLeft += e.wheelDelta / 4;
proxy.$refs.scrollbarRef.$refs.wrapRef.scrollLeft += e.wheelDelta / 4;
};
// tagsView 横向滚动
const tagsViewmoveToCurrentTag = () => {
@@ -251,7 +251,7 @@ export default {
// 最后 li
let liLast: any = tagsRefs.value[tagsRefs.value.length - 1];
// 当前滚动条的值
let scrollRefs = proxy.$refs.scrollbarRef.$refs.wrap$;
let scrollRefs = proxy.$refs.scrollbarRef.$refs.wrapRef;
// 当前滚动条滚动宽度
let scrollS = scrollRefs.scrollWidth;
// 当前滚动条偏移宽度

View File

@@ -1,197 +1,281 @@
<template>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form" size="large">
<el-form-item prop="username">
<el-input type="text" placeholder="请输入用户名" prefix-icon="user" v-model="loginForm.username" clearable autocomplete="off">
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
placeholder="请输入密码"
prefix-icon="lock"
v-model="loginForm.password"
autocomplete="off"
show-password
>
</el-input>
</el-form-item>
<el-form-item prop="captcha">
<el-row :gutter="15">
<el-col :span="16">
<el-input
type="text"
maxlength="6"
placeholder="请输入验证码"
prefix-icon="position"
v-model="loginForm.captcha"
clearable
autocomplete="off"
@keyup.enter="login"
></el-input>
</el-col>
<el-col :span="8">
<div class="login-content-code">
<img
class="login-content-code-img"
@click="getCaptcha"
width="130px"
height="40px"
:src="captchaImage"
style="cursor: pointer"
/>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-content-submit" round @click="login" :loading="loading.signIn">
<span> </span>
</el-button>
</el-form-item>
</el-form>
<div>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form" size="large">
<el-form-item prop="username">
<el-input type="text" placeholder="请输入用户名" prefix-icon="user" v-model="loginForm.username" clearable
autocomplete="off">
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="请输入密码" prefix-icon="lock" v-model="loginForm.password"
autocomplete="off" show-password>
</el-input>
</el-form-item>
<el-form-item v-if="isUseLoginCaptcha" prop="captcha">
<el-row :gutter="15">
<el-col :span="16">
<el-input type="text" maxlength="6" placeholder="请输入验证码" prefix-icon="position"
v-model="loginForm.captcha" clearable autocomplete="off" @keyup.enter="login"></el-input>
</el-col>
<el-col :span="8">
<div class="login-content-code">
<img class="login-content-code-img" @click="getCaptcha" width="130px" height="40px"
:src="captchaImage" style="cursor: pointer" />
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-content-submit" round @click="login" :loading="loading.signIn">
<span> </span>
</el-button>
</el-form-item>
</el-form>
<el-dialog title="修改密码" v-model="changePwdDialog.visible" :close-on-click-modal="false" width="450px"
:destroy-on-close="true">
<el-form :model="changePwdDialog.form" :rules="changePwdDialog.rules" ref="changePwdFormRef"
label-width="65px">
<el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="changePwdDialog.form.username" disabled></el-input>
</el-form-item>
<el-form-item prop="oldPassword" label="旧密码" required>
<el-input v-model.trim="changePwdDialog.form.oldPassword" autocomplete="new-password"
type="password"></el-input>
</el-form-item>
<el-form-item prop="newPassword" label="新密码" required>
<el-input v-model.trim="changePwdDialog.form.newPassword" placeholder="须为8位以上且包含字⺟⼤⼩写+数字+特殊符号"
type="password" autocomplete="new-password"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelChangePwd"> </el-button>
<el-button @click="changePwd" type="primary" :loading="loading.changePwd"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { onMounted, ref, toRefs, reactive, defineComponent, computed } from 'vue';
<script lang="ts" setup>
import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initBackEndControlRoutesFun } from '@/router/index.ts';
import { useStore } from '@/store/index.ts';
import { setSession } from '@/common/utils/storage.ts';
import { setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage.ts';
import { formatAxis } from '@/common/utils/formatTime.ts';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { useLoginCaptcha, useWartermark } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
export default defineComponent({
name: 'AccountLogin',
setup() {
const store = useStore();
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
const state = reactive({
captchaImage: '',
loginForm: {
username: '',
password: '',
captcha: '',
cid: '',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
},
loading: {
signIn: false,
},
});
onMounted(() => {
getCaptcha();
});
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
}
const getCaptcha = async () => {
let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid;
};
const store = useStore();
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
const changePwdFormRef: any = ref(null);
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 校验登录表单并登录
const login = () => {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
onSignIn();
} else {
return false;
}
});
};
// 登录
const onSignIn = async () => {
state.loading.signIn = true;
let loginRes;
try {
loginRes = await openApi.login(state.loginForm);
// // 存储 token 到浏览器缓存
setSession('token', loginRes.token);
setSession('menus', loginRes.menus);
} catch (e) {
state.loading.signIn = false;
state.loginForm.captcha = '';
getCaptcha();
return;
}
// 用户信息
const userInfos = {
username: state.loginForm.username,
// 头像
photo: letterAvatar(state.loginForm.username),
time: new Date().getTime(),
// // 菜单资源code数组
// menus: loginRes.menus,
permissions: loginRes.permissions,
lastLoginTime: loginRes.lastLoginTime,
lastLoginIp: loginRes.lastLoginIp,
};
// 存储用户信息到浏览器缓存
setSession('userInfo', userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
store.dispatch('userInfos/setUserInfos', userInfos);
if (!store.state.themeConfig.themeConfig.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
// await initAllFun();
await initBackEndControlRoutesFun();
signInSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
await initBackEndControlRoutesFun();
// 执行完 initBackEndControlRoutesFun再执行 signInSuccess
signInSuccess();
}
};
// 登录成功后的跳转
const signInSuccess = () => {
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
route.query?.redirect ? router.push(route.query.redirect as string) : router.push('/');
// 登录成功提示
setTimeout(() => {
// 关闭 loading
state.loading.signIn = true;
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
}, 300);
};
return {
getCaptcha,
currentTime,
loginFormRef,
login,
...toRefs(state),
};
const state = reactive({
isUseLoginCaptcha: false,
captchaImage: '',
loginForm: {
username: '',
password: '',
captcha: '',
cid: '',
},
changePwdDialog: {
visible: false,
form: {
username: '',
oldPassword: '',
newPassword: '',
},
rules: {
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[`~!@#$%^&*()_+<>?:"{},.\/\\;'[\]])[A-Za-z\d`~!@#$%^&*()_+<>?:"{},.\/\\;'[\]]{8,}$/,
message: '须为8位以上且包含字⺟⼤⼩写+数字+特殊符号',
trigger: 'blur',
},
],
},
},
loading: {
signIn: false,
changePwd: false,
},
});
const {
isUseLoginCaptcha,
captchaImage,
loginForm,
changePwdDialog,
loading,
} = toRefs(state)
onMounted(async () => {
nextTick(async () => {
state.isUseLoginCaptcha = await useLoginCaptcha();
getCaptcha();
});
// 移除公钥, 方便后续重新获取
sessionStorage.removeItem('RsaPublicKey');
});
const getCaptcha = async () => {
if (!state.isUseLoginCaptcha) {
return;
}
let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid;
};
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 校验登录表单并登录
const login = () => {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
onSignIn();
} else {
return false;
}
});
};
// 登录
const onSignIn = async () => {
state.loading.signIn = true;
let loginRes;
const originPwd = state.loginForm.password;
try {
const loginReq = { ...state.loginForm };
loginReq.password = await RsaEncrypt(originPwd);
loginRes = await openApi.login(loginReq);
// 存储 token 到浏览器缓存
setSession('token', loginRes.token);
setSession('menus', loginRes.menus);
} catch (e: any) {
state.loading.signIn = false;
state.loginForm.captcha = '';
// 密码强度不足
if (e.code && e.code == 401) {
state.changePwdDialog.form.username = state.loginForm.username;
state.changePwdDialog.form.oldPassword = originPwd;
state.changePwdDialog.form.newPassword = '';
state.changePwdDialog.visible = true;
} else {
getCaptcha();
}
return;
}
// 用户信息
const userInfos = {
name: loginRes.name,
username: state.loginForm.username,
// 头像
photo: letterAvatar(state.loginForm.username),
time: new Date().getTime(),
// // 菜单资源code数组
// menus: loginRes.menus,
permissions: loginRes.permissions,
lastLoginTime: loginRes.lastLoginTime,
lastLoginIp: loginRes.lastLoginIp,
};
// 存储用户信息到浏览器缓存
setUserInfo2Session(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
store.dispatch('userInfos/setUserInfos', userInfos);
if (!store.state.themeConfig.themeConfig.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
// await initAllFun();
await initBackEndControlRoutesFun();
signInSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
await initBackEndControlRoutesFun();
// 执行完 initBackEndControlRoutesFun再执行 signInSuccess
signInSuccess();
}
};
// 登录成功后的跳转
const signInSuccess = () => {
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
route.query?.redirect ? router.push(route.query.redirect as string) : router.push('/');
// 登录成功提示
setTimeout(async () => {
// 关闭 loading
state.loading.signIn = true;
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
if (await useWartermark()) {
setUseWatermark2Session(true);
}
}, 300);
};
const changePwd = () => {
changePwdFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.changePwd = true;
const form = state.changePwdDialog.form;
const changePwdReq: any = { ...form };
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd(changePwdReq);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false;
getCaptcha();
} finally {
state.loading.changePwd = false;
}
});
};
const cancelChangePwd = () => {
state.changePwdDialog.visible = false;
state.changePwdDialog.form.newPassword = '';
state.changePwdDialog.form.oldPassword = '';
state.changePwdDialog.form.username = '';
getCaptcha();
};
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
.login-content-code {
display: flex;
align-items: center;
justify-content: space-around;
.login-content-code-img {
width: 100%;
height: 40px;
@@ -208,12 +292,14 @@ export default defineComponent({
transition: all ease 0.2s;
border-radius: 4px;
user-select: none;
&:hover {
border-color: #c0c4cc;
transition: all ease 0.2s;
}
}
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;

View File

@@ -24,40 +24,37 @@
</div> -->
</div>
</div>
<div class="login-copyright">
<!-- <div class="login-copyright">
<div class="mb5 login-copyright-company">mayfly</div>
<div class="login-copyright-msg">mayfly</div>
</div>
</div> -->
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { toRefs, reactive, computed } from 'vue';
import Account from '@/views/login/component/AccountLogin.vue';
import { useStore } from '@/store/index.ts';
export default {
name: 'LoginPage',
components: { Account },
setup() {
const store = useStore();
const state = reactive({
tabsActiveName: 'account',
isTabPaneShow: true,
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig.themeConfig;
});
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
};
return {
onTabsClick,
getThemeConfig,
...toRefs(state),
};
},
const store = useStore();
const state = reactive({
tabsActiveName: 'account',
isTabPaneShow: true,
});
const {
isTabPaneShow,
tabsActiveName,
} = toRefs(state)
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig.themeConfig;
});
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
};
</script>
@@ -67,6 +64,7 @@ export default {
height: 100%;
background: url('@/assets/image/bg-login.png') no-repeat;
background-size: 100% 100%;
.login-logo {
position: absolute;
top: 30px;
@@ -80,6 +78,7 @@ export default {
width: 90%;
transform: translateX(-50%);
}
.login-content {
width: 500px;
padding: 20px;
@@ -94,9 +93,11 @@ export default {
height: 480px;
overflow: hidden;
z-index: 1;
.login-content-main {
margin: 0 auto;
width: 80%;
.login-content-title {
color: #333;
font-weight: 500;
@@ -108,9 +109,11 @@ export default {
}
}
}
.login-content-mobile {
height: 418px;
}
.login-copyright {
position: absolute;
left: 50%;
@@ -120,9 +123,11 @@ export default {
color: white;
font-size: 12px;
opacity: 0.8;
.login-copyright-company {
white-space: nowrap;
}
.login-copyright-msg {
@extend .login-copyright-company;
}

View File

@@ -1,80 +0,0 @@
<template>
<div>
<el-form class="search-form" label-position="right" :inline="true">
<el-form-item prop="project" label="项目" label-width="40px">
<el-select v-model="projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="env" label="env" label-width="33px">
<el-select style="width: 80px" v-model="envId" placeholder="环境" @change="changeEnv" filterable>
<el-option v-for="item in envs" :key="item.id" :label="item.name" :value="item.id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.remark }}</span>
</el-option>
</el-select>
</el-form-item>
<slot></slot>
</el-form>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, onMounted } from 'vue';
import { projectApi } from '../project/api';
export default defineComponent({
name: 'ProjectEnvSelect',
props: {
visible: {
type: Boolean,
},
data: {
type: Object,
},
title: {
type: String,
},
machineId: {
type: Number,
},
isCommon: {
type: Boolean,
},
},
setup(props: any, { emit }) {
const state = reactive({
projects: [] as any,
envs: [] as any,
projectId: null,
envId: null,
});
onMounted(async () => {
state.projects = await projectApi.accountProjects.request(null);
});
const changeProject = async (projectId: any) => {
emit('update:projectId', projectId);
emit('changeProjectEnv', state.projectId, null);
state.envId = null;
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeEnv = (envId: any) => {
emit('update:envId', envId);
emit('changeProjectEnv', state.projectId, envId);
};
return {
...toRefs(state),
changeProject,
changeEnv,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div style="width: 100%">
<el-select @focus="getSshTunnelMachines" @change="change" style="width: 100%" v-model="sshTunnelMachineId"
@clear="clear" placeholder="请选择SSH隧道机器" clearable>
<el-option v-for="item in sshTunnelMachineList" :key="item.id" :label="`${item.ip}:${item.port} [${item.name}]`"
:value="item.id">
</el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { machineApi } from '../machine/api';
const props = defineProps({
modelValue: {
type: Number,
},
})
//定义事件
const emit = defineEmits(['update:modelValue'])
const state = reactive({
// 单选则为id多选为id数组
sshTunnelMachineId: null as any,
sshTunnelMachineList: [] as any,
});
const {
sshTunnelMachineId,
sshTunnelMachineList,
} = toRefs(state)
onMounted(async () => {
if (!props.modelValue || props.modelValue <= 0) {
state.sshTunnelMachineId = null;
} else {
state.sshTunnelMachineId = props.modelValue;
}
await getSshTunnelMachines();
});
const getSshTunnelMachines = async () => {
if (state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const clear = () => {
state.sshTunnelMachineId = null;
change();
}
const change = () => {
emit('update:modelValue', state.sshTunnelMachineId);
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,65 @@
<template>
<div
style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer;vertical-align: middle;">
<el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
<span v-for="(v, i) in tags" :key="i">
<el-tooltip effect="customized" :content="v.remark" placement="top">
<span class="color-success">{{ v.name }}</span>
</el-tooltip>
<span v-if="i != state.tags.length - 1" class="color-primary"> / </span>
</span>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onMounted } from 'vue';
import { tagApi } from '../tag/api';
const props = defineProps({
tagPath: {
type: [String],
required: true,
},
})
const state = reactive({
tagPath: '',
tags: [] as any,
})
const {
tags,
} = toRefs(state)
onMounted(async () => {
state.tagPath = props.tagPath;
})
const showTagInfo = async () => {
if (state.tags && state.tags.length > 0) {
return;
}
const tagStrs = state.tagPath.split('/');
const tagPaths = [];
let nowTag = '';
for (let tagStr of tagStrs) {
if (nowTag) {
nowTag = `${nowTag}/${tagStr}`
} else {
nowTag = tagStr
}
tagPaths.push(nowTag)
}
state.tags = await tagApi.listByQuery.request({ tagPaths: tagPaths.join(',') })
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div>
<el-tree-select @check="changeTag" style="width: 100%" v-model="selectTags" :data="tags" placeholder="请选择关联标签"
:render-after-expand="true" :default-expanded-keys="[selectTags]" show-checkbox check-strictly node-key="id"
:props="{
value: 'id',
label: 'codePath',
children: 'children',
}">
<template #default="{ data }">
<span class="custom-tree-node">
<span style="font-size: 13px">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
</span>
</span>
</template>
</el-tree-select>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api';
const props = defineProps({
tagId: {
type: Number,
},
tagPath: {
type: String,
},
})
//定义事件
const emit = defineEmits(['changeTag', 'update:tagId', 'update:tagPath'])
const state = reactive({
tags: [],
// 单选则为id多选为id数组
selectTags: null as any,
});
const {
tags,
selectTags,
} = toRefs(state)
onMounted(async () => {
if (props.tagId) {
state.selectTags = props.tagId;
}
state.tags = await tagApi.getTagTrees.request(null);
});
const changeTag = (tag: any, checkInfo: any) => {
if (checkInfo.checkedNodes.length > 0) {
emit('update:tagId', tag.id);
emit('update:tagPath', tag.codePath);
emit('changeTag', tag);
} else {
emit('update:tagId', null);
emit('update:tagPath', null);
}
};
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="instances-box layout-aside">
<el-row type="flex" justify="space-between">
<el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" />
<el-tree ref="treeRef" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
:highlight-current="true" :indent="7" :load="loadNode" :props="treeProps" lazy node-key="key"
:expand-on-click-node="true" :filter-node-method="filterNode" @node-click="treeNodeClick" @node-expand="treeNodeClick">
<template #default="{ node, data }">
<span class="icon-middle ">
<span v-if="data.type == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" />
</span>
<slot v-else :node="node" :data="data" name="prefix"></slot>
<span class="ml3">
<slot name="label" :data="data"> {{ data.label }}</slot>
</span>
</span>
</template>
</el-tree>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
import { TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
const props = defineProps({
height: {
type: [Number, String],
default: 0
},
load: {
type: Function,
required: true,
}
})
const treeProps = {
label: 'name',
children: 'zones',
isLeaf: 'isLeaf',
}
const emit = defineEmits(['nodeClick'])
const treeRef: any = ref(null)
const state = reactive({
height: 600 as any,
filterText: '',
opend: {},
})
const { filterText } = toRefs(state)
onMounted(async () => {
if (!props.height) {
state.height = window.innerHeight - 145 + 'px';
} else {
state.height = props.height;
}
})
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
/**
* 加载树节点
* @param { Object } node
* @param { Object } resolve
*/
const loadNode = async (node: any, resolve: any) => {
if (typeof resolve !== 'function') {
return;
}
return resolve(await props.load(node));
};
const treeNodeClick = (data: any) => {
emit('nodeClick', data);
}
const reloadNode = (nodeKey: any) => {
let node = getNode(nodeKey);
node.loaded = false;
node.expand();
}
const getNode = (nodeKey: any) => {
let node = treeRef.value.getNode(nodeKey);
if (!node) {
throw new Error('未找到节点: ' + nodeKey);
}
return node;
}
defineExpose({
reloadNode,
})
</script>
<style lang="scss">
.instances-box {
overflow: 'auto';
.el-tree {
display: inline-block;
min-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,38 @@
export class TagTreeNode {
/**
* 节点id
*/
key: any
/**
* 节点名称
*/
label: string
/**
* 树节点类型
*/
type: any
isLeaf: boolean = false;
params: any;
static TagPath = -1;
constructor(key: any, label: string, type?: any) {
this.key = key;
this.label = label;
this.type = type || TagTreeNode.TagPath;
}
withIsLeaf(isLeaf: boolean) {
this.isLeaf = isLeaf;
return this;
}
withParams(params: any) {
this.params = params;
return this;
}
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-dialog title="创建表" v-model="dialogVisible" :before-close="cancel" width="90%">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%">
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
<el-row>
<el-col :span="12">
@@ -13,10 +13,21 @@
<el-input style="width: 80%" v-model="tableData.tableComment" size="small"></el-input>
</el-form-item>
</el-col>
<el-col style="margin-top: 20px" :span="12">
<el-form-item prop="characterSet" label="字符集">
<el-col :span="12">
<el-form-item prop="characterSet" label="charset">
<el-select filterable style="width: 80%" v-model="tableData.characterSet" size="small">
<el-option v-for="item in characterSetNameList" :key="item" :label="item" :value="item"> </el-option>
<el-option v-for="item in characterSetNameList" :key="item" :label="item" :value="item">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="characterSet" label="collation">
<el-select filterable style="width: 80%" v-model="tableData.collation" size="small">
<el-option v-for="item in collationNameList" :key="item"
:label="tableData.characterSet + '_' + item"
:value="tableData.characterSet + '_' + item">
</el-option>
</el-select>
</el-form-item>
</el-col>
@@ -24,35 +35,88 @@
<el-tabs v-model="activeName">
<el-tab-pane label="字段" name="1">
<el-table :data="tableData.fields.res">
<el-table-column :prop="item.prop" :label="item.label" v-for="item in tableData.fields.colNames" :key="item.prop">
<el-table :data="tableData.fields.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label"
v-for="item in tableData.fields.colNames" :key="item.prop">
<template #default="scope">
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"></el-input>
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name">
</el-input>
<el-select v-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-option v-for="typeValue in typeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
<el-select v-if="item.prop === 'type'" filterable size="small"
v-model="scope.row.type">
<el-option v-for="typeValue in columnTypeList" :key="typeValue"
:value="typeValue">{{ typeValue }}</el-option>
</el-select>
<el-input v-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
<el-input v-if="item.prop === 'value'" size="small" v-model="scope.row.value">
</el-input>
<el-input v-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
<el-input v-if="item.prop === 'length'" size="small" v-model="scope.row.length">
</el-input>
<el-checkbox v-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
<el-checkbox v-if="item.prop === 'notNull'" size="small"
v-model="scope.row.notNull"> </el-checkbox>
<el-checkbox v-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
<el-checkbox v-if="item.prop === 'pri'" size="small" v-model="scope.row.pri">
</el-checkbox>
<el-checkbox v-if="item.prop === 'auto_increment'" size="small" v-model="scope.row.auto_increment"> </el-checkbox>
<el-checkbox v-if="item.prop === 'auto_increment'" size="small"
v-model="scope.row.auto_increment"> </el-checkbox>
<el-input v-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
<el-input v-if="item.prop === 'remark'" size="small" v-model="scope.row.remark">
</el-input>
<el-button v-if="item.prop === 'action'" type="text" size="small" @click.prevent="deleteRow(scope.$index)">删除</el-button>
<el-link v-if="item.prop === 'action'" type="danger" plain size="small"
:underline="false" @click.prevent="deleteRow(scope.$index)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px">
<el-button @click="addRow()" type="text" icon="plus"></el-button>
<el-button @click="addDefaultRows()" link type="warning" icon="plus">添加默认列</el-button>
<el-button @click="addRow()" link type="primary" icon="plus">添加列</el-button>
</el-row>
</el-tab-pane>
<el-tab-pane label="索引" name="2">
<el-table :data="tableData.indexs.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label"
v-for="item in tableData.indexs.colNames" :key="item.prop">
<template #default="scope">
<el-input v-if="item.prop === 'indexName'" size="small"
v-model="scope.row.indexName"></el-input>
<el-select v-if="item.prop === 'columnNames'" v-model="scope.row.columnNames"
multiple collapse-tags collapse-tags-tooltip filterable placeholder="请选择字段"
style="width: 100%">
<el-option v-for="cl in tableData.indexs.columns" :key="cl.name"
:label="cl.name" :value="cl.name">
{{ cl.name + ' - ' + (cl.remark || '') }}
</el-option>
</el-select>
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique">
</el-checkbox>
<el-select v-if="item.prop === 'indexType'" filterable size="small"
v-model="scope.row.indexType">
<el-option v-for="typeValue in indexTypeList" :key="typeValue"
:value="typeValue">{{ typeValue }}</el-option>
</el-select>
<el-input v-if="item.prop === 'indexComment'" size="small"
v-model="scope.row.indexComment"> </el-input>
<el-link v-if="item.prop === 'action'" type="danger" plain size="small"
:underline="false" @click.prevent="deleteIndex(scope.$index)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px">
<el-button @click="addIndex()" link type="primary" icon="plus">添加索引</el-button>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
@@ -63,159 +127,84 @@
</template>
<script lang="ts">
import { watch, toRefs, reactive, defineComponent, ref, getCurrentInstance } from 'vue';
import { TYPE_LIST, CHARACTER_SET_NAME_LIST } from './service.ts';
<script lang="ts" setup>
import { watch, toRefs, reactive, ref } from 'vue';
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service.ts';
import { ElMessage } from 'element-plus';
import SqlExecBox from './component/SqlExecBox.ts';
export default defineComponent({
name: 'createTable',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
data: {
type: Object,
},
dbId: {
type: Number,
},
db: {
type: String,
}
const props = defineProps({
visible: {
type: Boolean,
},
setup(props: any, { emit }) {
const formRef: any = ref();
const { proxy } = getCurrentInstance() as any;
const state = reactive({
dialogVisible: false,
btnloading: false,
activeName: '1',
typeList: TYPE_LIST,
characterSetNameList: CHARACTER_SET_NAME_LIST,
tableData: {
fields: {
colNames: [
{
prop: 'name',
label: '字段名称',
},
{
prop: 'type',
label: '字段类型',
},
{
prop: 'length',
label: '长度',
},
{
prop: 'value',
label: '默认值',
},
title: {
type: String,
},
data: {
type: Object,
},
dbId: {
type: Number,
},
db: {
type: String,
}
})
{
prop: 'notNull',
label: '非空',
},
{
prop: 'pri',
label: '主键',
},
{
prop: 'auto_increment',
label: '自增',
},
{
prop: 'remark',
label: '备注',
},
{
prop: 'action',
label: '操作',
},
],
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql'])
res: [
{
name: '',
type: '',
value: '',
length: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
},
],
const formRef: any = ref();
const state = reactive({
dialogVisible: false,
btnloading: false,
activeName: '1',
columnTypeList: TYPE_LIST,
indexTypeList: ['BTREE'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
characterSetNameList: CHARACTER_SET_NAME_LIST,
collationNameList: COLLATION_SUFFIX_LIST,
tableData: {
fields: {
colNames: [
{
prop: 'name',
label: '字段名称',
},
characterSet: 'utf8mb4',
tableName: '',
tableComment: '',
},
});
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
});
const cancel = () => {
emit('update:visible', false);
reset();
};
const addRow = () => {
state.tableData.fields.res.push({
name: '',
type: '',
value: '',
length: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
});
};
const deleteRow = (index: any) => {
state.tableData.fields.res.splice(index, 1);
};
const submit = async () => {
let data = state.tableData;
let primary_key = '';
let fields: string[] = [];
data.fields.res.forEach((item) => {
fields.push(
`${item.name} ${item.type}${+item.length > 0 ? `(${item.length})` : ''} ${item.notNull ? 'NOT NULL' : ''} ${
item.auto_increment ? 'AUTO_INCREMENT' : ''
} ${item.value ? 'DEFAULT ' + item.value : item.notNull ? '' : 'DEFAULT NULL'} ${
item.remark ? `COMMENT '${item.remark}'` : ''
} \n`
);
if (item.pri) {
primary_key += `${item.name},`;
}
});
let sql = `
CREATE TABLE ${data.tableName} (
${fields.join(',')}
${primary_key ? `, PRIMARY KEY (${primary_key.slice(0, -1)})` : ''}
) ENGINE=InnoDB DEFAULT CHARSET=${data.characterSet} COLLATE=utf8mb4_bin COMMENT='${data.tableComment}';`;
SqlExecBox({
sql: sql,
dbId: props.dbId as any,
db: props.db,
runSuccessCallback: () => {
ElMessage.success('创建成功');
proxy.$parent.tableInfo({ id: props.dbId });
cancel();
{
prop: 'type',
label: '字段类型',
},
});
};
const reset = () => {
formRef.value.resetFields();
state.tableData.fields.res = [
{
prop: 'length',
label: '长度',
},
{
prop: 'value',
label: '默认值',
},
{
prop: 'notNull',
label: '非空',
},
{
prop: 'pri',
label: '主键',
},
{
prop: 'auto_increment',
label: '自增',
},
{
prop: 'remark',
label: '备注',
},
{
prop: 'action',
label: '操作',
},
],
res: [
{
name: '',
type: '',
@@ -226,18 +215,385 @@ export default defineComponent({
auto_increment: false,
remark: '',
},
];
};
return {
...toRefs(state),
formRef,
cancel,
reset,
addRow,
deleteRow,
submit,
};
],
},
indexs: {
colNames: [
{
prop: 'indexName',
label: '索引名',
},
{
prop: 'columnNames',
label: '列名',
},
{
prop: 'unique',
label: '唯一',
},
{
prop: 'indexType',
label: '类型',
},
{
prop: 'indexComment',
label: '备注',
},
{
prop: 'action',
label: '操作',
},
],
columns: [{ name: '', remark: '' }],
res: [
{
indexName: '',
columnNames: [],
unique: false,
indexType: 'BTREE',
indexComment: '',
},
],
},
characterSet: 'utf8mb4',
collation: 'utf8mb4_general_ci',
tableName: '',
tableComment: '',
height: 550
},
});
const {
dialogVisible,
btnloading,
activeName,
columnTypeList,
indexTypeList,
characterSetNameList,
collationNameList,
tableData,
} = toRefs(state)
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
});
const cancel = () => {
emit('update:visible', false);
reset();
};
const addRow = () => {
state.tableData.fields.res.push({
name: '',
type: '',
value: '',
length: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
});
};
const addIndex = () => {
state.tableData.indexs.res.push({
indexName: '',
columnNames: [],
unique: false,
indexType: 'BTREE',
indexComment: '',
});
};
const addDefaultRows = () => {
state.tableData.fields.res.push(
{ name: 'id', type: 'bigint', length: '20', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
{ name: 'creator_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
{ name: 'creator', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人姓名' },
{ name: 'creat_time', type: 'datetime', length: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建时间' },
{ name: 'updater_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
{ name: 'updater', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人姓名' },
{ name: 'update_time', type: 'datetime', length: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改时间' },
);
};
const deleteRow = (index: any) => {
state.tableData.fields.res.splice(index, 1);
};
const deleteIndex = (index: any) => {
state.tableData.indexs.res.splice(index, 1);
};
const submit = async () => {
let sql = genSql();
if (!sql) {
ElMessage.warning('没有更改');
return;
}
SqlExecBox({
sql: sql,
dbId: props.dbId as any,
db: props.db,
runSuccessCallback: () => {
emit('submit-sql', {tableName: state.tableData.tableName });
// cancel();
},
});
};
/**
* 对比两个数组,取出被修改过的对象数组
* @param oldArr 原对象数组
* @param nowArr 修改后的对象数组
* @param key 标志对象唯一属性
*/
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[], add: any[], upd: any[] } => {
let data = {
del: [] as object[], // 删除的数据
add: [] as object[], // 新增的数据
upd: [] as object[] // 修改的数据
}
// 旧数据为空
if (oldArr && Array.isArray(oldArr) && oldArr.length === 0
&& nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
data.add = nowArr;
return data;
}
// 新数据为空
if (nowArr && Array.isArray(nowArr) && nowArr.length === 0
&& oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
data.del = oldArr;
return data;
}
let oldMap = {}, newMap = {};
oldArr.forEach(a => oldMap[a[key]] = a)
nowArr.forEach(a => {
let k = a[key]
newMap[k] = a;
if (!oldMap.hasOwnProperty(k)) {// 新增
data.add.push(a)
}
})
oldArr.forEach(a => {
let k = a[key];
let newData = newMap[k];
if (!newData) { // 删除
data.del.push(a)
} else { // 判断每个字段是否相等,否则为修改
for (let f in a) {
let oldV = a[f]
let newV = newData[f]
if (oldV.toString() !== newV.toString()) {
data.upd.push(newData)
break;
}
}
}
})
return data;
}
const genSql = () => {
const genColumnBasicSql = (cl: any) => {
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : '\'' + cl.value + '\'') : '';
let defVal = `${val ? ('DEFAULT ' + val) : ''}`;
let length = cl.length ? `(${cl.length})` : '';
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${cl.auto_increment ? 'AUTO_INCREMENT' : ''} ${defVal} comment '${cl.remark || ''}' `
}
let data = state.tableData;
// 创建表
if (!props.data?.edit) {
if (state.activeName === '1') {// 创建表结构
let primary_key = '';
let fields: string[] = [];
data.fields.res.forEach((item) => {
fields.push(genColumnBasicSql(item));
if (item.pri) {
primary_key += `${item.name},`;
}
});
return `CREATE TABLE ${data.tableName}
( ${fields.join(',')}
${primary_key ? `, PRIMARY KEY (${primary_key.slice(0, -1)})` : ''}
) ENGINE=InnoDB DEFAULT CHARSET=${data.characterSet} COLLATE =${data.collation} COMMENT='${data.tableComment}';`;
} else if (state.activeName === '2' && data.indexs.res.length > 0) { // 创建索引
let sql = `ALTER TABLE ${data.tableName}`;
state.tableData.indexs.res.forEach(a => {
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
})
return sql.substring(0, sql.length - 1) + ';'
}
} else { // 修改
let addSql = '', updSql = '', delSql = '';
if (state.activeName === '1') {// 修改列
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name')
if (changeData.add.length > 0) {
addSql = `ALTER TABLE ${data.tableName}`
changeData.add.forEach(a => {
addSql += ` ADD ${genColumnBasicSql(a)},`
})
addSql = addSql.substring(0, addSql.length - 1)
addSql += ';'
}
if (changeData.upd.length > 0) {
updSql = `ALTER TABLE ${data.tableName}`;
changeData.upd.forEach(a => {
updSql += ` MODIFY ${genColumnBasicSql(a)},`
})
updSql = updSql.substring(0, updSql.length - 1)
updSql += ';'
}
if (changeData.del.length > 0) {
changeData.del.forEach(a => {
delSql += ` ALTER TABLE ${data.tableName} DROP COLUMN ${a.name}; `
})
}
return addSql + updSql + delSql;
} else if (state.activeName === '2') { // 修改索引
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName')
// 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1`
// DROP INDEX `test1_name_uindex`,
// DROP INDEX `test1_column_name4_index`,
// ADD UNIQUE INDEX `test1_name_uindex`(`id`) USING BTREE COMMENT 'ASDASD',
// ADD INDEX `111`(`column_name4`) USING BTREE COMMENT 'zasf';
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];
if (changeData.upd.length > 0) {
changeData.upd.forEach(a => {
dropIndexNames.push(a.indexName)
addIndexs.push(a)
})
}
if (changeData.del.length > 0) {
changeData.del.forEach(a => {
dropIndexNames.push(a.indexName)
})
}
if (changeData.add.length > 0) {
changeData.add.forEach(a => {
addIndexs.push(a)
})
}
if (dropIndexNames.length > 0 || addIndexs.length > 0) {
let sql = `ALTER TABLE ${data.tableName} `;
if (dropIndexNames.length > 0) {
dropIndexNames.forEach(a => {
sql += `DROP INDEX ${a},`
})
sql = sql.substring(0, sql.length - 1)
}
if (addIndexs.length > 0) {
if (dropIndexNames.length > 0){
sql += ','
}
addIndexs.forEach(a => {
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
})
sql = sql.substring(0, sql.length - 1)
}
return sql;
}
}
}
};
const reset = () => {
state.activeName = '1'
formRef.value.resetFields()
state.tableData.tableName = ''
state.tableData.tableComment = ''
state.tableData.fields.res = [
{
name: '',
type: '',
value: '',
length: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
},
];
state.tableData.indexs.res = [{
indexName: '',
columnNames: [],
unique: false,
indexType: 'BTREE',
indexComment: '',
},]
};
const oldData = { indexs: [] as any[], fields: [] as any[] }
watch(() => props.data, (newValue: any) => {
const { row, indexs, columns } = newValue;
// 回显表名表注释
state.tableData.tableName = row.tableName
state.tableData.tableComment = row.tableComment
// 回显列
if (columns && Array.isArray(columns) && columns.length > 0) {
oldData.fields = [];
state.tableData.fields.res = [];
// 索引列下拉选
state.tableData.indexs.columns = [];
columns.forEach(a => {
let typeObj = a.columnType.replace(')', '').split('(')
let type = typeObj[0];
let length = typeObj.length > 1 && typeObj[1] || '';
let data = {
name: a.columnName,
type,
value: a.columnDefault || '',
length,
notNull: a.nullable !== 'YES',
pri: a.columnKey === 'PRI',
auto_increment: a.extra?.indexOf('auto_increment') > -1,
remark: a.columnComment,
};
state.tableData.fields.res.push(data)
oldData.fields.push(JSON.parse(JSON.stringify(data)))
// 索引字段下拉选项
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment })
})
}
// 回显索引
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
oldData.indexs = [];
state.tableData.indexs.res = [];
// 索引过滤掉主键
indexs.filter(a => a.indexName !== "PRIMARY").forEach(a => {
let data = {
indexName: a.indexName,
columnNames: a.columnName?.split(','),
unique: a.nonUnique === 0 || false,
indexType: a.indexType,
indexComment: a.indexComment,
}
state.tableData.indexs.res.push(data)
oldData.indexs.push(JSON.parse(JSON.stringify(data)))
})
}
})
</script>

View File

@@ -1,68 +1,87 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="35%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="85px">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false"
:destroy-on-close="true" width="38%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="95px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.trim="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码,修改操作可不填"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-tag
v-for="db in databaseList"
:key="db"
class="ml5 mt5"
type="success"
effect="plain"
closable
:disable-transitions="false"
@close="handleClose(db)"
>
{{ db }}
</el-tag>
<el-input
v-if="inputDbVisible"
ref="InputDbRef"
v-model="inputDbValue"
style="width: 120px; margin-left: 5px; margin-top: 5px"
size="small"
@keyup.enter="handleInputDbConfirm"
@blur="handleInputDbConfirm"
/>
<el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
<el-option key="item.id" label="postgres" value="postgres"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip"
auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码,修改操作可不填"
autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码
</el-link>
</template>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-col :span="19">
<el-select @change="changeDatabase" v-model="databaseList" multiple clearable collapse-tags
collapse-tags-tooltip filterable allow-create placeholder="请确保数据库实例信息填写完整后获取库名"
style="width: 100%">
<el-option v-for="db in allDatabases" :key="db" :label="db" :value="db" />
</el-select>
</el-col>
<el-col style="text-align: center" :span="1">
<el-divider direction="vertical" border-style="dashed" />
</el-col>
<el-col :span="4">
<el-link @click="getAllDatabase" :underline="false" type="success">获取库名</el-link>
</el-col>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="params" label="连接参数:">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<template #suffix>
<el-link target="_blank" href="https://github.com/go-sql-driver/mysql#parameters"
:underline="false" type="primary" class="mr5">参数参考</el-link>
</template>
</el-input>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
@@ -75,243 +94,185 @@
</div>
</template>
<script lang="ts">
import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue';
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
import type { ElInput } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
export default defineComponent({
name: 'DbEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
db: {
type: [Boolean, Object],
},
title: {
type: String,
},
const props = defineProps({
visible: {
type: Boolean,
},
setup(props: any, { emit }) {
const dbForm: any = ref(null);
const InputDbRef = ref<InstanceType<typeof ElInput>>();
const state = reactive({
dialogVisible: false,
projects: [],
envs: [],
databaseList: [] as any,
inputDbVisible: false,
inputDbValue: '',
form: {
id: null,
name: null,
port: 3306,
username: null,
password: null,
database: '',
project: null,
projectId: null,
envId: null,
env: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
type: [
{
required: true,
message: '请选择数据库类型',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
// password: [
// {
// required: true,
// message: '请输入密码',
// trigger: ['change', 'blur'],
// },
// ],
database: [
{
required: true,
message: '请添加数据库',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, (newValue) => {
state.projects = newValue.projects;
if (newValue.db) {
getEnvs(newValue.db.projectId);
state.form = { ...newValue.db };
// 将数据库名使用空格切割,获取所有数据库列表
state.databaseList = newValue.db.database.split(' ');
} else {
state.envs = [];
state.form = { port: 3306 } as any;
state.databaseList = [];
}
state.dialogVisible = newValue.visible;
});
const handleClose = (db: string) => {
state.databaseList.splice(state.databaseList.indexOf(db), 1);
changeDatabase();
};
const showInputDb = () => {
state.inputDbVisible = true;
nextTick(() => {
InputDbRef.value!.input!.focus();
});
};
const handleInputDbConfirm = () => {
if (state.inputDbValue) {
state.databaseList.push(state.inputDbValue);
changeDatabase();
}
state.inputDbVisible = false;
state.inputDbValue = '';
};
/**
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
*/
const changeDatabase = () => {
state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
};
const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.project = p.name;
}
}
state.form.envId = null;
state.form.env = null;
state.envs = [];
getEnvs(projectId);
};
const changeEnv = (envId: number) => {
for (let p of state.envs as any) {
if (p.id == envId) {
state.form.env = p.name;
}
}
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
}
dbForm.value.validate((valid: boolean) => {
if (valid) {
state.form.port = Number.parseInt(state.form.port as any);
dbApi.saveDb.request(state.form).then(() => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const resetInputDb = () => {
state.inputDbVisible = false;
state.databaseList = [];
state.inputDbValue = '';
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
resetInputDb();
dbForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
dbForm,
InputDbRef,
handleClose,
showInputDb,
handleInputDbConfirm,
changeProject,
changeEnv,
btnOk,
cancel,
};
db: {
type: [Boolean, Object],
},
title: {
type: String,
},
})
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const rules = {
tagId: [
{
required: true,
message: '请选择标签',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
type: [
{
required: true,
message: '请选择数据库类型',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip和port',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
database: [
{
required: true,
message: '请添加数据库',
trigger: ['change', 'blur'],
},
],
}
const dbForm: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
allDatabases: [] as any,
databaseList: [] as any,
form: {
id: null,
tagId: null as any,
tagPath: null as any,
type: null,
name: null,
host: '',
port: 3306,
username: null,
password: null,
params: null,
database: '',
remark: '',
sshTunnelMachineId: null as any,
},
// 原密码
pwd: '',
btnLoading: false,
});
const {
dialogVisible,
tabActiveName,
allDatabases,
databaseList,
form,
pwd,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.db) {
state.form = { ...newValue.db };
// 将数据库名使用空格切割,获取所有数据库列表
state.databaseList = newValue.db.database.split(' ');
} else {
state.form = { port: 3306 } as any;
state.databaseList = [];
}
});
/**
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
*/
const changeDatabase = () => {
state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
};
const getAllDatabase = async () => {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
state.allDatabases = await dbApi.getAllDatabase.request(reqForm);
ElMessage.success('获取成功, 请选择需要管理操作的数据库');
};
const getDbPwd = async () => {
state.pwd = await dbApi.getDbPwd.request({ id: state.form.id });
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
}
dbForm.value.validate(async (valid: boolean) => {
if (valid) {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
if (!state.form.sshTunnelMachineId) {
reqForm.sshTunnelMachineId = -1;
}
dbApi.saveDb.request(reqForm).then(() => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const resetInputDb = () => {
state.databaseList = [];
state.allDatabases = [];
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
resetInputDb();
}, 500);
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,68 +2,70 @@
<div>
<el-dialog :title="`${title} 详情`" v-model="dialogVisible" :before-close="cancel" width="90%">
<el-table @cell-click="cellClick" :data="data.res">
<el-table-column :width="200" :prop="item" :label="item" v-for="item in data.colNames" :key="item"> </el-table-column>
<el-table-column :width="200" :prop="item" :label="item" v-for="item in data.colNames" :key="item">
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script lang="ts">
import { watch, toRefs, reactive, defineComponent } from 'vue';
<script lang="ts" setup>
import { watch, toRefs, reactive } from 'vue';
export default defineComponent({
name: 'tableEdit',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
data: {
type: Object,
},
const props = defineProps({
visible: {
type: Boolean,
},
setup(props: any, { emit }) {
const state = reactive({
dialogVisible: false,
data: {
res: [],
colNames: [],
},
});
title: {
type: String,
},
data: {
type: Object,
},
})
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
state.data.res = newValue.data.res;
state.data.colNames = newValue.data.colNames;
});
const cellClick = (row: any, column: any, cell: any, event: any) => {
console.log(cell.children[0].tagName);
let isDiv = cell.children[0].tagName === 'DIV';
let text = cell.children[0].innerText;
let div = cell.children[0];
if (isDiv) {
let input = document.createElement('input');
input.setAttribute('value', text);
cell.replaceChildren(input);
input.focus();
input.addEventListener('blur', () => {
div.innerText = input.value;
cell.replaceChildren(div);
});
}
};
const cancel = () => {
emit('update:visible', false);
};
return {
...toRefs(state),
cancel,
cellClick,
};
//定义事件
const emit = defineEmits(['update:visible'])
const state = reactive({
dialogVisible: false,
data: {
res: [],
colNames: [],
},
});
const {
dialogVisible,
data,
} = toRefs(state)
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.data.res = newValue.data.res;
state.data.colNames = newValue.data.colNames;
});
const cellClick = (row: any, column: any, cell: any) => {
let isDiv = cell.children[0].tagName === 'DIV';
let text = cell.children[0].innerText;
let div = cell.children[0];
if (isDiv) {
let input = document.createElement('input');
input.setAttribute('value', text);
cell.replaceChildren(input);
input.focus();
input.addEventListener('blur', () => {
div.innerText = input.value;
cell.replaceChildren(div);
});
}
};
const cancel = () => {
emit('update:visible', false);
};
</script>

View File

@@ -4,7 +4,10 @@ export const dbApi = {
// 获取权限列表
dbs: Api.create("/dbs", 'get'),
saveDb: Api.create("/dbs", 'post'),
getAllDatabase: Api.create("/dbs/databases", 'post'),
getDbPwd: Api.create("/dbs/{id}/pwd", 'get'),
deleteDb: Api.create("/dbs/{id}", 'delete'),
dumpDb: Api.create("/dbs/{id}/dump", 'post'),
tableInfos: Api.create("/dbs/{id}/t-infos", 'get'),
tableIndex: Api.create("/dbs/{id}/t-index", 'get'),
tableDdl: Api.create("/dbs/{id}/t-create-ddl", 'get'),
@@ -12,7 +15,7 @@ export const dbApi = {
columnMetadata: Api.create("/dbs/{id}/c-metadata", 'get'),
// 获取表即列提示
hintTables: Api.create("/dbs/{id}/hint-tables", 'get'),
sqlExec: Api.create("/dbs/{id}/exec-sql", 'get'),
sqlExec: Api.create("/dbs/{id}/exec-sql", 'post'),
// 保存sql
saveSql: Api.create("/dbs/{id}/sql", 'post'),
// 获取保存的sql
@@ -20,4 +23,6 @@ export const dbApi = {
// 获取保存的sql names
getSqlNames: Api.create("/dbs/{id}/sql-names", 'get'),
deleteDbSql: Api.create("/dbs/{id}/sql", 'delete'),
// 获取数据库sql执行记录
getSqlExecs: Api.create("/dbs/{dbId}/sql-execs", 'get'),
}

View File

@@ -0,0 +1,323 @@
<template>
<div>
<el-table @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
@sort-change="(sort: any) => onTableSortChange(sort)" @selection-change="onDataSelectionChange"
:data="datas" size="small" :max-height="tableHeight" v-loading="loading" element-loading-text="查询中..."
:empty-text="emptyText" stripe border class="mt5">
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
<el-table-column min-width="100" :width="DbInst.flexColumnWidth(item, datas)" align="center"
v-for="item in columnNames" :key="item" :prop="item" :label="item" show-overflow-tooltip
:sortable="sortable">
<template #header v-if="showColumnTip">
<el-tooltip raw-content placement="top" effect="customized">
<template #content> {{ getColumnTip(item) }} </template>
{{ item }}
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs } from 'vue';
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '../db';
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField'])
const props = defineProps({
dbId: {
type: Number,
required: true,
},
dbType: {
type: String,
default: ''
},
db: {
type: String,
required: true,
},
table: {
type: String,
default: '',
},
data: {
type: Array,
},
columnNames: {
type: Array,
},
sortable: {
type: [String, Boolean],
default: false,
},
loading: {
type: Boolean,
default: false,
},
emptyText: {
type: String,
default: '暂无数据',
},
showColumnTip: {
type: Boolean,
default: false,
},
height: {
type: String,
default: '600'
}
})
const state = reactive({
dbId: 0, // 当前选中操作的数据库实例
dbType: '',
db: '', // 数据库名
table: '', // 当前的表名
datas: [],
columnNames: [],
columns: [],
sortable: false,
loading: false,
selectionDatas: [] as any,
showColumnTip: false,
tableHeight: '600',
emptyText: '',
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
});
const {
tableHeight,
datas,
sortable,
loading,
columnNames,
showColumnTip,
} = toRefs(state);
watch(props, (newValue: any) => {
setState(newValue);
});
onMounted(async () => {
console.log('in DbTable mounted');
setState(props);
})
const setState = (props: any) => {
state.dbId = props.dbId;
state.dbType = props.dbType;
state.db = props.db;
state.table = props.table;
state.datas = props.data;
state.tableHeight = props.height;
state.sortable = props.sortable;
state.loading = props.loading;
state.columnNames = props.columnNames;
state.showColumnTip = props.showColumnTip;
state.emptyText = props.emptyText;
}
const getColumnTip = (columnName: string) => {
// 优先从 table map中获取
let columns = getNowDb().getColumns(state.table);
if (!columns) {
return '';
}
const column = columns.find((c: any) => c.columnName == columnName);
const comment = column.columnComment;
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
};
/**
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
cancelUpdateFields();
emits('sortChange', sort);
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
emits('selectionChange', datas);
};
// 监听单元格点击事件
const cellClick = (row: any, column: any, cell: any) => {
const property = column.property;
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
if (!state.table || !property) {
return;
}
let div: HTMLElement = cell.children[0];
if (div && div.tagName === 'DIV') {
// 转为字符串比较,可能存在数字等
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
let input = document.createElement('input');
input.setAttribute('value', text);
// 将表格width也赋值于输入框避免输入框长度超过表格长度
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
cell.replaceChildren(input);
input.focus();
input.addEventListener('blur', async () => {
row[property] = input.value;
cell.replaceChildren(div);
if (input.value !== text) {
let currentUpdatedFields = state.updatedFields
const dbInst = getNowDbInst();
// 主键
const primaryKey = await dbInst.loadTableColumn(state.db, state.table);
const primaryKeyValue = row[primaryKey.columnName];
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
const newField = {
div, row,
fieldName: column.rawColumnKey,
fieldType: updateColumn.columnType,
oldValue: text,
newValue: input.value
} as FieldsMeta;
// 被修改的字段
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
let hasKey = false;
if (primaryKeyFields.length <= 0) {
primaryKeyFields[0] = {
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields: [newField]
}
} else {
hasKey = true
let hasField = primaryKeyFields[0].fields.some(a => {
if (a.fieldName === newField.fieldName) {
a.newValue = newField.newValue
}
return a.fieldName === newField.fieldName
})
if (!hasField) {
primaryKeyFields[0].fields.push(newField)
}
}
let fields = primaryKeyFields[0].fields
const fieldsParam = fields.filter((a) => {
if (a.fieldName === column.rawColumnKey) {
a.newValue = input.value
}
return a.fieldName === column.rawColumnKey
})
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
if (field.oldValue === input.value) { // 新值=旧值
// 删除数据
div.classList.remove('update_field_active')
let delIndex: number[] = [];
currentUpdatedFields.forEach((a, i) => {
if (a.primaryKey === primaryKeyValue) {
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.rawColumnKey) : [];
a.fields.length <= 0 && delIndex.push(i)
}
});
delIndex.forEach(i => delete currentUpdatedFields[i])
currentUpdatedFields = currentUpdatedFields.filter(a => a)
} else {
// 新增数据
div.classList.add('update_field_active')
if (hasKey) {
currentUpdatedFields.forEach((value, index, array) => {
if (value.primaryKey === primaryKeyValue) {
array[index].fields = fields
}
})
} else {
currentUpdatedFields.push({
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields
})
}
}
state.updatedFields = currentUpdatedFields;
changeUpdatedField();
}
});
}
};
const submitUpdateFields = () => {
let currentUpdatedFields = state.updatedFields;
if (currentUpdatedFields.length <= 0) {
return;
}
const db = state.db;
let res = '';
let divs: HTMLElement[] = [];
currentUpdatedFields.forEach(a => {
let sql = `UPDATE ${state.table} SET `;
let primaryKey = a.primaryKey;
let primaryKeyType = a.primaryKeyType;
let primaryKeyName = a.primaryKeyName;
a.fields.forEach(f => {
sql += ` ${f.fieldName} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
divs.push(f.div)
})
sql = sql.substring(0, sql.length - 1)
sql += ` WHERE ${primaryKeyName} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
res += sql;
})
DbInst.getInst(state.dbId).promptExeSql(db, res, () => { }, () => {
currentUpdatedFields = [];
divs.forEach(a => {
a.classList.remove('update_field_active');
})
state.updatedFields = [];
changeUpdatedField();
});
}
const cancelUpdateFields = () => {
state.updatedFields.forEach((a: any) => {
a.fields.forEach((b: any) => {
b.div.classList.remove('update_field_active')
b.row[b.fieldName] = b.oldValue
})
})
state.updatedFields = [];
changeUpdatedField();
}
const changeUpdatedField = () => {
emits('changeUpdatedField', state.updatedFields);
}
const getNowDb = () => {
return DbInst.getInst(state.dbId).getDb(state.db);
}
const getNowDbInst = () => {
return DbInst.getInst(state.dbId);
}
defineExpose({
submitUpdateFields,
cancelUpdateFields
})
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -34,11 +34,11 @@ const SqlExecBox = (props: SqlExecProps): void => {
if (boxInstance) {
const boxVue = boxInstance.component
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.proxy.open(props);
boxVue.exposed.open(props);
} else {
boxInstance = renderBox()
SqlExecBox(props)
}
}
export default SqlExecBox;
export default SqlExecBox;

View File

@@ -1,7 +1,8 @@
<template>
<div>
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
<codemirror height="350px" class="codesql" ref="cmEditor" language="sql" v-model="sqlValue" :options="cmOptions" />
<el-dialog :destroy-on-close="true" title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel"> </el-button>
@@ -12,128 +13,127 @@
</div>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent } from 'vue';
<script lang="ts" setup>
import { toRefs, ref, nextTick, reactive } from 'vue';
import { dbApi } from '../api';
import { ElDialog, ElButton } from 'element-plus';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
// import base style
import 'codemirror/lib/codemirror.css';
// 引入主题后还需要在 options 中指定主题才会生效
import 'codemirror/theme/base16-light.css';
import 'codemirror/addon/selection/active-line';
import { codemirror } from '@/components/codemirror';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter';
import { SqlExecProps } from './SqlExecBox';
export default defineComponent({
name: 'SqlExecDialog',
components: {
codemirror,
ElButton,
ElDialog,
const props = defineProps({
visible: {
type: Boolean,
},
props: {
visible: {
type: Boolean,
},
dbId: {
type: [Number],
},
db: {
type: String,
},
sql: {
type: String,
},
dbId: {
type: [Number],
},
setup(props: any) {
const state = reactive({
dialogVisible: false,
sqlValue: '',
dbId: 0,
db: '',
btnLoading: false,
cmOptions: {
tabSize: 4,
mode: 'text/x-sql',
lineNumbers: true,
line: true,
indentWithTabs: true,
smartIndent: true,
matchBrackets: true,
theme: 'base16-light',
autofocus: true,
extraKeys: { Tab: 'autocomplete' }, // 自定义快捷键
},
});
state.sqlValue = props.sql;
let runSuccessCallback: any;
let cancelCallback: any;
let runSuccess: boolean = false;
/**
* 执行sql
*/
const runSql = async () => {
try {
state.btnLoading = true;
await dbApi.sqlExec.request({
id: state.dbId,
db: state.db,
sql: state.sqlValue.trim(),
});
runSuccess = true;
} catch (e) {
runSuccess = false;
}
if (runSuccess && runSuccessCallback) {
runSuccessCallback();
}
state.btnLoading = false;
cancel();
};
const cancel = () => {
state.dialogVisible = false;
// 没有执行成功,并且取消回调函数存在,则执行
if (!runSuccess && cancelCallback) {
cancelCallback();
}
setTimeout(() => {
state.dbId = 0;
state.sqlValue = '';
runSuccessCallback = null;
cancelCallback = null;
runSuccess = false;
}, 200);
};
const open = (props: SqlExecProps) => {
runSuccessCallback = props.runSuccessCallback;
cancelCallback = props.cancelCallback;
state.sqlValue = sqlFormatter(props.sql);
state.dbId = props.dbId;
state.db = props.db;
state.dialogVisible = true;
};
return {
...toRefs(state),
open,
runSql,
cancel,
};
db: {
type: String,
},
sql: {
type: String,
},
})
const remarkInputRef = ref<InputInstance>();
const state = reactive({
dialogVisible: false,
sqlValue: '',
dbId: 0,
db: '',
remark: '',
btnLoading: false,
});
const {
dialogVisible,
sqlValue,
remark,
btnLoading
} = toRefs(state)
state.sqlValue = props.sql as any;
let runSuccessCallback: any;
let cancelCallback: any;
let runSuccess: boolean = false;
/**
* 执行sql
*/
const runSql = async () => {
if (!state.remark) {
ElMessage.error('请输入执行的备注信息');
return;
}
try {
state.btnLoading = true;
const res = await dbApi.sqlExec.request({
id: state.dbId,
db: state.db,
remark: state.remark,
sql: state.sqlValue.trim(),
});
for (let re of res.res) {
if (re.result !== 'success') {
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
throw new Error(re.result)
}
}
runSuccess = true;
ElMessage.success('执行成功');
} catch (e) {
runSuccess = false;
}
if (runSuccess) {
if (runSuccessCallback) {
runSuccessCallback();
}
cancel();
}
state.btnLoading = false;
};
const cancel = () => {
state.dialogVisible = false;
// 没有执行成功,并且取消回调函数存在,则执行
if (!runSuccess && cancelCallback) {
cancelCallback();
}
setTimeout(() => {
state.dbId = 0;
state.sqlValue = '';
state.remark = '';
runSuccessCallback = null;
cancelCallback = null;
runSuccess = false;
}, 200);
};
const open = (props: SqlExecProps) => {
runSuccessCallback = props.runSuccessCallback;
cancelCallback = props.cancelCallback;
state.sqlValue = sqlFormatter(props.sql);
state.dbId = props.dbId;
state.db = props.db;
state.dialogVisible = true;
nextTick(() => {
setTimeout(() => {
remarkInputRef.value?.focus();
});
});
};
defineExpose({ open })
</script>
<style lang="scss">
.codesql {
font-size: 9pt;
font-weight: 600;
}
.footer {
float: right;
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<div>
<div>
<div class="toolbar">
<div class="fl">
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay">
</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-upload class="sql-file-exec" :before-upload="beforeUpload" :on-success="execSqlFileSuccess"
:headers="{ Authorization: token }" :action="getUploadSqlFileUrl()" :show-file-list="false"
name="file" multiple :limit="100">
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
<el-link type="success" :underline="false" icon="Document"></el-link>
</el-tooltip>
</el-upload>
</div>
<div style="float: right" class="fl">
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL
</el-button>
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL
</el-button>
</div>
</div>
</div>
<div class="mt5 sqlEditor">
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }">
</div>
</div>
<div class="mt5">
<el-row>
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete"
:underline="false"></el-link>
<span v-if="execRes.data.length > 0">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="exportData"><span
style="font-size: 12px">导出</span></el-link>
</span>
<span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span
style="font-size: 12px">提交</span></el-link>
</span>
<span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span
style="font-size: 12px">取消</span></el-link>
</span>
</el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db"
:data="execRes.data" :table="state.table" :column-names="execRes.tableColumn" :loading="loading"
height="250" empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange" @change-updated-field="changeUpdatedField"></db-table>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, watch, onMounted, computed, reactive, toRefs, ref, Ref } from 'vue';
import { useStore } from '@/store/index.ts';
import { getSession } from '@/common/utils/storage';
import { isTrue, notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import * as monaco from 'monaco-editor';
import { editor } from 'monaco-editor';
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import DbTable from '../DbTable.vue'
import { TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import { dbApi } from '../../api';
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess'])
const props = defineProps({
data: {
type: TabInfo,
required: true
},
// sql脚本名若有则去加载该sql内容
sqlName: {
type: String,
default: '',
},
editorHeight: {
type: String,
default: '600'
}
})
const store = useStore();
const token = getSession('token');
let monacoEditor = {} as editor.IStandaloneCodeEditor;
const dbTableRef = ref(null) as Ref;
const state = reactive({
token,
ti: {} as TabInfo,
dbs: [],
dbId: null, // 当前选中操作的数据库实例
table: '', // 当前单表操作sql的表信息
sqlName: '',
sql: '', // 当前编辑器的sql内容
loading: false, // 是否在加载数据
execRes: {
data: [],
tableColumn: []
},
selectionDatas: [] as any,
editorHeight: '500',
hasUpdatedFileds: false,
});
const {
editorHeight,
ti,
execRes,
table,
sqlName,
loading,
hasUpdatedFileds,
} = toRefs(state);
watch(() => props.editorHeight, (newValue: any) => {
state.editorHeight = newValue;
});
onMounted(async () => {
console.log('in query mounted');
state.ti = props.data;
state.editorHeight = props.editorHeight;
const params = state.ti.params;
state.dbs = params && params.dbs;
if (params && params.sqlName) {
state.sqlName = params.sqlName;
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
state.sql = res.sql;
}
nextTick(() => {
setTimeout(() => initMonacoEditor(), 50)
})
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
})
// 获取布局配置信息
const getThemeConfig: any = computed(() => {
return store.state.themeConfig.themeConfig;
});
self.MonacoEnvironment = {
getWorker() {
return new EditorWorker();
}
};
const initMonacoEditor = () => {
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
monacoEditor = monaco.editor.create(monacoTextarea, {
language: 'sql',
theme: getThemeConfig.value.editorTheme,
automaticLayout: true, //自适应宽高布局
folding: false,
roundedSelection: false, // 禁用选择文本背景的圆角
matchBrackets: 'near',
linkedEditing: true,
cursorBlinking: 'smooth',// 光标闪烁样式
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时在编辑器中缩放字体
overviewRulerBorder: false, // 不要滚动条的边框
tabSize: 2, // tab 缩进长度
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
fontWeight: 'bold',
// letterSpacing: 1, 字符间距
// quickSuggestions:false, // 禁用代码提示
minimap: {
enabled: false, // 不要小地图
},
});
// 注册快捷键ctrl + R 运行选中的sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
id: 'run-sql-action' + state.ti.key,
// A label of the action that will be presented to the user.
label: '执行SQL',
// A precondition for this action.
precondition: undefined,
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0)
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: async function () {
try {
await onRunSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
}
});
// 注册快捷键ctrl + shift + f 格式化sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
id: 'format-sql-action' + state.ti.key,
// A label of the action that will be presented to the user.
label: '格式化SQL',
// A precondition for this action.
precondition: undefined,
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0)
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 2,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: async function () {
try {
await formatSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
}
});
// 动态设置主题
// monaco.editor.setTheme('hc-black');
// 如果sql有值则默认赋值
if (state.sql) {
monacoEditor.getModel()?.setValue(state.sql);
}
};
/**
* 执行sql
*/
const onRunSql = async () => {
// 没有选中的文本,则为全部文本
let sql = getSql() as string;
notBlank(sql && sql.trim(), '请选中需要执行的sql');
// 去除字符串前的空格、换行等
sql = sql.replace(/(^\s*)/g, '');
let execRemark = '';
let canRun = true;
if (
sql.startsWith('update') ||
sql.startsWith('UPDATE') ||
sql.startsWith('INSERT') ||
sql.startsWith('insert') ||
sql.startsWith('DELETE') ||
sql.startsWith('delete')
) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
inputErrorMessage: '请输入执行该sql的备注信息',
});
execRemark = res.value;
if (!execRemark) {
canRun = false;
}
}
if (!canRun) {
return;
}
try {
state.loading = true;
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集')
}
state.execRes.data = colAndData.res;
state.execRes.tableColumn = colAndData.colNames;
cancelUpdateFields()
} catch (e: any) {
state.execRes.data = [];
state.execRes.tableColumn = [];
state.table = '';
return;
} finally {
state.loading = false;
}
// 即只有以该字符串开头的sql才可修改表数据内容
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
state.selectionDatas = [];
const tableName = sql.split(/from/i)[1];
if (tableName) {
const tn = tableName.trim().split(' ')[0];
state.table = tn;
state.table = tn;
} else {
state.table = '';
}
} else {
state.table = '';
}
};
/**
* 获取sql如果有鼠标选中则返回选中内容否则返回输入框内所有内容
*/
const getSql = () => {
let res = '' as string | undefined;
// 编辑器还没初始化
if (!monacoEditor?.getModel) {
return res;
}
// 选择选中的sql
let selection = monacoEditor.getSelection()
if (selection) {
res = monacoEditor.getModel()?.getValueInRange(selection)
}
// 整个编辑器的sql
if (!res) {
return monacoEditor.getModel()?.getValue()
}
return res
};
const saveSql = async () => {
const sql = monacoEditor.getModel()?.getValue();
notBlank(sql, 'sql内容不能为空');
let sqlName = state.sqlName;
const newSql = !sqlName;
if (newSql) {
try {
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern:
/\w+/,
inputErrorMessage: '请输入SQL脚本名',
});
sqlName = input.value;
state.sqlName = sqlName;
} catch (e) {
return;
}
}
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
ElMessage.success('保存成功');
// 保存sql脚本成功事件
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
};
const deleteSql = async () => {
const sqlName = state.sqlName;
notBlank(sqlName, "该sql内容未保存");
const { dbId, db } = state.ti;
try {
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
ElMessage.success('删除成功');
emits('deleteSqlSuccess', dbId, db);
} catch (err) { }
};
/**
* 格式化sql
*/
const formatSql = () => {
let selection = monacoEditor.getSelection()
if (!selection) {
return;
}
let sql = monacoEditor.getModel()?.getValueInRange(selection)
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
if (sql) {
replaceSelection(sqlFormatter(sql), selection)
return;
}
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
};
/**
* 提交事务,用于没有开启自动提交事务
*/
const onCommit = () => {
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
ElMessage.success('COMMIT success');
};
/**
* 替换选中的内容
*/
const replaceSelection = (str: string, selection: any) => {
const model = monacoEditor.getModel();
if (!model) {
return;
}
if (!selection) {
model.setValue(str);
return;
}
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
const textBeforeSelection = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: startLineNumber,
endColumn: startColumn,
})
const textAfterSelection = model.getValueInRange({
startLineNumber: endLineNumber,
startColumn: endColumn,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
})
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection)
monacoEditor.focus()
monacoEditor.setPosition({
lineNumber: startLineNumber,
column: 0,
})
}
/**
* 导出当前页数据
*/
const exportData = () => {
const dataList = state.execRes.data as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.execRes.tableColumn, dataList)
};
const beforeUpload = (file: File) => {
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
};
// 执行sql成功
const execSqlFileSuccess = (res: any) => {
if (res.code !== 200) {
ElMessage.error(res.msg);
}
};
// 获取sql文件上传执行url
const getUploadSqlFileUrl = () => {
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
const primaryKey = await dbInst.loadTableColumn(db, state.table);
const primaryKeyColumnName = primaryKey.columnName;
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
state.execRes.data = state.execRes.data.filter(
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
);
state.selectionDatas = [];
});
};
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
}
</script>
<style lang="scss">
.sql-file-exec {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
vertical-align: middle;
position: relative;
text-decoration: none;
}
.sqlEditor {
font-size: 8pt;
font-weight: 600;
border: 1px solid #ccc;
}
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -0,0 +1,348 @@
<template>
<div>
<el-row>
<el-col :span="8">
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5">
</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="addRow()" type="primary" icon="plus" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onDeleteData()" type="danger" icon="delete" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
<el-link @click="onGenerateInsertSql()" type="success" :underline="false">gi</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="导出当前页的csv文件" placement="top">
<el-link type="success" :underline="false" @click="exportData"><span class="f12">导出</span></el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
</el-tooltip>
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="取消修改" placement="top">
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="f12">取消</el-link>
</el-tooltip>
</el-col>
<el-col :span="16">
<el-input v-model="condition" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可" clearable size="small"
style="width: 100%">
<template #prepend>
<el-popover trigger="click" :width="320" placement="right">
<template #reference>
<el-link type="success" :underline="false">选择列</el-link>
</template>
<el-table :data="columns" max-height="500" size="small" @row-click="
(...event: any) => {
onConditionRowClick(event);
}
" style="cursor: pointer">
<el-table-column property="columnName" label="列名" show-overflow-tooltip>
</el-table-column>
<el-table-column property="columnComment" label="备注" show-overflow-tooltip>
</el-table-column>
</el-table>
</el-popover>
</template>
<template #append>
<el-button @click="onSelectByCondition()" icon="search" size="small"></el-button>
</template>
</el-input>
</el-col>
</el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db" :data="datas"
:table="state.table" :column-names="columnNames" :loading="loading" :height="tableHeight"
:show-column-tip="true" :sortable="'custom'" @sort-change="(sort: any) => onTableSortChange(sort)"
@selection-change="onDataSelectionChange" @change-updated-field="changeUpdatedField"></db-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination small :total="count" @current-change="pageChange()" layout="prev, pager, next, total, jumper"
v-model:current-page="pageNum" :page-size="DbInst.DefaultLimit"></el-pagination>
</el-row>
<div style=" font-size: 12px; padding: 0 10px; color: #606266"><span>{{ state.sql }}</span>
</div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
<el-row>
<el-col :span="5">
<el-select v-model="conditionDialog.condition">
<el-option label="=" value="="> </el-option>
<el-option label="LIKE" value="LIKE"> </el-option>
<el-option label=">" value=">"> </el-option>
<el-option label=">=" value=">="> </el-option>
<el-option label="<" value="<"> </el-option>
<el-option label="<=" value="<="> </el-option>
</el-select>
</el-col>
<el-col :span="19">
<el-input v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancelCondition">取消</el-button>
<el-button type="primary" @click="onConfirmCondition">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs, ref, Ref } from 'vue';
import { isTrue, notEmpty, notBlank } from '@/common/assert';
import { ElMessage } from 'element-plus';
import { DbInst, TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import DbTable from '../DbTable.vue'
const emits = defineEmits(['genInsertSql'])
const props = defineProps({
data: {
type: TabInfo,
required: true
},
tableHeight: {
type: String,
default: '600'
}
})
const dbTableRef = ref(null) as Ref;
const state = reactive({
ti: {} as TabInfo,
table: '', // 当前的表名
datas: [],
sql: '', // 当前数据tab执行的sql
orderBy: '',
condition: '', // 当前条件框的条件
loading: false, // 是否在加载数据
columnNames: [],
columns: [],
pageNum: 1,
count: 0,
selectionDatas: [] as any,
conditionDialog: {
title: '',
placeholder: '',
columnRow: null,
dataTab: null,
visible: false,
condition: '=',
value: null
},
tableHeight: '600',
hasUpdatedFileds: false,
});
const {
datas,
condition,
loading,
columns,
columnNames,
pageNum,
count,
hasUpdatedFileds,
conditionDialog,
} = toRefs(state);
watch(() => props.tableHeight, (newValue: any) => {
state.tableHeight = newValue;
});
onMounted(async () => {
console.log('in table data mounted');
state.ti = props.data;
state.tableHeight = props.tableHeight;
state.table = state.ti.params.table;
notBlank(state.table, "TableData组件params.table信息不能为空")
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
state.columns = columns;
state.columnNames = columns.map((t: any) => t.columnName);
await onRefresh();
})
const onRefresh = async () => {
// 查询条件置空
state.condition = '';
state.pageNum = 1;
await selectData();
}
/**
* 数据tab修改页数
*/
const pageChange = async () => {
await selectData();
};
/**
* 单表数据信息查询数据
*/
const selectData = async () => {
state.loading = true;
const dbInst = state.ti.getNowDbInst();
const { db } = state.ti;
try {
const countRes = await dbInst.runSql(db, DbInst.getDefaultCountSql(state.table));
state.count = countRes.res[0].count;
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum);
state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res;
} else {
state.datas = [];
}
} finally {
state.loading = false;
}
}
/**
* 导出当前页数据
*/
const exportData = () => {
const dataList = state.datas as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.columnNames, dataList)
};
/**
* 条件查询,点击列信息后显示输入对应的值
*/
const onConditionRowClick = (event: any) => {
const row = event[0];
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
state.conditionDialog.columnRow = row;
state.conditionDialog.visible = true;
};
// 确认条件
const onConfirmCondition = () => {
const conditionDialog = state.conditionDialog;
let condition = state.condition;
if (condition) {
condition += ` AND `;
}
const row = conditionDialog.columnRow as any;
condition += `${row.columnName} ${conditionDialog.condition} `;
state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
onCancelCondition();
};
const onCancelCondition = () => {
state.conditionDialog.visible = false;
state.conditionDialog.title = ``;
state.conditionDialog.placeholder = ``;
state.conditionDialog.value = null;
state.conditionDialog.columnRow = null;
state.conditionDialog.dataTab = null;
};
/**
* 提交事务,用于没有开启自动提交事务
*/
const onCommit = () => {
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
ElMessage.success('COMMIT success');
};
const onSelectByCondition = async () => {
notEmpty(state.condition, '条件不能为空');
state.pageNum = 1;
await selectData();
}
/**
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
const sortType = sort.order == 'descending' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${sort.prop} ${sortType}`;
await onRefresh();
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
onRefresh();
});
};
const onGenerateInsertSql = async () => {
isTrue(state.selectionDatas && state.selectionDatas.length > 0, '请先选择数据');
emits('genInsertSql', state.ti.getNowDbInst().genInsertSql(state.ti.db, state.table, state.selectionDatas));
};
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
}
// 添加新数据行
const addRow = async () => {
const columns = state.ti.getNowDb().getColumns(state.table);
// key: 字段名value: 字段名提示
let obj: any = {};
columns.forEach((item: any) => {
obj[`${item.columnName}`] = `'${item.columnComment || ''} ${item.columnName}[${item.columnType}]${item.nullable == 'YES' ? '' : '[not null]'}'`;
});
let columnNames = Object.keys(obj).join(',');
let values = Object.values(obj).join(',');
let sql = `INSERT INTO ${state.table} (${columnNames}) VALUES (${values});`;
state.ti.getNowDbInst().promptExeSql(state.ti.db, sql, null, () => {
onRefresh();
});
};
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -0,0 +1,473 @@
/* eslint-disable no-unused-vars */
import { dbApi } from './api';
import SqlExecBox from './component/SqlExecBox';
const dbInstCache: Map<number, DbInst> = new Map();
export class DbInst {
/**
* 标签路径
*/
tagPath: string
/**
* 实例id
*/
id: number
/**
* 实例名
*/
name: string
/**
* 数据库类型, mysql postgres
*/
type: string
/**
* schema -> db
*/
dbs: Map<string, Db> = new Map()
/**
* 默认查询分页数量
*/
static DefaultLimit = 20;
/**
* 获取指定数据库实例,若不存在则新建并缓存
* @param dbName 数据库名
* @returns db实例
*/
getDb(dbName: string) {
if (!dbName) {
throw new Error('dbName不能为空')
}
let db = this.dbs.get(dbName)
if (db) {
return db;
}
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
db = new Db();
db.name = dbName;
this.dbs.set(dbName, db);
return db;
}
/**
* 加载数据库表信息
* @param dbName 数据库名
* @returns 表信息
*/
async loadTables(dbName: string) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let tables = db.tables;
if (tables) {
return tables;
}
console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName });
db.tables = tables;
return tables;
}
/**
* 获取表的所有列信息
* @param table 表名
*/
async loadColumns(dbName: string, table: string) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let columns = db.getColumns(table);
if (columns) {
return columns;
}
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
columns = await dbApi.columnMetadata.request({
id: this.id,
db: dbName,
tableName: table,
});
db.columnsMap.set(table, columns);
return columns;
}
/**
* 获取指定表的指定信息
* @param table 表名
*/
async loadTableColumn(dbName: string, table: string, columnName?: string) {
// 确保该表的列信息都已加载
await this.loadColumns(dbName, table);
return this.getDb(dbName).getColumn(table, columnName);
}
/**
* 获取库信息提示
*/
async loadDbHints(dbName: string) {
const db = this.getDb(dbName);
if (db.tableHints) {
return db.tableHints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name, })
db.tableHints = hits;
return hits;
}
/**
* 执行sql
*
* @param sql sql
* @param remark 执行备注
*/
async runSql(dbName: string, sql: string, remark: string = '') {
return await dbApi.sqlExec.request({
id: this.id,
db: dbName,
sql: sql.trim(),
remark,
});
}
// 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
const baseSql = `SELECT * FROM ${table} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`;
if (this.type == 'mysql') {
return `${baseSql} LIMIT ${(pageNum - 1) * limit}, ${limit};`;
}
if (this.type == 'postgres') {
return `${baseSql} OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
}
return baseSql;
}
/**
* 生成指定数据的insert语句
* @param dbName 数据库名
* @param table 表名
* @param datas 要生成的数据
*/
genInsertSql(dbName: string, table: string, datas: any[]): string {
if (!datas) {
return '';
}
const columns = this.getDb(dbName).getColumns(table);
const sqls = [];
for (let data of datas) {
let colNames = [];
let values = [];
for (let column of columns) {
const colName = column.columnName;
colNames.push(colName);
values.push(DbInst.wrapValueByType(data[colName]));
}
sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
}
return sqls.join(';\n') + ';'
}
/**
* 生成根据主键删除的sql语句
* @param table 表名
* @param datas 要删除的记录
*/
genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
const primaryKey = this.getDb(db).getColumn(table);
const primaryKeyColumnName = primaryKey.columnName;
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
return `DELETE FROM ${table} WHERE ${primaryKeyColumnName} IN (${ids})`;
}
/*
* 弹框提示是否执行sql
*/
promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
SqlExecBox({
sql, dbId: this.id, db,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
});
};
/**
* 获取或新建dbInst如果缓存中不存在则新建否则直接返回
* @param inst 数据库实例,后端返回的列表接口中的信息
* @returns DbInst
*/
static getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空')
}
let dbInst = dbInstCache.get(inst.id);
if (dbInst) {
return dbInst;
}
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
dbInst = new DbInst();
dbInst.tagPath = inst.tagPath;
dbInst.id = inst.id;
dbInst.name = inst.name;
dbInst.type = inst.type;
dbInstCache.set(dbInst.id, dbInst);
return dbInst;
}
/**
* 获取数据库实例id若不存在则新建一个并缓存
* @param dbId 数据库实例id
* @param dbType 第一次获取时为必传项,即第一次创建时
* @returns 数据库实例
*/
static getInst(dbId?: number): DbInst {
if (!dbId) {
throw new Error('dbId不能为空');
}
let dbInst = dbInstCache.get(dbId);
if (dbInst) {
return dbInst;
}
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.newInst()新建该实例');
}
/**
* 清空所有实例缓存信息
*/
static clearAll() {
dbInstCache.clear();
}
/**
* 获取count sql
* @param table 表名
* @param condition 条件
* @returns count sql
*/
static getDefaultCountSql = (table: string, condition?: string) => {
return `SELECT COUNT(*) count FROM ${table} ${condition ? 'WHERE ' + condition : ''}`;
};
/**
* 根据返回值包装值,若值为字符串类型则添加''
* @param val 值
* @returns 包装后的值
*/
static wrapValueByType = (val: any) => {
if (val == null) {
return 'NULL';
}
if (typeof val == 'number') {
return val;
}
return `'${val}'`;
};
/**
* 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可
*/
static wrapColumnValue(columnType: string, value: any) {
if (columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi)) {
return value;
}
return `'${value}'`;
};
/**
*
* @param str 字符串
* @param tableData 表数据
* @param flag 标志
* @returns 列宽度
*/
static flexColumnWidth = (str: any, tableData: any, flag = 'equal') => {
// str为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
// flag为可选值可不传该参数,传参时可选'max'或'equal',默认为'max'
// flag为'max'则设置列宽适配该列中最长的内容,flag为'equal'则设置列宽适配该列中第一行内容的长度。
str = str + '';
let columnContent = '';
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return;
}
if (!str || !str.length || str.length === 0 || str === undefined) {
return;
}
if (flag === 'equal') {
// 获取该列中第一个不为空的数据(内容)
for (let i = 0; i < tableData.length; i++) {
// 转为字符串后比较
if ((tableData[i][str] + '').length > 0) {
columnContent = tableData[i][str] + '';
break;
}
}
} else {
// 获取该列中最长的数据(内容)
let index = 0;
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str] === null) {
return;
}
const now_temp = tableData[i][str] + '';
const max_temp = tableData[index][str] + '';
if (now_temp.length > max_temp.length) {
index = i;
}
}
columnContent = tableData[index][str] + '';
}
const contentWidth: number = DbInst.getContentWidth(columnContent);
// 获取列名称的长度 加上排序图标长度
const columnWidth: number = DbInst.getContentWidth(str) + 43;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth + 'px';
};
/**
* 获取内容所需要占用的宽度
*/
static getContentWidth = (content: any): number => {
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 如果是小写字母、数字字符分配8个单位宽度
flexWidth += 8.5;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 16;
} else {
// 其他种类字符为字符分配9个单位宽度
flexWidth += 8;
}
}
if (flexWidth > 500) {
// 设置最大宽度
flexWidth = 500;
}
return flexWidth;
};
}
/**
* 数据库实例信息
*/
class Db {
name: string // 库名
tables: [] // 数据库实例表信息
columnsMap: Map<string, any> = new Map // table -> columns
tableHints: any = null // 提示词
/**
* 获取指定表列信息前提需要dbInst.loadColumns
* @param table 表名
*/
getColumns(table: string) {
return this.columnsMap.get(table);
}
/**
* 获取指定表中的指定列名信息,若列名为空则默认返回主键
* @param table 表名
* @param columnName 列名
*/
getColumn(table: string, columnName: string = '') {
const cols = this.getColumns(table);
if (!columnName) {
const col = cols.find((c: any) => c.columnKey == 'PRI');
return col || cols[0];
}
return cols.find((c: any) => c.columnName == columnName);
}
}
export enum TabType {
/**
* 表数据
*/
TableData,
/**
* 查询框
*/
Query,
}
export class TabInfo {
/**
* tab唯一key。与label、name都一致
*/
key: string
/**
* 菜单树节点key
*/
treeNodeKey: string
/**
* 数据库实例id
*/
dbId: number
/**
* 库名
*/
db: string = ''
/**
* tab 类型
*/
type: TabType
/**
* tab需要的其他信息
*/
params: any
getNowDbInst() {
return DbInst.getInst(this.dbId);
}
getNowDb() {
return this.getNowDbInst().getDb(this.db);
}
}
/** 修改表字段所需数据 */
export type UpdateFieldsMeta = {
// 主键值
primaryKey: string
// 主键名
primaryKeyName: string
// 主键类型
primaryKeyType: string
// 新值
fields: FieldsMeta[]
}
export type FieldsMeta = {
// 字段所在div
div: HTMLElement
// 字段名
fieldName: string
// 字段所在的表格行数据
row: any
// 字段类型
fieldType: string
// 原值
oldValue: string
// 新值
newValue: string
}

View File

@@ -0,0 +1,12 @@
import { Enum } from '@/common/Enum'
/**
* 枚举类
*/
export default {
// 数据库sql执行类型
DbSqlExecTypeEnum: new Enum().add('UPDATE', 'UPDATE', 1)
.add('DELETE', 'DELETE', 2)
.add('INSERT', 'INSERT', 3)
.add('QUERY', 'QUERY', 4),
}

View File

@@ -1,3 +1,5 @@
export const TYPE_LIST = ['bigint', 'binary', 'blob', 'char', 'datetime', 'decimal', 'double', 'enum', 'float', 'int', 'json', 'longblob', 'longtext', 'mediumblob', 'mediumtext', 'set', 'smallint', 'text', 'time', 'timestamp', 'tinyint', 'varbinary', 'varchar']
export const TYPE_LIST = ['bigint', 'binary', 'blob', 'char', 'datetime', 'date', 'decimal', 'double', 'enum', 'float', 'int', 'json', 'longblob', 'longtext', 'mediumblob', 'mediumtext', 'set', 'smallint', 'text', 'time', 'timestamp', 'tinyint', 'varbinary', 'varchar']
export const CHARACTER_SET_NAME_LIST = ['armscii8', 'ascii', 'big5', 'binary', 'cp1250', 'cp1251', 'cp1256', 'cp1257', 'cp850', 'cp852', 'cp866', 'cp932', 'dec8', 'eucjpms', 'euckr', 'gb18030', 'gb2312', 'gbk', 'geostd8', 'greek', 'hebrew', 'hp8', 'keybcs2', 'koi8r', 'koi8u', 'latin1', 'latin2', 'latin5', 'latin7', 'macce', 'macroman', 'sjis', 'swe7', 'tis620', 'ucs2', 'ujis', 'utf16', 'utf16le', 'utf32', 'utf8', 'utf8mb4']
export const CHARACTER_SET_NAME_LIST = ['armscii8', 'ascii', 'big5', 'binary', 'cp1250', 'cp1251', 'cp1256', 'cp1257', 'cp850', 'cp852', 'cp866', 'cp932', 'dec8', 'eucjpms', 'euckr', 'gb18030', 'gb2312', 'gbk', 'geostd8', 'greek', 'hebrew', 'hp8', 'keybcs2', 'koi8r', 'koi8u', 'latin1', 'latin2', 'latin5', 'latin7', 'macce', 'macroman', 'sjis', 'swe7', 'tis620', 'ucs2', 'ujis', 'utf16', 'utf16le', 'utf32', 'utf8', 'utf8mb4']
export const COLLATION_SUFFIX_LIST = ['unicode_ci', 'bin', 'croatian_ci', 'czech_ci', 'danish_ci', 'esperanto_ci', 'estonian_ci', 'general_ci', 'german2_ci', 'hungarian_ci', 'icelandic_ci', 'latvian_ci', 'lithuanian_ci', 'persian_ci', 'polish_ci', 'roman_ci', 'romanian_ci', 'sinhala_ci', 'slovak_ci', 'slovenian_ci', 'spanish2_ci', 'spanish_ci', 'swedish_ci', 'turkish_ci', 'unicode_520_ci', 'vietnamese_ci']

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,73 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" :before-close="cancel" width="35%">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true"
:before-close="cancel" width="650px">
<el-form :model="form" ref="machineForm" :rules="rules" label-width="85px">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-input v-model.trim="form.ip" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码,修改操作可不填"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required :rules="{
required: true,
message: '请选择标签',
trigger: ['change', 'blur'],
}">
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-col :span="18">
<el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off">
</el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:">
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item label="认证方式:">
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType"
placeholder="请选认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="授权凭证" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.authType == 1" prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码"
autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证:" required>
<auth-cert-select ref="authCertSelectRef" v-model="form.authCertId" />
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="enableRecorder" label="终端回放:">
<el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div>
<el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
@@ -43,150 +76,194 @@
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { machineApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue'
export default defineComponent({
name: 'MachineEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
machine: {
type: [Boolean, Object],
},
title: {
type: String,
},
const props = defineProps({
visible: {
type: Boolean,
},
setup(props: any, { emit }) {
const machineForm: any = ref(null);
const state = reactive({
dialogVisible: false,
projects: [],
form: {
id: null,
projectId: null,
projectName: null,
name: null,
port: 22,
username: null,
password: null,
remark: '',
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
ip: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
state.projects = newValue.projects;
if (newValue.machine) {
state.form = { ...newValue.machine };
} else {
state.form = { port: 22 } as any;
}
});
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.projectName = p.name;
}
}
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
}
machineForm.value.validate((valid: boolean) => {
if (valid) {
machineApi.saveMachine.request(state.form).then(() => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
machineForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
machineForm,
changeProject,
btnOk,
cancel,
};
machine: {
type: [Boolean, Object],
},
title: {
type: String,
},
})
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const rules = {
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
ip: [
{
required: true,
message: '请输入主机ip和端口',
trigger: ['change', 'blur'],
},
],
authCertId: [
{
required: true,
message: '请选择授权凭证',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入授权用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入授权密码',
trigger: ['change', 'blur'],
},
],
}
const machineForm: any = ref(null);
const authCertSelectRef: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
sshTunnelMachineList: [] as any,
authCerts: [] as any,
authType: 1,
form: {
id: null,
ip: null,
port: 22,
name: null,
authCertId: null as any,
username: '',
password: '',
tagId: null as any,
tagPath: null as any,
remark: '',
sshTunnelMachineId: null as any,
enableRecorder: -1,
},
pwd: '',
testConnBtnLoading: false,
btnLoading: false,
});
const {
dialogVisible,
tabActiveName,
form,
testConnBtnLoading,
btnLoading,
} = toRefs(state)
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.machine) {
state.form = { ...newValue.machine };
// 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId
if (authCertId > 0) {
state.authType = 2;
} else {
state.authType = 1;
}
} else {
state.form = { port: 22 } as any;
state.authType = 1;
}
});
const changeAuthMethod = (val: any) => {
if (state.form.id) {
if (val == 2) {
state.form.authCertId = null;
} else {
state.form.password = '';
}
}
}
const testConn = async () => {
machineForm.value.validate(async (valid: boolean) => {
if (valid) {
const form = getReqForm();
if (!form) {
return;
}
state.testConnBtnLoading = true;
try {
await machineApi.testConn.request(form);
ElMessage.success('连接成功');
} finally {
state.testConnBtnLoading = false;
}
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
}
const btnOk = async () => {
machineForm.value.validate(async (valid: boolean) => {
if (valid) {
const form = getReqForm();
if (!form) {
return;
}
state.btnLoading = true;
try {
await machineApi.saveMachine.request(form);
ElMessage.success('保存成功');
emit('val-change', form);
cancel();
} finally {
state.btnLoading = false;
}
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const getReqForm = () => {
const reqForm: any = { ...state.form };
debugger
// 如果为密码认证则置空授权凭证id
if (state.authType == 1) {
reqForm.authCertId = -1;
}
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1
}
return reqForm
}
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -2,33 +2,21 @@
<div>
<el-card>
<div>
<el-button v-auth="'machine:add'" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加</el-button>
<el-button
v-auth="'machine:update'"
type="primary"
icon="edit"
:disabled="currentId == null"
@click="openFormDialog(currentData)"
plain
>编辑</el-button
>
<el-button v-auth="'machine:del'" :disabled="currentId == null" @click="deleteMachine(currentId)" type="danger" icon="delete"
>删除</el-button
>
<el-button v-auth="'machine:add'" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加
</el-button>
<el-button v-auth="'machine:update'" type="primary" icon="edit" :disabled="!currentId"
@click="openFormDialog(currentData)" plain>编辑</el-button>
<el-button v-auth="'machine:del'" :disabled="!currentId" @click="deleteMachine(currentId)" type="danger"
icon="delete">删除</el-button>
<div style="float: right">
<el-select v-model="params.projectId" placeholder="请选择项目" @clear="search" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
<el-select @focus="getTags" v-model="params.tagPath" placeholder="请选择标签" @clear="search" filterable
clearable>
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-input
class="ml5"
placeholder="请输入名称"
style="width: 150px"
v-model="params.name"
@clear="search"
plain
clearable
></el-input>
<el-input class="ml5" placeholder="请输入ip" style="width: 150px" v-model="params.ip" @clear="search" plain clearable></el-input>
<el-input class="ml5" placeholder="请输入名称" style="width: 150px" v-model="params.name" @clear="search"
plain clearable></el-input>
<el-input class="ml5" placeholder="请输入ip" style="width: 150px" v-model="params.ip" @clear="search" plain
clearable></el-input>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button>
</div>
</div>
@@ -41,326 +29,374 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="ip" label="ip:port" min-width="140">
<el-table-column prop="ip" label="ip:port" min-width="150">
<template #default="scope">
<el-link :disabled="scope.row.status == -1" @click="showMachineStats(scope.row)" type="primary" :underline="false">{{
`${scope.row.ip}:${scope.row.port}`
}}</el-link>
<el-link :disabled="scope.row.status == -1" @click="showMachineStats(scope.row)" type="primary"
:underline="false">
{{ `${scope.row.ip}:${scope.row.port}` }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="75">
<el-table-column prop="username" label="用户名" min-width="100">
</el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template #default="scope">
<el-switch
v-auth:disabled="'machine:update'"
:width="47"
v-model="scope.row.status"
:active-value="1"
:inactive-value="-1"
active-color="#13ce66"
inactive-color="#ff4949"
inline-prompt
active-text="启用"
inactive-text="停用"
@change="changeStatus(scope.row)"
></el-switch>
<el-switch v-auth:disabled="'machine:update'" :width="52" v-model="scope.row.status"
:active-value="1" :inactive-value="-1" inline-prompt active-text="启用" inactive-text="停用"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="changeStatus(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="90"></el-table-column>
<el-table-column prop="projectName" label="项目" min-width="120"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="250" show-overflow-tooltip></el-table-column>
<el-table-column prop="ip" label="hasCli" width="70">
<el-table-column label="操作" min-width="235" fixed="right">
<template #default="scope">
{{ `${scope.row.hasCli ? '是' : '否'}` }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="165">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者" min-width="80"></el-table-column>
<el-table-column label="操作" min-width="280" fixed="right">
<template #default="scope">
<el-link
v-auth="'machine:terminal'"
:disabled="scope.row.status == -1"
type="primary"
@click="showTerminal(scope.row)"
plain
size="small"
:underline="false"
>终端</el-link
>
<el-divider v-auth="'machine:terminal'" direction="vertical" border-style="dashed" />
<el-link
v-auth="'machine:file'"
type="success"
:disabled="scope.row.status == -1"
@click="fileManage(scope.row)"
plain
size="small"
:underline="false"
>文件</el-link
>
<el-divider v-auth="'machine:file'" direction="vertical" border-style="dashed" />
<el-link
:disabled="scope.row.status == -1"
type="warning"
@click="serviceManager(scope.row)"
plain
size="small"
:underline="false"
>脚本</el-link
>
<span v-auth="'machine:terminal'">
<el-link :disabled="scope.row.status == -1" type="primary" @click="showTerminal(scope.row)"
plain size="small" :underline="false">终端</el-link>
<el-divider direction="vertical" border-style="dashed" />
</span>
<span v-auth="'machine:file'">
<el-link type="success" :disabled="scope.row.status == -1" @click="showFileManage(scope.row)"
plain size="small" :underline="false">文件</el-link>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-link :disabled="scope.row.status == -1" type="warning" @click="serviceManager(scope.row)" plain
size="small" :underline="false">脚本</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1" plain :underline="false" size="small"
>进程</el-link
>
<el-divider direction="vertical" border-style="dashed" />
<el-link
:disabled="!scope.row.hasCli || scope.row.status == -1"
type="danger"
@click="closeCli(scope.row)"
plain
size="small"
:underline="false"
>关闭连接</el-link
>
<el-dropdown>
<span class="el-dropdown-link-machine-list">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-link @click="showInfo(scope.row)" plain :underline="false" size="small">详情
</el-link>
</el-dropdown-item>
<el-dropdown-item>
<el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1" plain
:underline="false" size="small">进程</el-link>
</el-dropdown-item>
<el-dropdown-item v-if="scope.row.enableRecorder == 1">
<el-link v-auth="'machine:update'" @click="showRec(scope.row)" plain
:underline="false" size="small">终端回放</el-link>
</el-dropdown-item>
<el-dropdown-item>
<el-link :disabled="!scope.row.hasCli || scope.row.status == -1" type="danger"
@click="closeCli(scope.row)" plain size="small" :underline="false">关闭连接
</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination
style="text-align: right"
:total="data.total"
layout="prev, pager, next, total, jumper"
v-model:current-page="params.pageNum"
:page-size="params.pageSize"
@current-change="handlePageChange"
></el-pagination>
<el-pagination style="text-align: right" :total="data.total" layout="prev, pager, next, total, jumper"
v-model:current-page="params.pageNum" :page-size="params.pageSize"
@current-change="handlePageChange"></el-pagination>
</el-row>
</el-card>
<machine-edit
:title="machineEditDialog.title"
:projects="projects"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
@valChange="submitSuccess"
></machine-edit>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<machine-edit :title="machineEditDialog.title" v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data" @valChange="submitSuccess"></machine-edit>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<service-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<service-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible"
v-model:machineId="serviceDialog.machineId" />
<file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible"
v-model:machineId="fileDialog.machineId" />
<machine-stats
v-model:visible="machineStatsDialog.visible"
:machineId="machineStatsDialog.machineId"
:title="machineStatsDialog.title"
></machine-stats>
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId"
:title="machineStatsDialog.title"></machine-stats>
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId"
:title="machineRecDialog.title"></machine-rec>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { machineApi } from './api';
import { projectApi } from '../project/api.ts';
import { tagApi } from '../tag/api.ts';
import ServiceManage from './ServiceManage.vue';
import FileManage from './FileManage.vue';
import MachineEdit from './MachineEdit.vue';
import ProcessList from './ProcessList.vue';
import MachineStats from './MachineStats.vue';
import MachineRec from './MachineRec.vue';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
export default defineComponent({
name: 'MachineList',
components: {
ServiceManage,
ProcessList,
FileManage,
MachineEdit,
MachineStats,
const router = useRouter();
const state = reactive({
tags: [] as any,
params: {
pageNum: 1,
pageSize: 10,
ip: null,
name: null,
tagPath: null,
},
setup() {
const router = useRouter();
const state = reactive({
projects: [],
stats: '',
params: {
pageNum: 1,
pageSize: 10,
ip: null,
name: null,
},
// 列表数据
data: {
list: [],
total: 10,
},
// 当前选中数据id
currentId: null,
currentData: null,
serviceDialog: {
visible: false,
machineId: 0,
title: '',
},
processDialog: {
visible: false,
machineId: 0,
},
fileDialog: {
visible: false,
machineId: 0,
title: '',
},
machineStatsDialog: {
visible: false,
stats: null,
title: '',
machineId: 0,
},
machineEditDialog: {
visible: false,
data: null,
title: '新增机器',
},
});
onMounted(async () => {
search();
state.projects = await projectApi.accountProjects.request(null);
});
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
const showTerminal = (row: any) => {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: row.id,
name: row.name,
},
});
window.open(href, '_blank');
};
const closeCli = async (row: any) => {
await machineApi.closeCli.request({ id: row.id });
ElMessage.success('关闭成功');
search();
};
const openFormDialog = (redis: any) => {
let dialogTitle;
if (redis) {
state.machineEditDialog.data = state.currentData as any;
dialogTitle = '编辑机器';
} else {
state.machineEditDialog.data = { port: 22 } as any;
dialogTitle = '添加机器';
}
state.machineEditDialog.title = dialogTitle;
state.machineEditDialog.visible = true;
};
const deleteMachine = async (id: number) => {
try {
await ElMessageBox.confirm(`确定删除该机器信息? 该操作将同时删除脚本及文件配置信息`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.del.request({ id });
ElMessage.success('操作成功');
state.currentId = null;
state.currentData = null;
search();
} catch (err) {}
};
const serviceManager = (row: any) => {
state.serviceDialog.machineId = row.id;
state.serviceDialog.visible = true;
state.serviceDialog.title = `${row.name} => ${row.ip}`;
};
/**
* 调整机器状态
*/
const changeStatus = async (row: any) => {
await machineApi.changeStatus.request({ id: row.id, status: row.status });
};
/**
* 显示机器状态统计信息
*/
const showMachineStats = async (machine: any) => {
state.machineStatsDialog.machineId = machine.id;
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
state.machineStatsDialog.visible = true;
};
const submitSuccess = () => {
state.currentId = null;
state.currentData = null;
search();
};
const fileManage = (currentData: any) => {
state.fileDialog.visible = true;
state.fileDialog.machineId = currentData.id;
state.fileDialog.title = `${currentData.name} => ${currentData.ip}`;
};
const search = async () => {
const res = await machineApi.list.request(state.params);
state.data = res;
};
const handlePageChange = (curPage: number) => {
state.params.pageNum = curPage;
search();
};
const showProcess = (row: any) => {
state.processDialog.machineId = row.id;
state.processDialog.visible = true;
};
return {
...toRefs(state),
choose,
showTerminal,
openFormDialog,
deleteMachine,
closeCli,
serviceManager,
showMachineStats,
showProcess,
changeStatus,
submitSuccess,
fileManage,
search,
handlePageChange,
};
// 列表数据
data: {
list: [],
total: 10,
},
infoDialog: {
visible: false,
data: null as any,
},
// 当前选中数据id
currentId: 0,
currentData: null,
serviceDialog: {
visible: false,
machineId: 0,
title: '',
},
processDialog: {
visible: false,
machineId: 0,
},
fileDialog: {
visible: false,
machineId: 0,
title: '',
},
machineStatsDialog: {
visible: false,
stats: null,
title: '',
machineId: 0,
},
machineEditDialog: {
visible: false,
data: null as any,
title: '新增机器',
},
machineRecDialog: {
visible: false,
machineId: 0,
title: '',
},
});
const {
tags,
params,
data,
infoDialog,
currentId,
currentData,
serviceDialog,
processDialog,
fileDialog,
machineStatsDialog,
machineEditDialog,
machineRecDialog,
} = toRefs(state)
onMounted(async () => {
search();
});
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
const showTerminal = (row: any) => {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: row.id,
name: row.name,
},
});
window.open(href, '_blank');
};
const closeCli = async (row: any) => {
await ElMessageBox.confirm(`确定关闭该机器客户端连接?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.closeCli.request({ id: row.id });
ElMessage.success('关闭成功');
search();
};
const getTags = async () => {
state.tags = await tagApi.getAccountTags.request(null);
};
const openFormDialog = async (machine: any) => {
let dialogTitle;
if (machine) {
state.machineEditDialog.data = state.currentData as any;
dialogTitle = '编辑机器';
} else {
state.machineEditDialog.data = null;
dialogTitle = '添加机器';
}
state.machineEditDialog.title = dialogTitle;
state.machineEditDialog.visible = true;
};
const deleteMachine = async (id: number) => {
try {
await ElMessageBox.confirm(`确定删除该机器信息? 该操作将同时删除脚本及文件配置信息`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.del.request({ id });
ElMessage.success('操作成功');
state.currentId = 0;
state.currentData = null;
search();
} catch (err) { }
};
const serviceManager = (row: any) => {
state.serviceDialog.machineId = row.id;
state.serviceDialog.visible = true;
state.serviceDialog.title = `${row.name} => ${row.ip}`;
};
/**
* 调整机器状态
*/
const changeStatus = async (row: any) => {
await machineApi.changeStatus.request({ id: row.id, status: row.status });
};
/**
* 显示机器状态统计信息
*/
const showMachineStats = async (machine: any) => {
state.machineStatsDialog.machineId = machine.id;
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
state.machineStatsDialog.visible = true;
};
const submitSuccess = () => {
state.currentId = 0;
state.currentData = null;
search();
};
const showFileManage = (currentData: any) => {
state.fileDialog.visible = true;
state.fileDialog.machineId = currentData.id;
state.fileDialog.title = `${currentData.name} => ${currentData.ip}`;
};
const search = async () => {
const res = await machineApi.list.request(state.params);
state.data = res;
};
const handlePageChange = (curPage: number) => {
state.params.pageNum = curPage;
search();
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;
}
const showProcess = (row: any) => {
state.processDialog.machineId = row.id;
state.processDialog.visible = true;
};
const showRec = (row: any) => {
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
state.machineRecDialog.machineId = row.id;
state.machineRecDialog.visible = true;
};
</script>
<style>
.el-dialog__body {
padding: 2px 2px;
}
.el-dropdown-link-machine-list {
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
margin-top: 6px;
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div id="terminalRecDialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="handleClose" :close-on-click-modal="false"
:destroy-on-close="true" width="70%">
<div class="toolbar">
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="getRecs" filterable v-model="user" placeholder="请选择操作人">
<el-option v-for="item in users" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="playRec" filterable v-model="rec" placeholder="请选择操作记录">
<el-option v-for="item in recs" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-divider direction="vertical" border-style="dashed" />
快捷键-> space[空格键]: 暂停/播放
</div>
<div ref="playerRef" id="rc-player"></div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, watch, ref, reactive } from 'vue';
import { machineApi } from './api';
import * as AsciinemaPlayer from 'asciinema-player';
import 'asciinema-player/dist/bundle/asciinema-player.css';
const props = defineProps({
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
})
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId'])
const playerRef = ref(null);
const state = reactive({
dialogVisible: false,
title: '',
machineId: 0,
operateDates: [],
users: [],
recs: [],
operateDate: '',
user: '',
rec: '',
});
const {
dialogVisible,
title,
operateDates,
operateDate,
users,
recs,
user,
rec,
} = toRefs(state)
watch(props, async (newValue: any) => {
const visible = newValue.visible;
if (visible) {
state.machineId = newValue.machineId;
state.title = newValue.title;
await getOperateDate();
}
state.dialogVisible = visible;
});
const getOperateDate = async () => {
const res = await machineApi.recDirNames.request({ path: state.machineId });
state.operateDates = res as any;
};
const getUsers = async (operateDate: string) => {
state.users = [];
state.user = '';
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${operateDate}` });
state.users = res as any;
};
const getRecs = async (user: string) => {
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${state.operateDate}/${user}` });
state.recs = res as any;
};
let player: any = null;
const playRec = async (rec: string) => {
if (player) {
player.dispose();
}
const content = await machineApi.recDirNames.request({
isFile: '1',
path: `${state.machineId}/${state.operateDate}/${state.user}/${rec}`,
});
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,
});
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
emit('update:visible', false);
emit('update:machineId', null);
emit('cancel');
state.operateDates = [];
state.users = [];
state.recs = [];
state.operateDate = '';
state.user = '';
state.rec = '';
};
</script>
<style lang="scss">
#terminalRecDialog {
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
padding: 0px !important;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true"
:before-close="cancel" width="1050px">
<el-row :gutter="20">
<el-col :lg="12" :md="12">
<el-descriptions size="small" title="基础信息" :column="2" border>
@@ -19,7 +20,8 @@
<el-descriptions-item label="运行中任务">
{{ stats.RunningProcs }}
</el-descriptions-item>
<el-descriptions-item label="负载"> {{ stats.Load1 }} {{ stats.Load5 }} {{ stats.Load10 }} </el-descriptions-item>
<el-descriptions-item label="负载"> {{ stats.Load1 }} {{ stats.Load5 }} {{ stats.Load10 }}
</el-descriptions-item>
</el-descriptions>
</el-col>
@@ -36,7 +38,8 @@
<el-col :lg="8" :md="8">
<span style="font-size: 16px; font-weight: 700">磁盘</span>
<el-table :data="stats.FSInfos" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="MountPoint" label="挂载点" min-width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="MountPoint" label="挂载点" min-width="100" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="Used" label="可使用" min-width="70" show-overflow-tooltip>
<template #default="scope">
{{ formatByteSize(scope.row.Free) }}
@@ -54,8 +57,10 @@
<span style="font-size: 16px; font-weight: 700">网卡</span>
<el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="name" label="网卡" min-width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="IPv4" label="IPv4" min-width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="IPv6" label="IPv6" min-width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="IPv4" label="IPv4" min-width="130" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="IPv6" label="IPv6" min-width="130" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="Rx" label="接收(rx)" min-width="110" show-overflow-tooltip>
<template #default="scope">
{{ formatByteSize(scope.row.Rx) }}
@@ -73,245 +78,240 @@
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref, nextTick } from 'vue';
<script lang="ts" setup>
import { toRefs, reactive, watch, ref, nextTick } from 'vue';
import useEcharts from '@/common/echarts/useEcharts.ts';
import tdTheme from '@/common/echarts/theme.json';
import { formatByteSize } from '@/common/utils/format';
import { machineApi } from './api';
export default defineComponent({
name: 'MachineStats',
components: {},
props: {
visible: {
type: Boolean,
},
stats: {
type: Object,
},
machineId: {
type: Number,
},
title: {
type: String,
},
const props = defineProps({
visible: {
type: Boolean,
},
setup(props: any, { emit }) {
const cpuRef: any = ref();
const memRef: any = ref();
let cpuChart: any = null;
let memChart: any = null;
const state = reactive({
dialogVisible: false,
charts: [] as any,
stats: {} as any,
netInter: [] as any,
});
watch(props, async (newValue) => {
const visible = newValue.visible;
if (visible) {
await setStats();
}
state.dialogVisible = visible;
if (visible) {
initCharts();
}
});
const setStats = async () => {
state.stats = await machineApi.stats.request({ id: props.machineId });
};
const onRefresh = async () => {
await setStats();
initCharts();
};
const initMemStats = () => {
const data = [
{ name: '可用内存', value: state.stats.MemAvailable },
{
name: '已用内存',
value: state.stats.MemTotal - state.stats.MemAvailable,
},
];
const option = {
title: {
text: '内存',
x: 'left',
textStyle: { fontSize: 15 },
},
tooltip: {
trigger: 'item',
valueFormatter: formatByteSize,
},
legend: {
top: '15%',
orient: 'vertical',
left: 'left',
textStyle: { fontSize: 12 },
},
series: [
{
name: '内存',
type: 'pie',
radius: ['30%', '60%'], // 饼图内圈和外圈大小
center: ['60%', '50%'], // 饼图位置0: 左右1: 上下
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '15',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data,
},
],
};
if (memChart) {
memChart.setOption(option, true);
return;
}
const chart: any = useEcharts(memRef.value, tdTheme, option);
memChart = chart;
state.charts.push(chart);
};
const initCpuStats = () => {
const cpu = state.stats.CPU;
const data = [
{ name: 'Idle', value: cpu.Idle },
{
name: 'Iowait',
value: cpu.Iowait,
},
{
name: 'System',
value: cpu.System,
},
{
name: 'User',
value: cpu.User,
},
];
const option = {
title: {
text: 'CPU使用率',
x: 'left',
textStyle: { fontSize: 15 },
},
tooltip: {
trigger: 'item',
valueFormatter: (value: any) => value + '%',
},
legend: {
top: '15%',
orient: 'vertical',
left: 'left',
textStyle: { fontSize: 12 },
},
series: [
{
name: 'CPU',
type: 'pie',
radius: ['30%', '60%'], // 饼图内圈和外圈大小
center: ['60%', '50%'], // 饼图位置0: 左右1: 上下
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '15',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data,
},
],
};
if (cpuChart) {
cpuChart.setOption(option, true);
return;
}
const chart: any = useEcharts(cpuRef.value, tdTheme, option);
cpuChart = chart;
state.charts.push(chart);
};
const initCharts = () => {
nextTick(() => {
initMemStats();
initCpuStats();
});
parseNetInter();
initEchartsResize();
};
const initEchartResizeFun = () => {
nextTick(() => {
for (let i = 0; i < state.charts.length; i++) {
setTimeout(() => {
state.charts[i].resize();
}, i * 1000);
}
});
};
const initEchartsResize = () => {
window.addEventListener('resize', initEchartResizeFun);
};
const parseNetInter = () => {
state.netInter = [];
const netInter = state.stats.NetIntf;
const keys = Object.keys(netInter);
const values = Object.values(netInter);
for (let i = 0; i < values.length; i++) {
let value: any = values[i];
// 将网卡名称赋值新属性值name
value.name = keys[i];
state.netInter.push(value);
}
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
cpuChart = null;
memChart = null;
}, 200);
};
return {
...toRefs(state),
cpuRef,
memRef,
cancel,
formatByteSize,
onRefresh,
};
stats: {
type: Object,
},
machineId: {
type: Number,
},
title: {
type: String,
},
})
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId'])
const cpuRef: any = ref();
const memRef: any = ref();
let cpuChart: any = null;
let memChart: any = null;
const state = reactive({
dialogVisible: false,
stats: {} as any,
netInter: [] as any,
});
const {
dialogVisible,
stats,
netInter,
} = toRefs(state)
let charts = [] as any
watch(props, async (newValue: any) => {
const visible = newValue.visible;
if (visible) {
await setStats();
}
state.dialogVisible = visible;
if (visible) {
initCharts();
}
});
const setStats = async () => {
state.stats = await machineApi.stats.request({ id: props.machineId });
};
const onRefresh = async () => {
await setStats();
initCharts();
};
const initMemStats = () => {
const data = [
{ name: '可用内存', value: state.stats.MemAvailable },
{
name: '已用内存',
value: state.stats.MemTotal - state.stats.MemAvailable,
},
];
const option = {
title: {
text: '内存',
x: 'left',
textStyle: { fontSize: 15 },
},
tooltip: {
trigger: 'item',
valueFormatter: formatByteSize,
},
legend: {
top: '15%',
orient: 'vertical',
left: 'left',
textStyle: { fontSize: 12 },
},
series: [
{
name: '内存',
type: 'pie',
radius: ['30%', '60%'], // 饼图内圈和外圈大小
center: ['60%', '50%'], // 饼图位置0: 左右1: 上下
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '15',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data,
},
],
};
if (memChart) {
memChart.setOption(option, true);
return;
}
const chart: any = useEcharts(memRef.value, tdTheme, option);
memChart = chart;
charts.push(chart);
};
const initCpuStats = () => {
const cpu = state.stats.CPU;
const data = [
{ name: 'Idle', value: cpu.Idle },
{
name: 'Iowait',
value: cpu.Iowait,
},
{
name: 'System',
value: cpu.System,
},
{
name: 'User',
value: cpu.User,
},
];
const option = {
title: {
text: 'CPU使用率',
x: 'left',
textStyle: { fontSize: 15 },
},
tooltip: {
trigger: 'item',
valueFormatter: (value: any) => value + '%',
},
legend: {
top: '15%',
orient: 'vertical',
left: 'left',
textStyle: { fontSize: 12 },
},
series: [
{
name: 'CPU',
type: 'pie',
radius: ['30%', '60%'], // 饼图内圈和外圈大小
center: ['60%', '50%'], // 饼图位置0: 左右1: 上下
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '15',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data,
},
],
};
if (cpuChart) {
cpuChart.setOption(option, true);
return;
}
const chart: any = useEcharts(cpuRef.value, tdTheme, option);
cpuChart = chart;
charts.push(chart);
};
const initCharts = () => {
nextTick(() => {
initMemStats();
initCpuStats();
});
parseNetInter();
initEchartsResize();
};
const initEchartResizeFun = () => {
nextTick(() => {
for (let i = 0; i < charts.length; i++) {
setTimeout(() => {
charts[i].resize();
}, i * 1000);
}
});
};
const initEchartsResize = () => {
window.addEventListener('resize', initEchartResizeFun);
};
const parseNetInter = () => {
state.netInter = [];
const netInter = state.stats.NetIntf;
const keys = Object.keys(netInter);
const values = Object.values(netInter);
for (let i = 0; i < values.length; i++) {
let value: any = values[i];
// 将网卡名称赋值新属性值name
value.name = keys[i];
state.netInter.push(value);
}
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
cpuChart = null;
memChart = null;
}, 200);
};
</script>
<style lang="scss">
.card-item-chart {

View File

@@ -1,6 +1,7 @@
<template>
<div class="file-manage">
<el-dialog title="进程信息" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" :before-close="handleClose" width="65%">
<el-dialog title="进程信息" v-model="dialogVisible" :destroy-on-close="true" :show-close="true"
:before-close="handleClose" width="65%">
<div class="toolbar">
<el-row>
<el-col :span="4">
@@ -21,7 +22,8 @@
</el-select>
</el-col>
<el-col :span="6">
<el-button class="ml5" @click="getProcess" type="primary" icon="tickets" size="small" plain>刷新</el-button>
<el-button class="ml5" @click="getProcess" type="primary" icon="tickets" size="small" plain>刷新
</el-button>
</el-col>
</el-row>
</div>
@@ -35,7 +37,9 @@
<template #header>
VSZ
<el-tooltip class="box-item" effect="dark" content="虚拟内存" placement="top">
<el-icon><question-filled /></el-icon>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
@@ -43,7 +47,9 @@
<template #header>
RSS
<el-tooltip class="box-item" effect="dark" content="固定内存" placement="top">
<el-icon><question-filled /></el-icon>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
@@ -51,7 +57,9 @@
<template #header>
STAT
<el-tooltip class="box-item" effect="dark" content="进程状态" placement="top">
<el-icon><question-filled /></el-icon>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
@@ -59,7 +67,9 @@
<template #header>
START
<el-tooltip class="box-item" effect="dark" content="启动时间" placement="top">
<el-icon><question-filled /></el-icon>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
@@ -67,17 +77,21 @@
<template #header>
TIME
<el-tooltip class="box-item" effect="dark" content="该进程实际使用CPU运作的时间" placement="top">
<el-icon><question-filled /></el-icon>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="command" label="command" :min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="command" label="command" :min-width="120" show-overflow-tooltip>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)">
<template #reference>
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" plain>终止</el-button>
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small"
plain>终止</el-button>
</template>
</el-popconfirm>
<!-- <el-button @click="addFiles(scope.row)" type="danger" icon="delete" size="small" plain>终止</el-button> -->
@@ -88,118 +102,114 @@
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent } from 'vue';
<script lang="ts" setup>
import { toRefs, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { machineApi } from './api';
import enums from './enums';
export default defineComponent({
name: 'ProcessList',
components: {},
props: {
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
},
setup(props: any, context) {
const state = reactive({
dialogVisible: false,
params: {
name: '',
sortType: '1',
count: '10',
id: 0,
},
processList: [],
});
watch(props, (newValue) => {
if (props.machineId) {
state.params.id = props.machineId;
getProcess();
}
state.dialogVisible = newValue.visible;
});
const getProcess = async () => {
const res = await machineApi.process.request(state.params);
// 解析字符串
// USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
// root 1 0.0 0.0 125632 3352 ? Ss 2019 154:04 /usr/lib/systemd/systemd --system --deserialize 22
const psStrings = res.split('\n');
const ps = [];
// 如果有根据名称查进程,则第一行没有表头
const index = state.params.name == '' ? 1 : 0;
for (let i = index; i < psStrings.length; i++) {
const psStr = psStrings[i];
const process = psStr.split(/\s+/);
if (process.length < 2) {
continue;
}
let command = process[10];
// 搜索进程时由于使用grep命令可能会多个bash或grep进程
if (state.params.name) {
if (command == 'bash' || command == 'grep') {
continue;
}
}
// 获取command由于command中也有可能存在空格被切割故重新拼接
for (let j = 10; j < process.length - 1; j++) {
command += ' ' + process[j + 1];
}
ps.push({
user: process[0],
pid: process[1],
cpu: process[2],
mem: process[3],
vsz: kb2Mb(process[4]),
rss: kb2Mb(process[5]),
stat: process[7],
start: process[8],
time: process[9],
command,
});
}
state.processList = ps as any;
};
const confirmKillProcess = async (pid: any) => {
await machineApi.killProcess.request({
pid,
id: state.params.id,
});
ElMessage.success('kill success');
state.params.name = '';
getProcess();
};
const kb2Mb = (kb: string) => {
return (parseInt(kb) / 1024).toFixed(2) + 'M';
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
context.emit('update:visible', false);
context.emit('update:machineId', null);
context.emit('cancel');
state.params = {
name: '',
sortType: '1',
count: '10',
id: 0,
};
state.processList = [];
};
return {
...toRefs(state),
getProcess,
confirmKillProcess,
enums,
handleClose,
};
const props = defineProps({
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
})
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId'])
const state = reactive({
dialogVisible: false,
params: {
name: '',
sortType: '1',
count: '10',
id: 0,
},
processList: [],
});
const {
dialogVisible,
params,
processList,
} = toRefs(state)
watch(props, (newValue) => {
if (props.machineId) {
state.params.id = props.machineId;
getProcess();
}
state.dialogVisible = newValue.visible;
});
const getProcess = async () => {
const res = await machineApi.process.request(state.params);
// 解析字符串
// USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
// root 1 0.0 0.0 125632 3352 ? Ss 2019 154:04 /usr/lib/systemd/systemd --system --deserialize 22
const psStrings = res.split('\n');
const ps = [];
// 如果有根据名称查进程,则第一行没有表头
const index = state.params.name == '' ? 1 : 0;
for (let i = index; i < psStrings.length; i++) {
const psStr = psStrings[i];
const process = psStr.split(/\s+/);
if (process.length < 2) {
continue;
}
let command = process[10];
// 搜索进程时由于使用grep命令可能会多个bash或grep进程
if (state.params.name) {
if (command == 'bash' || command == 'grep') {
continue;
}
}
// 获取command由于command中也有可能存在空格被切割故重新拼接
for (let j = 10; j < process.length - 1; j++) {
command += ' ' + process[j + 1];
}
ps.push({
user: process[0],
pid: process[1],
cpu: process[2],
mem: process[3],
vsz: kb2Mb(process[4]),
rss: kb2Mb(process[5]),
stat: process[7],
start: process[8],
time: process[9],
command,
});
}
state.processList = ps as any;
};
const confirmKillProcess = async (pid: any) => {
await machineApi.killProcess.request({
pid,
id: state.params.id,
});
ElMessage.success('kill success');
state.params.name = '';
getProcess();
};
const kb2Mb = (kb: string) => {
return (parseInt(kb) / 1024).toFixed(2) + 'M';
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
emit('update:visible', false);
emit('update:machineId', null);
emit('cancel');
state.params = {
name: '',
sortType: '1',
count: '10',
id: 0,
};
state.processList = [];
};
</script>

View File

@@ -1,15 +1,8 @@
<template>
<div class="mock-data-dialog">
<el-dialog
:title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:before-close="cancel"
:show-close="true"
:destroy-on-close="true"
width="800px"
>
<el-form :model="form" ref="mockDataForm" label-width="70px">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :before-close="cancel"
:show-close="true" :destroy-on-close="true" width="900px">
<el-form :model="form" ref="scriptForm" label-width="50px" size="small">
<el-form-item prop="method" label="名称">
<el-input v-model.trim="form.name" placeholder="请输入名称"></el-input>
</el-form-item>
@@ -20,146 +13,171 @@
<el-form-item prop="type" label="类型">
<el-select v-model="form.type" default-first-option style="width: 100%" placeholder="请选择类型">
<el-option v-for="item in enums.scriptTypeEnum" :key="item.value" :label="item.label" :value="item.value"></el-option>
<el-option v-for="item in enums.scriptTypeEnum as any" :key="item.value" :label="item.label"
:value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="params" label="参数">
<el-input v-model.trim="form.params" placeholder="参数数组json若无可不填"></el-input>
<el-row style="margin-left: 30px; margin-bottom: 5px">
<el-button @click="onAddParam" size="small" type="success">新增占位符参数</el-button>
</el-row>
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
<el-row>
<el-col :span="5">
<el-input v-model="param.model" placeholder="内容中用{{.model}}替换"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.name" placeholder="字段名"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.placeholder" placeholder="字段说明"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="2">
<el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="script" label="内容" id="content">
<codemirror ref="cmEditor" v-model="form.script" language="shell" width="700px" />
</el-form-item>
<monaco-editor v-model="form.script" language="shell" height="300px" />
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()" :disabled="submitDisabled" size="small"> </el-button>
<el-button
v-auth="'machine:script:save'"
type="primary"
:loading="btnLoading"
@click="btnOk"
size="small"
:disabled="submitDisabled"
> </el-button
>
<el-button @click="cancel()" :disabled="submitDisabled"> </el-button>
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk"
:disabled="submitDisabled"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { machineApi } from './api';
import enums from './enums';
import { notEmpty } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { codemirror } from '@/components/codemirror';
export default defineComponent({
name: 'ScriptEdit',
components: {
codemirror,
const props = defineProps({
visible: {
type: Boolean,
},
props: {
visible: {
type: Boolean,
},
data: {
type: Object,
},
title: {
type: String,
},
machineId: {
type: Number,
},
isCommon: {
type: Boolean,
},
data: {
type: Object,
},
setup(props: any, { emit }) {
const { isCommon, machineId } = toRefs(props);
const mockDataForm: any = ref(null);
const state = reactive({
dialogVisible: false,
submitDisabled: false,
form: {
id: null,
name: '',
machineId: 0,
description: '',
script: '',
params: null,
type: null,
},
btnLoading: false,
});
watch(props, (newValue) => {
if (newValue.data) {
state.form = { ...newValue.data };
} else {
state.form = {} as any;
state.form.script = '';
}
state.dialogVisible = newValue.visible;
});
const btnOk = () => {
state.form.machineId = isCommon.value ? 9999999 : (machineId.value as any);
console.log('machineid:', machineId);
mockDataForm.value.validate((valid: any) => {
if (valid) {
notEmpty(state.form.name, '名称不能为空');
notEmpty(state.form.description, '描述不能为空');
notEmpty(state.form.script, '内容不能为空');
machineApi.saveScript.request(state.form).then(
() => {
ElMessage.success('保存成功');
emit('submitSuccess');
state.submitDisabled = false;
cancel();
},
() => {
state.submitDisabled = false;
}
);
} else {
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
return {
...toRefs(state),
enums,
mockDataForm,
btnOk,
cancel,
};
title: {
type: String,
},
machineId: {
type: Number,
},
isCommon: {
type: Boolean,
},
})
const emit = defineEmits(['update:visible', 'cancel', 'submitSuccess'])
const { isCommon, machineId } = toRefs(props);
const scriptForm: any = ref(null);
const state = reactive({
dialogVisible: false,
submitDisabled: false,
params: [] as any,
form: {
id: null,
name: '',
machineId: 0,
description: '',
script: '',
params: '',
type: null,
},
btnLoading: false,
});
</script>
<style lang="scss">
// .m-dialog {
// .el-cascader {
// width: 100%;
// }
// }
#content {
.CodeMirror {
height: 300px !important;
const {
dialogVisible,
submitDisabled,
params,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!newValue.visible) {
return;
}
}
</style>
if (newValue.data) {
state.form = { ...newValue.data };
if (state.form.params) {
state.params = JSON.parse(state.form.params);
}
} else {
state.form = {} as any;
state.form.script = '';
}
});
const onAddParam = () => {
state.params.push({ name: '', model: '', placeholder: '' });
};
const onDeleteParam = (idx: number) => {
state.params.splice(idx, 1);
};
const btnOk = () => {
state.form.machineId = isCommon.value ? 9999999 : (machineId?.value as any);
console.log('machineid:', machineId);
scriptForm.value.validate((valid: any) => {
if (valid) {
notEmpty(state.form.name, '名称不能为空');
notEmpty(state.form.description, '描述不能为空');
notEmpty(state.form.script, '内容不能为空');
if (state.params) {
state.form.params = JSON.stringify(state.params);
}
machineApi.saveScript.request(state.form).then(
() => {
ElMessage.success('保存成功');
emit('submitSuccess');
state.submitDisabled = false;
cancel();
},
() => {
state.submitDisabled = false;
}
);
} else {
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
state.params = [];
};
</script>
<style lang="scss"></style>

View File

@@ -1,6 +1,7 @@
<template>
<div class="file-manage">
<el-dialog :title="title" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" :before-close="handleClose" width="60%">
<el-dialog :title="title" v-model="dialogVisible" :destroy-on-close="true" :show-close="true"
:before-close="handleClose" width="60%">
<div class="toolbar">
<div style="float: left">
<el-select v-model="type" @change="getScripts" size="small" placeholder="请选择">
@@ -9,20 +10,12 @@
</el-select>
</div>
<div style="float: right">
<el-button @click="editScript(currentData)" :disabled="currentId == null" type="primary" icon="tickets" size="small" plain
>查看</el-button
>
<el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus" size="small" plain>添加</el-button>
<el-button
v-auth="'machine:script:del'"
:disabled="currentId == null"
type="danger"
@click="deleteRow(currentData)"
icon="delete"
size="small"
plain
>删除</el-button
>
<el-button @click="editScript(currentData)" :disabled="currentId == null" type="primary"
icon="tickets" size="small" plain>查看</el-button>
<el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus"
size="small" plain>添加</el-button>
<el-button v-auth="'machine:script:del'" :disabled="currentId == null" type="danger"
@click="deleteRow(currentData)" icon="delete" size="small" plain>删除</el-button>
</div>
</div>
@@ -43,40 +36,33 @@
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="el-icon-success" size="small" plain
>确定</el-button
>
<el-button v-if="scope.row.id == null" type="success" icon="el-icon-success" size="small" plain>
确定</el-button>
<el-button
v-auth="'machine:script:run'"
v-if="scope.row.id != null"
@click="runScript(scope.row)"
type="primary"
icon="video-play"
size="small"
plain
>执行</el-button
>
<el-button v-auth="'machine:script:run'" v-if="scope.row.id != null"
@click="runScript(scope.row)" type="primary" icon="video-play" size="small" plain>执行
</el-button>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 10px" type="flex" justify="end">
<el-pagination
small
style="text-align: center"
:total="total"
layout="prev, pager, next, total, jumper"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
@current-change="handlePageChange"
></el-pagination>
<el-pagination small style="text-align: center" :total="total" layout="prev, pager, next, total, jumper"
v-model:current-page="query.pageNum" :page-size="query.pageSize" @current-change="handlePageChange">
</el-pagination>
</el-row>
</el-dialog>
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="small">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" autocomplete="off"></el-input>
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name"
:prop="item.model" :label="item.name" required>
<el-input v-if="!item.options" v-model="scriptParamsDialog.params[item.model]"
:placeholder="item.placeholder" autocomplete="off" clearable></el-input>
<el-select v-else v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder"
filterable autocomplete="off" clearable style="width: 100%">
<el-option v-for="option in item.options.split(',')" :key="option" :label="option"
:value="option" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
@@ -88,263 +74,241 @@
<el-dialog title="执行结果" v-model="resultDialog.visible" width="50%">
<div style="white-space: pre-line; padding: 10px; color: #000000">
<!-- {{ resultDialog.result }} -->
<el-input v-model="resultDialog.result" :rows="20" type="textarea" />
</div>
</el-dialog>
<el-dialog
v-if="terminalDialog.visible"
title="终端"
v-model="terminalDialog.visible"
width="70%"
:close-on-click-modal="false"
:modal="false"
@close="closeTermnial"
>
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="600px" />
<el-dialog v-if="terminalDialog.visible" title="终端" v-model="terminalDialog.visible" width="80%"
:close-on-click-modal="false" :modal="false" @close="closeTermnial">
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId"
height="560px" />
</el-dialog>
<script-edit
v-model:visible="editDialog.visible"
v-model:data="editDialog.data"
:title="editDialog.title"
v-model:machineId="editDialog.machineId"
:isCommon="type == 1"
@submitSuccess="submitSuccess"
/>
<script-edit v-model:visible="editDialog.visible" v-model:data="editDialog.data" :title="editDialog.title"
v-model:machineId="editDialog.machineId" :isCommon="type == 1" @submitSuccess="submitSuccess" />
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import SshTerminal from './SshTerminal.vue';
import { machineApi } from './api';
import enums from './enums';
import ScriptEdit from './ScriptEdit.vue';
export default defineComponent({
name: 'ServiceManage',
components: {
ScriptEdit,
SshTerminal,
const props = defineProps({
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
})
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId'])
const paramsForm: any = ref(null);
const state = reactive({
dialogVisible: false,
type: 0,
currentId: null,
currentData: null,
query: {
machineId: 0 as any,
pageNum: 1,
pageSize: 8,
},
props: {
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
editDialog: {
visible: false,
data: null as any,
title: '',
machineId: 9999999,
},
setup(props: any, context) {
const paramsForm: any = ref(null);
const state = reactive({
dialogVisible: false,
type: 0,
currentId: null,
currentData: null,
query: {
machineId: 0,
pageNum: 1,
pageSize: 8,
},
editDialog: {
visible: false,
data: null,
title: '',
machineId: 9999999,
},
total: 0,
scriptTable: [],
scriptParamsDialog: {
visible: false,
params: {},
paramsFormItem: [],
},
resultDialog: {
visible: false,
result: '',
},
terminalDialog: {
visible: false,
cmd: '',
machineId: 0,
},
});
watch(props, (newValue) => {
if (props.machineId) {
getScripts();
}
state.dialogVisible = newValue.visible;
});
const getScripts = async () => {
state.currentId = null;
state.currentData = null;
state.query.machineId = state.type == 0 ? props.machineId : 9999999;
const res = await machineApi.scripts.request(state.query);
state.scriptTable = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
getScripts();
};
const runScript = async (script: any) => {
// 如果存在参数,则弹窗输入参数后执行
if (script.params) {
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
state.scriptParamsDialog.visible = true;
return;
}
run(script);
};
// 有参数的脚本执行函数
const hasParamsRun = async (script: any) => {
// 如果脚本参数弹窗显示,则校验参数表单数据通过后执行
if (state.scriptParamsDialog.visible) {
paramsForm.value.validate((valid: any) => {
if (valid) {
run(script);
state.scriptParamsDialog.params = {};
state.scriptParamsDialog.visible = false;
paramsForm.value.resetFields();
} else {
return false;
}
});
}
};
const run = async (script: any) => {
const noResult = script.type == enums.scriptTypeEnum['NO_RESULT'].value;
// 如果脚本类型为有结果类型,则显示结果信息
if (script.type == enums.scriptTypeEnum['RESULT'].value || noResult) {
const res = await machineApi.runScript.request({
machineId: props.machineId,
scriptId: script.id,
params: state.scriptParamsDialog.params,
});
if (noResult) {
ElMessage.success('执行完成');
return;
}
state.resultDialog.result = res;
state.resultDialog.visible = true;
return;
}
if (script.type == enums.scriptTypeEnum['REAL_TIME'].value) {
script = script.script;
if (state.scriptParamsDialog.params) {
script = templateResolve(script, state.scriptParamsDialog.params);
}
state.terminalDialog.cmd = script;
state.terminalDialog.visible = true;
state.terminalDialog.machineId = props.machineId;
return;
}
};
/**
* 解析 {{.param}} 形式模板字符串
*/
function templateResolve(template: string, param: any) {
return template.replace(/\{{.\w+\}}/g, (word) => {
const key = word.substring(3, word.length - 2);
const value = param[key];
if (value != null || value != undefined) {
return value;
}
return '';
});
}
const closeTermnial = () => {
state.terminalDialog.visible = false;
state.terminalDialog.machineId = 0;
// const t: any = this.$refs['terminal']
// t.closeAll()
};
/**
* 选择数据
*/
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
const editScript = (data: any) => {
state.editDialog.machineId = props.machineId;
state.editDialog.data = data;
if (data) {
state.editDialog.title = '查看编辑脚本';
} else {
state.editDialog.title = '新增脚本';
}
state.editDialog.visible = true;
};
const submitSuccess = () => {
// this.delChoose()
// this.search()
getScripts();
};
const deleteRow = (row: any) => {
ElMessageBox.confirm(`此操作将删除 [${row.name}], 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
machineApi.deleteScript
.request({
machineId: props.machineId,
scriptId: row.id,
})
.then(() => {
getScripts();
});
// 删除配置文件
});
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
context.emit('update:visible', false);
context.emit('update:machineId', null);
context.emit('cancel');
state.scriptTable = [];
};
return {
...toRefs(state),
paramsForm,
enums,
getScripts,
handlePageChange,
runScript,
hasParamsRun,
closeTermnial,
choose,
editScript,
submitSuccess,
deleteRow,
handleClose,
};
total: 0,
scriptTable: [],
scriptParamsDialog: {
visible: false,
params: {},
paramsFormItem: [],
},
resultDialog: {
visible: false,
result: '',
},
terminalDialog: {
visible: false,
cmd: '',
machineId: 0,
},
});
const {
dialogVisible,
type,
currentId,
currentData,
query,
editDialog,
total,
scriptTable,
scriptParamsDialog,
resultDialog,
terminalDialog,
} = toRefs(state)
watch(props, async (newValue) => {
if (props.machineId && newValue.visible) {
await getScripts();
}
state.dialogVisible = newValue.visible;
});
const getScripts = async () => {
state.currentId = null;
state.currentData = null;
state.query.machineId = state.type == 0 ? props.machineId : 9999999;
const res = await machineApi.scripts.request(state.query);
state.scriptTable = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
getScripts();
};
const runScript = async (script: any) => {
// 如果存在参数,则弹窗输入参数后执行
if (script.params) {
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
if (state.scriptParamsDialog.paramsFormItem && state.scriptParamsDialog.paramsFormItem.length > 0) {
state.scriptParamsDialog.visible = true;
return;
}
}
run(script);
};
// 有参数的脚本执行函数
const hasParamsRun = async (script: any) => {
// 如果脚本参数弹窗显示,则校验参数表单数据通过后执行
if (state.scriptParamsDialog.visible) {
paramsForm.value.validate((valid: any) => {
if (valid) {
run(script);
state.scriptParamsDialog.params = {};
state.scriptParamsDialog.visible = false;
paramsForm.value.resetFields();
} else {
return false;
}
});
}
};
const run = async (script: any) => {
const noResult = script.type == enums.scriptTypeEnum['NO_RESULT'].value;
// 如果脚本类型为有结果类型,则显示结果信息
if (script.type == enums.scriptTypeEnum['RESULT'].value || noResult) {
const res = await machineApi.runScript.request({
machineId: props.machineId,
scriptId: script.id,
params: state.scriptParamsDialog.params,
});
if (noResult) {
ElMessage.success('执行完成');
return;
}
state.resultDialog.result = res;
state.resultDialog.visible = true;
return;
}
if (script.type == enums.scriptTypeEnum['REAL_TIME'].value) {
script = script.script;
if (state.scriptParamsDialog.params) {
script = templateResolve(script, state.scriptParamsDialog.params);
}
state.terminalDialog.cmd = script;
state.terminalDialog.visible = true;
state.terminalDialog.machineId = props.machineId as any;
return;
}
};
/**
* 解析 {{.param}} 形式模板字符串
*/
function templateResolve(template: string, param: any) {
return template.replace(/\{{.\w+\}}/g, (word) => {
const key = word.substring(3, word.length - 2);
const value = param[key];
if (value != null || value != undefined) {
return value;
}
return '';
});
}
const closeTermnial = () => {
state.terminalDialog.visible = false;
state.terminalDialog.machineId = 0;
};
/**
* 选择数据
*/
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
const editScript = (data: any) => {
state.editDialog.machineId = props.machineId as any;
state.editDialog.data = data;
if (data) {
state.editDialog.title = '查看编辑脚本';
} else {
state.editDialog.title = '新增脚本';
}
state.editDialog.visible = true;
};
const submitSuccess = () => {
getScripts();
};
const deleteRow = (row: any) => {
ElMessageBox.confirm(`此操作将删除 [${row.name}], 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
machineApi.deleteScript
.request({
machineId: props.machineId,
scriptId: row.id,
})
.then(() => {
getScripts();
});
// 删除配置文件
});
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
emit('update:visible', false);
emit('update:machineId', null);
emit('cancel');
state.scriptTable = [];
state.scriptParamsDialog.paramsFormItem = [];
};
</script>
<style lang="sass">
</style>

View File

@@ -1,196 +1,185 @@
<template>
<div :style="{ height: height }" id="xterm" class="xterm" />
<div :style="{ height: props.height }" id="xterm" class="xterm" />
</template>
<script lang="ts">
<script lang="ts" setup>
import 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { getSession } from '@/common/utils/storage.ts';
import config from '@/common/config';
import { useStore } from '@/store/index.ts';
import { toRefs, watch, computed, reactive, defineComponent, onMounted, onBeforeUnmount } from 'vue';
import { nextTick, computed, reactive, onMounted, onBeforeUnmount } from 'vue';
export default defineComponent({
name: 'SshTerminal',
props: {
machineId: { type: Number },
cmd: { type: String },
height: { type: String },
},
setup(props: any) {
const state = reactive({
machineId: 0,
cmd: '',
height: '',
term: null as any,
socket: null as any,
});
const props = defineProps({
machineId: { type: Number },
cmd: { type: String },
height: { type: [String, Number] },
})
watch(props, (newValue) => {
state.machineId = newValue.machineId;
state.cmd = newValue.cmd;
state.height = newValue.height;
if (state.machineId) {
initSocket();
}
});
onMounted(() => {
state.machineId = props.machineId;
state.height = props.height;
state.cmd = props.cmd;
if (state.machineId) {
initSocket();
}
});
onBeforeUnmount(() => {
closeAll();
});
const store = useStore();
// 获取布局配置信息
const getThemeConfig: any = computed(() => {
return store.state.themeConfig.themeConfig;
});
function initXterm() {
const term: any = new Terminal({
fontSize: getThemeConfig.value.terminalFontSize || 15,
// fontWeight: getThemeConfig.value.terminalFontWeight || 'normal',
fontFamily: 'JetBrainsMono, Consolas, Menlo, Monaco',
cursorBlink: true,
// cursorStyle: 'underline', //光标样式
disableStdin: false,
theme: {
foreground: getThemeConfig.value.terminalForeground || '#7e9192', //字体
background: getThemeConfig.value.terminalBackground || '#002833', //背景色
cursor: getThemeConfig.value.terminalCursor || '#268F81', //设置光标
} as any,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('xterm'));
fitAddon.fit();
term.focus();
state.term = term;
// 监听窗口resize
window.addEventListener('resize', () => {
try {
// 窗口大小改变时触发xterm的resize方法使自适应
fitAddon.fit();
} catch (e) {
console.log(e);
}
});
// / **
// *添加事件监听器,用于按下键时的事件。事件值包含
// *将在data事件以及DOM事件中发送的字符串
// *触发了它。
// * @返回一个IDisposable停止监听。
// * /
// / ** 更新xterm 4.x新增
// *为数据事件触发时添加事件侦听器。发生这种情况
// *用户输入或粘贴到终端时的示例。事件值
// *是`string`结果的结果,在典型的设置中,应该通过
// *到支持pty。
// * @返回一个IDisposable停止监听。
// * /
// 支持输入与粘贴方法
term.onData((key: any) => {
sendCmd(key);
});
// 为解决窗体resize方法才会向后端发送列数和行数所以页面加载时也要触发此方法
send({
type: 'resize',
Cols: parseInt(term.cols),
Rows: parseInt(term.rows),
});
// 如果有初始要执行的命令,则发送执行命令
if (state.cmd) {
sendCmd(state.cmd + ' \r');
}
}
function initSocket() {
state.socket = new WebSocket(`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}`);
// 监听socket连接
state.socket.onopen = open;
// 监听socket错误信息
state.socket.onerror = error;
// 监听socket消息
state.socket.onmessage = getMessage;
// 发送socket消息
state.socket.onsend = send;
}
function open() {
console.log('socket连接成功');
initXterm();
//开启心跳
// this.start();
}
function error() {
console.log('连接错误');
//重连
// reconnect();
}
function close() {
if (state.socket) {
state.socket.close();
console.log('socket关闭');
}
//重连
// this.reconnect()
}
function getMessage(msg: string) {
// console.log(msg)
state.term.write(msg['data']);
//msg是返回的数据
// msg = JSON.parse(msg.data);
// this.socket.send("ping");//有事没事ping一下看看ws还活着没
// //switch用于处理返回的数据根据返回数据的格式去判断
// switch (msg["operation"]) {
// case "stdout":
// this.term.write(msg["data"]);//这里write也许不是固定的失败后找后端看一下该怎么往term里面write
// break;
// default:
// console.error("Unexpected message type:", msg);//但是错误是固定的。。。。
// }
//收到服务器信息,心跳重置
// this.reset();
}
function send(msg: any) {
state.socket.send(JSON.stringify(msg));
}
function sendCmd(key: any) {
send({
type: 'cmd',
msg: key,
});
}
function closeAll() {
close();
if (state.term) {
state.term.dispose();
state.term = null;
}
}
return {
...toRefs(state),
};
},
const state = reactive({
cmd: '',
term: null as any,
socket: null as any,
});
</script>
const resize = 1;
const data = 2;
const ping = 3;
onMounted(() => {
state.cmd = props.cmd as any;
nextTick(() => {
initXterm();
initSocket();
});
});
onBeforeUnmount(() => {
closeAll();
});
const store = useStore();
// 获取布局配置信息
const getThemeConfig: any = computed(() => {
return store.state.themeConfig.themeConfig;
});
function initXterm() {
const term: any = new Terminal({
fontSize: getThemeConfig.value.terminalFontSize || 15,
fontWeight: getThemeConfig.value.terminalFontWeight || 'normal',
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
cursorBlink: true,
disableStdin: false,
theme: {
foreground: getThemeConfig.value.terminalForeground || '#7e9192', //字体
background: getThemeConfig.value.terminalBackground || '#002833', //背景色
cursor: getThemeConfig.value.terminalCursor || '#268F81', //设置光标
// cursorAccent: "red", // 光标停止颜色
} as any,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('xterm'));
fitAddon.fit();
term.focus();
state.term = term;
// 监听窗口resize
window.addEventListener('resize', () => {
try {
// 窗口大小改变时触发xterm的resize方法使自适应
fitAddon.fit();
if (state.term) {
state.term.focus();
send({
type: resize,
Cols: parseInt(state.term.cols),
Rows: parseInt(state.term.rows),
});
}
} catch (e) {
console.log(e);
}
});
// / **
// *添加事件监听器,用于按下键时的事件。事件值包含
// *将在data事件以及DOM事件中发送的字符串
// *触发了它。
// * @返回一个IDisposable停止监听。
// * /
// / ** 更新xterm 4.x新增
// *为数据事件触发时添加事件侦听器。发生这种情况
// *用户输入或粘贴到终端时的示例。事件值
// *是`string`结果的结果,在典型的设置中,应该通过
// *到支持pty。
// * @返回一个IDisposable停止监听。
// * /
// 支持输入与粘贴方法
term.onData((key: any) => {
sendCmd(key);
});
}
let pingInterval: any;
function initSocket() {
state.socket = new WebSocket(
`${config.baseWsUrl}/machines/${props.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows
}`
);
// 监听socket连接
state.socket.onopen = () => {
// 如果有初始要执行的命令,则发送执行命令
if (state.cmd) {
sendCmd(state.cmd + ' \r');
}
// 开启心跳
pingInterval = setInterval(() => {
send({ type: ping, msg: 'ping' });
}, 8000);
};
// 监听socket错误信息
state.socket.onerror = (e: any) => {
console.log('连接错误', e);
};
state.socket.onclose = () => {
if (state.term) {
state.term.writeln('\r\n\x1b[31m提示: 连接已关闭...');
}
if (pingInterval) {
clearInterval(pingInterval);
}
};
// 发送socket消息
state.socket.onsend = send;
// 监听socket消息
state.socket.onmessage = getMessage;
}
function getMessage(msg: any) {
// msg.data是真正后端返回的数据
state.term.write(msg.data);
}
function send(msg: any) {
state.socket.send(JSON.stringify(msg));
}
function sendCmd(key: any) {
send({
type: data,
msg: key,
});
}
function close() {
if (state.socket) {
state.socket.close();
console.log('socket关闭');
}
}
function closeAll() {
close();
if (state.term) {
state.term.dispose();
state.term = null;
}
}
</script>
<style lang="scss">
#xterm {
.xterm-viewport {
overflow-y: hidden
}
}
</style>

View File

@@ -4,36 +4,27 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import SshTerminal from './SshTerminal.vue';
import { reactive, toRefs, defineComponent, onMounted } from 'vue';
import { reactive, toRefs, onMounted } from 'vue';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'SshTerminalPage',
components: {
SshTerminal,
},
props: {
machineId: { type: Number },
},
setup() {
const route = useRoute();
const state = reactive({
machineId: 0,
height: 700,
});
const route = useRoute();
const state = reactive({
machineId: 0,
height: 0,
});
onMounted(() => {
state.height = window.innerHeight + 5;
state.machineId = Number.parseInt(route.query.id as string);
});
const {
machineId,
height,
} = toRefs(state)
return {
...toRefs(state),
};
},
onMounted(() => {
state.height = window.innerHeight + 5;
state.machineId = Number.parseInt(route.query.id as string);
});
</script>
<style lang="scss">
</style>

View File

@@ -3,12 +3,14 @@ import Api from '@/common/Api';
export const machineApi = {
// 获取权限列表
list: Api.create("/machines", 'get'),
getMachinePwd: Api.create("/machines/{id}/pwd", 'get'),
info: Api.create("/machines/{id}/sysinfo", 'get'),
stats: Api.create("/machines/{id}/stats", 'get'),
process: Api.create("/machines/{id}/process", 'get'),
// 终止进程
killProcess: Api.create("/machines/{id}/process", 'delete'),
closeCli: Api.create("/machines/{id}/close-cli", 'delete'),
testConn: Api.create("/machines/test-conn", 'post'),
// 保存按钮
saveMachine: Api.create("/machines", 'post'),
// 调整状态
@@ -25,11 +27,20 @@ export const machineApi = {
rmFile: Api.create("/machines/{machineId}/files/{fileId}/remove", 'delete'),
uploadFile: Api.create("/machines/{machineId}/files/{fileId}/upload?token={token}", 'post'),
fileContent: Api.create("/machines/{machineId}/files/{fileId}/read", 'get'),
createFile: Api.create("/machines/{machineId}/files/{id}/create-file", 'post'),
// 修改文件内容
updateFileContent: Api.create("/machines/{machineId}/files/{id}/write", 'post'),
// 添加文件or目录
addConf: Api.create("/machines/{machineId}/files", 'post'),
// 删除配置的文件or目录
delConf: Api.create("/machines/{machineId}/files/{id}", 'delete'),
terminal: Api.create("/api/machines/{id}/terminal", 'get')
terminal: Api.create("/api/machines/{id}/terminal", 'get'),
recDirNames: Api.create("/machines/rec/names", 'get')
}
export const authCertApi = {
baseList : Api.create("/sys/authcerts/base", 'get'),
list: Api.create("/sys/authcerts", 'get'),
save: Api.create("/sys/authcerts", 'post'),
delete: Api.create("/sys/authcerts/{id}", 'delete'),
}

View File

@@ -0,0 +1,126 @@
<template>
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px"
:destroy-on-close="true">
<el-form ref="acForm" :rules="rules" :model="form" label-width="90px">
<el-form-item prop="name" label="名称:" required>
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="authMethod" label="认证方式:" required>
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="密钥" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码"
autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此">
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码:">
<el-input type="password" v-model="form.passphrase">
</el-input>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { authCertApi } from '../api';
const props = defineProps({
visible: {
type: Boolean,
},
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
})
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const acForm: any = ref(null);
const rules = {
name: [
{
required: true,
message: '授权凭证名称不能为空',
trigger: ['change', 'blur'],
},
]
}
const state = reactive({
dvisible: false,
params: [] as any,
form: {
id: null,
name: '',
authMethod: 1,
password: '',
passphrase: '',
remark: '',
},
btnLoading: false,
});
const {
dvisible,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => {
state.dvisible = newValue.visible;
if (newValue.data) {
state.form = { ...newValue.data };
} else {
state.form = { authMethod: 1 } as any;
state.params = [];
}
});
const cancel = () => {
// 更新父组件visible prop对应的值为false
emit('update:visible', false);
// 若父组件有取消事件,则调用
emit('cancel');
};
const btnOk = async () => {
acForm.value.validate(async (valid: boolean) => {
if (valid) {
state.btnLoading = true;
try {
await authCertApi.save.request(state.form);
emit('val-change', state.form);
cancel();
} finally {
state.btnLoading = false;
}
}
});
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="role-list">
<el-card>
<div>
<el-button type="primary" icon="plus" @click="edit(false)">添加</el-button>
<el-button :disabled="chooseId == null" @click="edit(chooseData)" type="primary" icon="edit">编辑
</el-button>
<el-button :disabled="chooseId == null" @click="deleteAc(chooseData)" type="danger" icon="delete">删除
</el-button>
<div style="float: right">
<el-select v-model="query.type" placeholder="请选择标签" @clear="search" filterable clearable>
<el-option label="" value="item"> </el-option>
</el-select>
<el-input class="ml5" placeholder="请输入凭证名称" style="width: 150px" v-model="query.name" @clear="search"
plain clearable></el-input>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button>
</div>
</div>
<el-table :data="authcerts" @current-change="choose" ref="table" style="width: 100%">
<el-table-column label="选择" width="55px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="60px" show-overflow-tooltip></el-table-column>
<el-table-column prop="authMethod" label="认证方式" min-width="50px">
<template #default="scope">
<el-tag v-if="scope.row.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="scope.row.authMethod == 2" type="primary" size="small">密钥</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="100px" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="creator" label="创建人" min-width="60px"></el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="100px">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="modifier" label="修改者" min-width="60px" show-overflow-tooltip></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="100px">
<template #default="scope">
{{ dateFormat(scope.row.updateTime) }}
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination style="text-align: right" @current-change="handlePageChange" :total="total"
layout="prev, pager, next, total, jumper" v-model:current-page="query.pageNum"
:page-size="query.pageSize"></el-pagination>
</el-row>
</el-card>
<auth-cert-edit :title="editor.title" v-model:visible="editor.visible" :data="editor.authcert"
@val-change="editChange" />
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import AuthCertEdit from './AuthCertEdit.vue';
import { authCertApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date';
const state = reactive({
query: {
pageNum: 1,
pageSize: 10,
name: null,
type: null,
},
total: 0,
authcerts: [],
chooseId: null,
chooseData: null,
paramsDialog: {
visible: false,
config: null as any,
params: {},
paramsFormItem: [] as any,
},
editor: {
title: '授权凭证保存',
visible: false,
authcert: {},
},
});
const {
query,
total,
authcerts,
chooseId,
chooseData,
editor,
} = toRefs(state)
onMounted(() => {
search();
});
const search = async () => {
let res = await authCertApi.list.request(state.query);
state.authcerts = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const editChange = () => {
ElMessage.success('保存成功');
state.chooseId = null;
state.chooseData = null;
search();
};
const edit = (data: any) => {
if (data) {
state.editor.authcert = data;
} else {
state.editor.authcert = false;
}
state.editor.visible = true;
};
const deleteAc = async (data: any) => {
try {
await ElMessageBox.confirm(`确定删除该授权凭证?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await authCertApi.delete.request({ id: data.id });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) { }
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,63 @@
<template>
<div style="width: 100%">
<el-select @change="changeValue" v-model="id" filterable placeholder="请选择授权凭证,可前往[机器管理->授权凭证]添加"
style="width: 100%">
<el-option v-for="ac in acs" :key="ac.id" :value="ac.id" :label="ac.name">
<el-tag v-if="ac.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="ac.authMethod == 2" type="primary" size="small">密钥</el-tag>
<el-divider direction="vertical" border-style="dashed" />
{{ ac.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ ac.remark }}
</el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onMounted } from 'vue';
import { authCertApi } from '../api';
//定义事件
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: {
type: [Number],
required: true,
},
})
const state = reactive({
acs: [] as any,
id: null as any,
})
const {
acs,
id,
} = toRefs(state)
onMounted(async () => {
await getAcs();
if (props.modelValue) {
state.id = props.modelValue;
}
})
const changeValue = (val: any) => {
emit('update:modelValue', val);
emit('change', val);
}
const getAcs = async () => {
const acs = await authCertApi.baseList.request({ pageSize: 100, type: 2 })
state.acs = acs.list;
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,515 @@
<template>
<div>
<el-row>
<el-col :span="4">
<tag-tree @node-click="nodeClick" :load="loadNode">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.Mongo">
<el-popover placement="right-start" title="mongo实例信息" trigger="hover" :width="210">
<template #reference>
<img src="@/assets/icon/mongo.png" class="img-icon" />
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="链接:">{{ data.params.uri }}</el-form-item>
</el-form>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Dbs">
<Coin color="#67c23a" />
</el-icon>
<el-icon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu">
<Document class="color-primary" />
</el-icon>
</template>
<template #label="{ data }">
<span v-if="data.type == NodeType.Dbs">
{{ data.params.dbName }}
<span style="color: #8492a6;font-size: 13px">
[{{ formatByteSize(data.params.size) }}]
</span>
</span>
<span v-else>{{ data.label }}</span>
</template>
</tag-tree>
</el-col>
<el-col :span="20">
<el-container id="mongo-tab" style="border: 1px solid #eee; margin-top: 1px">
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px"
v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label"
:name="dt.key">
<el-row>
<el-col :span="2">
<div>
<el-link @click="findCommand(state.activeName)" icon="refresh"
:underline="false" class="">
</el-link>
<el-link @click="showInsertDocDialog" class="ml5" type="primary" icon="plus"
:underline="false">
</el-link>
</div>
</el-col>
<el-col :span="22">
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)">
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onJsonEditor(item)" :underline="false" type="success"
icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onSaveDoc(item.value)" :underline="false"
type="warning" icon="DocumentChecked"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
<template #reference>
<el-link :underline="false" type="danger" icon="DocumentDelete">
</el-link>
</template>
</el-popconfirm>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-container>
</el-col>
</el-row>
<el-dialog width="600px" title="find参数" v-model="findDialog.visible">
<el-form label-width="70px">
<el-form-item label="filter">
<monaco-editor style="width: 100%;" height="150px" ref="monacoEditorRef"
v-model="findDialog.findParam.filter" language="json" />
</el-form-item>
<el-form-item label="sort">
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable
auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="limit">
<el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="skip">
<el-input v-model.number="findDialog.findParam.skip" type="number" auto-complete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="findDialog.visible = false"> </el-button>
<el-button @click="confirmFindDialog" type="primary"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog width="60%" :title="`新增'${state.activeName}'集合文档`" v-model="insertDocDialog.visible"
:close-on-click-modal="false">
<monaco-editor v-model="insertDocDialog.doc" language="json" />
<template #footer>
<div>
<el-button @click="insertDocDialog.visible = false"> </el-button>
<el-button @click="onInsertDoc" type="primary"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog width="60%" title="json编辑器" v-model="jsonEditorDialog.visible" @close="onCloseJsonEditDialog"
:close-on-click-modal="false">
<monaco-editor v-model="jsonEditorDialog.doc" language="json" />
</el-dialog>
<div style="text-align: center; margin-top: 10px"></div>
</div>
</template>
<script lang="ts" setup>
import { mongoApi } from './api';
import { reactive, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { isTrue, notBlank } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format';
/**
* 树节点类型
*/
class NodeType {
static Mongo = 1
static Dbs = 2
static CollMenu = 3
static Coll = 4
}
const findParamInputRef: any = ref(null);
const state = reactive({
tags: [],
mongoList: [] as any,
activeName: '', // 当前操作的tab
dataTabs: {} as any, // 数据tabs
findDialog: {
visible: false,
findParam: {
limit: 0,
skip: 0,
filter: '',
sort: '',
},
},
insertDocDialog: {
visible: false,
doc: '',
},
jsonEditorDialog: {
visible: false,
doc: '',
item: {} as any,
},
});
const {
findDialog,
insertDocDialog,
jsonEditorDialog,
} = toRefs(state)
/**
* instmap; tagPaht -> mongo info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, });
if (!res.total) return
for (const mongoInfo of res.list) {
const tagPath = mongoInfo.tagPath;
let mongoInsts = instMap.get(tagPath) || [];
mongoInsts.push(mongoInfo);
instMap.set(tagPath, mongoInsts);
}
}
/**
* 加载文件树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
const params = data.params;
const nodeType = data.type;
// 点击标签 -> 显示mongo信息列表
if (nodeType === TagTreeNode.TagPath) {
const mongoInfos = instMap.get(data.key)
return mongoInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x);
});
}
// 点击mongo -> 加载mongo数据库列表
if (nodeType === NodeType.Mongo) {
return await getDatabases(params);
}
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
if (nodeType === NodeType.Dbs) {
return [new TagTreeNode(`${params.id}.${params.dbName}.mongo-coll`, '集合', NodeType.CollMenu).withParams(params)];
}
// 点击数据库集合节点 -> 加载集合列表
if (nodeType === NodeType.CollMenu) {
return await getCollections(params.id, params.dbName)
}
return [];
};
/**
* 获取实例的所有库信息
* @param inst 实例信息
*/
const getDatabases = async (inst: any) => {
const res = await mongoApi.databases.request({ id: inst.id });
return res.Databases.map((x: any) => {
const dbName = x.Name;
return new TagTreeNode(`${inst.id}.${dbName}`, dbName, NodeType.Dbs).withParams({
id: inst.id,
dbName,
size: x.SizeOnDisk,
})
})
}
/**
* 获取集合列表信息
* @param inst
*/
const getCollections = async (id: any, database: string) => {
const colls = await mongoApi.collections.request({ id, database });
return colls.map((x: any) => {
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeType.Coll).withIsLeaf(true).withParams({
id,
database,
collection: x,
});
});
}
const nodeClick = (data: any) => {
// 点击集合
if (data.type === NodeType.Coll) {
const { id, database, collection } = data.params;
changeCollection(id, database, collection);
}
}
const changeCollection = (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label];
if (!dataTab) {
// 默认查询参数
const findParam = {
filter: '{}',
sort: '{"_id": -1}',
skip: 0,
limit: 12,
};
state.dataTabs[label] = {
key: label,
label: label,
name: label,
mongoId: id,
database: schema,
collection,
datas: [],
findParamStr: JSON.stringify(findParam),
findParam,
};
}
state.activeName = label;
findCommand(label);
};
const showFindDialog = (key: string) => {
// 获取当前tab的索引位置将其输入框失去焦点防止输入以及重复获取焦点
const dataTabNames = Object.keys(state.dataTabs);
for (let i = 0; i < dataTabNames.length; i++) {
if (key == dataTabNames[i]) {
findParamInputRef.value[i].blur();
}
}
state.findDialog.findParam = state.dataTabs[key].findParam;
state.findDialog.visible = true;
};
const confirmFindDialog = () => {
state.dataTabs[state.activeName].findParam = state.findDialog.findParam;
state.dataTabs[state.activeName].findParamStr = JSON.stringify(state.findDialog.findParam);
state.findDialog.visible = false;
findCommand(state.activeName);
};
const findCommand = async (key: string) => {
const dataTab = getNowDataTab();
const findParma = dataTab.findParam;
let filter, sort;
try {
filter = findParma.filter ? JSON.parse(findParma.filter) : {};
sort = findParma.sort ? JSON.parse(findParma.sort) : {};
} catch (e) {
ElMessage.error('filter或sort字段json字符串值错误。注意: json key需双引号');
return;
}
const datas = await mongoApi.findCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
filter,
sort,
limit: findParma.limit || 12,
skip: findParma.skip || 0,
});
state.dataTabs[key].datas = wrapDatas(datas);
};
/**
* 包装mongo查询回来的对象即将其都转为json字符串并用value属性值描述方便显示
*/
const wrapDatas = (datas: any) => {
const wrapDatas = [] as any;
if (!datas) {
return wrapDatas;
}
for (let data of datas) {
wrapDatas.push({ value: JSON.stringify(data, null, 4) });
}
return wrapDatas;
};
const showInsertDocDialog = () => {
// tab数据中的第一个文档因为该集合的文档都类似故使用第一个文档赋值至需要新增的文档输入框方便直接修改新增
const datasFirstDoc = state.dataTabs[state.activeName].datas[0];
let doc = '';
if (datasFirstDoc) {
// 移除_id字段因为新增无需该字段
const docObj = JSON.parse(datasFirstDoc.value);
delete docObj['_id'];
doc = JSON.stringify(docObj, null, 4);
}
state.insertDocDialog.doc = doc;
state.insertDocDialog.visible = true;
};
const onInsertDoc = async () => {
let docObj;
try {
docObj = JSON.parse(state.insertDocDialog.doc);
} catch (e) {
ElMessage.error('文档内容错误,无法解析为json对象');
}
const dataTab = getNowDataTab();
const res = await mongoApi.insertCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
doc: docObj,
});
isTrue(res.InsertedID, '新增失败');
ElMessage.success('新增成功');
findCommand(state.activeName);
state.insertDocDialog.visible = false;
};
const onJsonEditor = (item: any) => {
state.jsonEditorDialog.item = item;
state.jsonEditorDialog.doc = item.value;
state.jsonEditorDialog.visible = true;
};
const onCloseJsonEditDialog = () => {
state.jsonEditorDialog.item.value = JSON.stringify(JSON.parse(state.jsonEditorDialog.doc), null, 4);
};
const onSaveDoc = async (doc: string) => {
const docObj = parseDocJsonString(doc);
const id = docObj._id;
notBlank(id, '文档的_id属性不存在');
delete docObj['_id'];
const dataTab = getNowDataTab();
const res = await mongoApi.updateByIdCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
docId: id,
update: { $set: docObj },
});
isTrue(res.ModifiedCount == 1, '修改失败');
ElMessage.success('保存成功');
};
const onDeleteDoc = async (doc: string) => {
const docObj = parseDocJsonString(doc);
const id = docObj._id;
notBlank(id, '文档的_id属性不存在');
const dataTab = getNowDataTab();
const res = await mongoApi.deleteByIdCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
docId: id,
});
isTrue(res.DeletedCount == 1, '删除失败');
ElMessage.success('删除成功');
findCommand(state.activeName);
};
/**
* 将json字符串解析为json对象
*/
const parseDocJsonString = (doc: string) => {
try {
return JSON.parse(doc);
} catch (e) {
ElMessage.error('文档内容解析为json对象失败');
throw e;
}
};
const removeDataTab = (targetName: string) => {
const tabNames = Object.keys(state.dataTabs);
let activeName = state.activeName;
tabNames.forEach((name, index) => {
if (name === targetName) {
const nextTab = tabNames[index + 1] || tabNames[index - 1];
if (nextTab) {
activeName = nextTab;
}
}
});
state.activeName = activeName;
delete state.dataTabs[targetName];
};
const getNowDataTab = () => {
return state.dataTabs[state.activeName]
}
</script>
<style lang="scss">
.mongo-doc-btns {
position: absolute;
z-index: 2;
right: 3px;
top: 2px;
max-width: 120px;
}
#mongo-tab {
.el-tabs__header {
margin: 0 0 5px;
.el-tabs__item {
padding: 0 5px;
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%"
:destroy-on-close="true">
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="uri" label="uri" required>
<el-input type="textarea" :rows="2" v-model.trim="form.uri"
placeholder="形如 mongodb://username:password@host1:port1" auto-complete="off"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { mongoApi } from './api';
import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
const props = defineProps({
visible: {
type: Boolean,
},
mongo: {
type: [Boolean, Object],
},
title: {
type: String,
},
})
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const rules = {
tagId: [
{
required: true,
message: '请选择标签',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入名称',
trigger: ['change', 'blur'],
},
],
uri: [
{
required: true,
message: '请输入mongo uri',
trigger: ['change', 'blur'],
},
],
}
const mongoForm: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
form: {
id: null,
name: null,
uri: null,
sshTunnelMachineId: null as any,
tagId: null as any,
tagPath: null as any,
},
btnLoading: false,
});
const {
dialogVisible,
tabActiveName,
form,
btnLoading,
} = toRefs(state)
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.mongo) {
state.form = { ...newValue.mongo };
} else {
state.form = { db: 0 } as any;
}
});
const btnOk = async () => {
mongoForm.value.validate(async (valid: boolean) => {
if (valid) {
const reqForm = { ...state.form };
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1
}
// reqForm.uri = await RsaEncrypt(reqForm.uri);
mongoApi.saveMongo.request(reqForm).then(() => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,178 @@
<template>
<tag-menu :instanceMenuMaxHeight="state.instanceMenuMaxHeight" :tags="instances.tags" ref="menuRef">
<template #submenu="props">
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'mongo-instance-' + inst.id"
:key="'mongo-instance-' + inst.id" @click.stop="changeInstance(inst, () => { })">
<template #title>
<el-popover placement="right-start" title="mongo数据库实例信息" trigger="hover" :width="210">
<template #reference>
<span>&nbsp;&nbsp;<el-icon>
<MostlyCloudy color="#409eff" />
</el-icon>{{ inst.name }}</span>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ inst.name }}</el-form-item>
<el-form-item label="链接:">{{ inst.uri }}</el-form-item>
</el-form>
</template>
</el-popover>
</template>
<!-- 第三级数据库 -->
<el-sub-menu v-for="db in instances.dbs[inst.id]" :index="inst.id + db.Name" :key="inst.id + db.Name"
:class="state.nowSchema === (inst.id + db.Name) && 'checked'"
@click.stop="changeSchema(inst, db.Name)">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Coin color="#67c23a" />
</el-icon>
<span class="checked-schema">
{{ db.Name }}
<span style="color: #8492a6;font-size: 13px">[{{
formatByteSize(db.SizeOnDisk)
}}]</span>
</span>
</template>
<!-- 第四级 01 -->
<el-sub-menu :index="inst.id + db.Name + '-table'">
<template #title>
<div style="width: 100%" @click="loadTableNames(inst, db.Name, () => { })">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Calendar color="#409eff" />
</el-icon>
<span>集合</span>
<el-icon v-show="state.loading[inst.id + db.Name]" class="is-loading">
<Loading />
</el-icon>
</div>
</template>
<el-menu-item :index="inst.id + db.Name + '-tableSearch'"
:key="inst.id + db.Name + '-tableSearch'">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<el-input size="small" placeholder="过滤" clearable
@change="filterTableName(inst.id, db.Name)"
@keyup="(e: any) => filterTableName(inst.id, db.Name, e)"
v-model="state.filterParam[inst.id + db.Name]" />
</template>
</el-menu-item>
<template v-for="tb in instances.tables[inst.id + db.Name]">
<el-menu-item :index="inst.id + db.Name + tb.tableName"
:key="inst.id + db.Name + tb.tableName" v-if="tb.show"
@click="loadTableData(inst, db.Name, tb.tableName)">
<template #title>
<div style="width: 100%">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Calendar color="#409eff" />
</el-icon>
<span :title="tb.tableComment || ''">{{ tb.tableName }}</span>
</div>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</el-sub-menu>
</el-sub-menu>
</template>
</tag-menu>
</template>
<script lang="ts" setup>
import { onBeforeMount, reactive } from 'vue';
import { formatByteSize } from '@/common/utils/format';
import TagMenu from '../component/TagMenu.vue';
const props = defineProps({
instances: {
type: Object, required: true
},
})
const emits = defineEmits(['initLoadInstances', 'changeInstance', 'loadTableNames', 'loadTableData', 'changeSchema'])
onBeforeMount(async () => {
await initLoadInstances()
setHeight()
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
const state = reactive({
instanceMenuMaxHeight: '800px',
nowSchema: '',
filterParam: {},
loading: {},
})
/**
* 初始化加载实例数据
*/
const initLoadInstances = () => {
emits('initLoadInstances')
}
/**
* 改变选中的数据库实例
* @param inst 选中的实例对象
* @param fn 选中的实例对象后的回调事件
*/
const changeInstance = (inst: any, fn: Function) => {
emits('changeInstance', inst, fn)
}
/**
* 改变选中的数据库schema
* @param inst 选中的实例对象
* @param schema 选中的数据库schema
*/
const changeSchema = (inst: any, schema: string) => {
state.nowSchema = inst.id + schema
emits('changeSchema', inst, schema)
}
/**
* 加载schema下所有表
* @param inst 数据库实例
* @param fn 加载表名后的回调函数参数表名list
* @param schema database名
*/
const loadTableNames = async (inst: any, schema: string, fn: Function) => {
state.loading[inst.id + schema] = true
await emits('loadTableNames', inst, schema, (res: any) => {
state.loading[inst.id + schema] = false
fn && fn(res)
})
}
/**
* 加载选中表数据
* @param inst 数据库实例
* @param schema database名
* @param tableName 表名
*/
const loadTableData = (inst: any, schema: string, tableName: string) => {
emits('loadTableData', inst, schema, tableName)
}
const filterTableName = (instId: number, schema: string, event?: any) => {
if (event) {
state.filterParam[instId + schema] = event.target.value
}
let param = state.filterParam[instId + schema] as string
param = param?.replace('/', '\/')
const key = instId + schema;
props.instances.tables[key].forEach((a: any) => {
a.show = param ? eval('/' + param.split('').join('[_\w]*') + '[_\w]*/ig').test(a.tableName) : true
})
}
</script>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -0,0 +1,416 @@
<template>
<div>
<el-card>
<el-button type="primary" icon="plus" @click="editMongo(true)" plain>添加</el-button>
<el-button type="primary" icon="edit" :disabled="currentId == null" @click="editMongo(false)" plain>编辑
</el-button>
<el-button type="danger" icon="delete" :disabled="currentId == null" @click="deleteMongo" plain>删除
</el-button>
<div style="float: right">
<el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" filterable clearable>
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button>
</div>
<el-table :data="list" style="width: 100%" @current-change="choose" stripe>
<el-table-column label="选择" width="60px">
<template #default="scope">
<el-radio v-model="currentId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column>
<el-table-column prop="uri" label="连接uri" min-width="150" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.uri.split('@')[1] }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="150">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人"></el-table-column>
<el-table-column label="操作" width>
<template #default="scope">
<el-link type="primary" @click="showDatabases(scope.row.id)" plain size="small"
:underline="false">数据库</el-link>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination style="text-align: right" @current-change="handlePageChange" :total="total"
layout="prev, pager, next, total, jumper" v-model:current-page="query.pageNum"
:page-size="query.pageSize"></el-pagination>
</el-row>
</el-card>
<el-dialog width="800px" :title="databaseDialog.title" v-model="databaseDialog.visible">
<el-table :data="databaseDialog.data" size="small">
<el-table-column min-width="130" property="Name" label="库名" />
<el-table-column min-width="90" property="SizeOnDisk" label="size">
<template #default="scope">
{{ formatByteSize(scope.row.SizeOnDisk) }}
</template>
</el-table-column>
<el-table-column min-width="80" property="Empty" label="是否为空" />
<el-table-column min-width="150" label="操作">
<template #default="scope">
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small"
:underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small"
:underline="false">集合</el-link>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="databaseDialog.statsDialog.title"
v-model="databaseDialog.statsDialog.visible">
<el-descriptions title="库状态信息" :column="3" border size="small">
<el-descriptions-item label="db" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.db }}
</el-descriptions-item>
<el-descriptions-item label="collections" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.collections }}
</el-descriptions-item>
<el-descriptions-item label="objects" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.objects }}
</el-descriptions-item>
<el-descriptions-item label="indexes" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.indexes }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="dataSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.dataSize) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsTotalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsTotalSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsUsedSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsUsedSize) }}
</el-descriptions-item>
<el-descriptions-item label="indexSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.indexSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="600px" :title="collectionsDialog.title" v-model="collectionsDialog.visible">
<div>
<el-button @click="showCreateCollectionDialog" type="primary" icon="plus" size="small">新建</el-button>
</div>
<el-table border stripe :data="collectionsDialog.data" size="small">
<el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column min-width="80" label="操作">
<template #default="scope">
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small"
:underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteCollection(scope.row.name)" title="确定删除该集合?">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="collectionsDialog.statsDialog.title"
v-model="collectionsDialog.statsDialog.visible">
<el-descriptions title="集合状态信息" :column="3" border size="small">
<el-descriptions-item label="ns" label-align="right" :span="2" align="center">
{{ collectionsDialog.statsDialog.data.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="nindexes" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.nindexes }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="400px" title="新建集合" v-model="createCollectionDialog.visible" :destroy-on-close="true">
<el-form :model="createCollectionDialog.form" label-width="70px">
<el-form-item prop="name" label="集合名" required>
<el-input v-model="createCollectionDialog.form.name" clearable></el-input>
</el-form-item>
<!-- <el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item> -->
</el-form>
<template #footer>
<div>
<el-button @click="createCollectionDialog.visible = false"> </el-button>
<el-button @click="onCreateCollection" type="primary"> </el-button>
</div>
</template>
</el-dialog>
<mongo-edit @val-change="valChange" :title="mongoEditDialog.title" v-model:visible="mongoEditDialog.visible"
v-model:mongo="mongoEditDialog.data"></mongo-edit>
</div>
</template>
<script lang="ts" setup>
import { mongoApi } from './api';
import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from '../tag/api.ts';
import MongoEdit from './MongoEdit.vue';
import { formatByteSize } from '@/common/utils/format';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
const state = reactive({
tags: [],
dbOps: {
dbId: 0,
db: '',
},
list: [],
total: 0,
currentId: null,
currentData: null as any,
query: {
pageNum: 1,
pageSize: 10,
tagPath: null,
},
mongoEditDialog: {
visible: false,
data: null as any,
title: '新增mongo',
},
databaseDialog: {
visible: false,
data: [],
title: '',
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
collectionsDialog: {
database: '',
visible: false,
data: [],
title: '',
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
createCollectionDialog: {
visible: false,
form: {
name: '',
},
},
});
const {
tags,
list,
total,
currentId,
query,
mongoEditDialog,
databaseDialog,
collectionsDialog,
createCollectionDialog,
} = toRefs(state)
onMounted(async () => {
search();
});
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
const showDatabases = async (id: number) => {
// state.query.tagPath = row.tagPath
state.dbOps.dbId = id
state.databaseDialog.data = (await mongoApi.databases.request({ id })).Databases;
state.databaseDialog.title = `数据库列表`;
state.databaseDialog.visible = true;
};
const showDatabaseStats = async (dbName: string) => {
state.databaseDialog.statsDialog.data = await mongoApi.runCommand.request({
id: state.currentId,
database: dbName,
command: {
dbStats: 1,
},
});
state.databaseDialog.statsDialog.title = `'${dbName}' stats`;
state.databaseDialog.statsDialog.visible = true;
};
const showCollections = async (database: string) => {
state.collectionsDialog.database = database;
state.collectionsDialog.data = [];
setCollections(database);
state.collectionsDialog.title = `'${database}' 集合`;
state.collectionsDialog.visible = true;
};
const setCollections = async (database: string) => {
const res = await mongoApi.collections.request({ id: state.currentId, database });
const collections = [] as any;
for (let r of res) {
collections.push({ name: r });
}
state.collectionsDialog.data = collections;
};
/**
* 显示集合状态
*/
const showCollectionStats = async (collection: string) => {
state.collectionsDialog.statsDialog.data = await mongoApi.runCommand.request({
id: state.currentId,
database: state.collectionsDialog.database,
command: {
collStats: collection,
},
});
state.collectionsDialog.statsDialog.title = `'${collection}' stats`;
state.collectionsDialog.statsDialog.visible = true;
};
/**
* 删除集合
*/
const onDeleteCollection = async (collection: string) => {
await mongoApi.runCommand.request({
id: state.currentId,
database: state.collectionsDialog.database,
command: {
drop: collection,
},
});
ElMessage.success('集合删除成功');
setCollections(state.collectionsDialog.database);
};
const showCreateCollectionDialog = () => {
state.createCollectionDialog.visible = true;
};
const onCreateCollection = async () => {
const form = state.createCollectionDialog.form;
await mongoApi.runCommand.request({
id: state.currentId,
database: state.collectionsDialog.database,
command: {
create: form.name,
},
});
ElMessage.success('集合创建成功');
state.createCollectionDialog.visible = false;
state.createCollectionDialog.form = {} as any;
setCollections(state.collectionsDialog.database);
};
const deleteMongo = async () => {
try {
await ElMessageBox.confirm(`确定删除该mongo?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await mongoApi.deleteMongo.request({ id: state.currentId });
ElMessage.success('删除成功');
state.currentData = null;
state.currentId = null;
search();
} catch (err) { }
};
const search = async () => {
const res = await mongoApi.mongoList.request(state.query);
state.list = res.list;
state.total = res.total;
};
const getTags = async () => {
state.tags = await tagApi.getAccountTags.request(null);
};
const editMongo = async (isAdd = false) => {
if (isAdd) {
state.mongoEditDialog.data = null;
state.mongoEditDialog.title = '新增mongo';
} else {
state.mongoEditDialog.data = state.currentData;
state.mongoEditDialog.title = '修改mongo';
}
state.mongoEditDialog.visible = true;
};
const valChange = () => {
state.currentId = null;
state.currentData = null;
search();
};
</script>
<style>
</style>

View File

@@ -0,0 +1,14 @@
import Api from '@/common/Api';
export const mongoApi = {
mongoList : Api.create("/mongos", 'get'),
saveMongo : Api.create("/mongos", 'post'),
deleteMongo : Api.create("/mongos/{id}", 'delete'),
databases: Api.create("/mongos/{id}/databases", 'get'),
collections: Api.create("/mongos/{id}/collections", 'get'),
runCommand: Api.create("/mongos/{id}/run-command", 'post'),
findCommand: Api.create("/mongos/{id}/command/find", 'post'),
updateByIdCommand: Api.create("/mongos/{id}/command/update-by-id", 'post'),
deleteByIdCommand: Api.create("/mongos/{id}/command/delete-by-id", 'post'),
insertCommand: Api.create("/mongos/{id}/command/insert", 'post'),
}

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