107 Commits

Author SHA1 Message Date
meilin.huang
dd4ac390de refactor: 界面小优化 2023-09-14 17:21:01 +08:00
meilin.huang
0bd7d38c23 fix: 切换暗模式时编辑器主题同步调整 2023-09-13 23:57:28 +08:00
meilin.huang
ead3b0d0d8 feat: 界面新增暗模式 2023-09-13 19:54:43 +08:00
meilin.huang
4b973b22a4 refactor: 系统websocket消息重构 2023-09-12 20:54:07 +08:00
meilin.huang
e4e68d02bc feat: 机器文件优化&sql新增引号包裹 2023-09-11 22:59:13 +08:00
meilin.huang
ef8822d671 refactor: monaco编辑器按需加载 2023-09-11 17:34:24 +08:00
meilin.huang
8e75e1f6ef refactor: 部分日志请求入参调整为json 2023-09-09 23:34:26 +08:00
meilin.huang
08c381fa60 feat: 机器文件支持文件夹上传&数据库列表组件拆分 2023-09-08 22:24:45 +08:00
may-fly
d7a10d4032 Merge pull request #60 from kanzihuang/feature-export-databases
fix: 前端 DBMS->数据操作->新建查询,页面不显示查询结果
2023-09-08 13:53:17 +08:00
wanli
c324a030f9 fix: 前端 DBMS->数据操作->新建查询,页面不显示查询结果 2023-09-08 12:21:24 +08:00
may-fly
b618b8f93b Merge pull request #58 from kanzihuang/feature-export-databases
feat: 优化数据库批量导出功能
2023-09-07 19:55:53 +08:00
wanli
4d2e110e1e refactor: 优化数据库导出速度 2023-09-07 19:34:35 +08:00
may-fly
ecd79a2e15 Merge pull request #57 from kanzihuang/feature-export-databases
feat: 批量导出数据库时可按名称筛选数据库
2023-09-07 17:20:42 +08:00
wanli
f4f297d3f7 refactor: 优化数据库导出速度 2023-09-07 16:51:49 +08:00
kanzihuang
b5549c0fae fix: 数据库导出失败时将错误提示输出到 SQL 文件 2023-09-07 16:41:47 +08:00
kanzihuang
929bfb3200 fix: 数据库导出失败时向前端发消息 2023-09-07 16:41:47 +08:00
kanzihuang
7d3593a944 feat: 批量导出数据库时可按名称筛选数据库 2023-09-07 16:41:37 +08:00
meilin.huang
9e0db2bc99 feat: 机器文件新增批量删除、copy、mv、rename等操作 2023-09-07 16:33:53 +08:00
meilin.huang
25b0d276b3 refactor: 机器文件操作界面重构 2023-09-06 18:06:52 +08:00
may-fly
0cb7a7cf83 Merge pull request #56 from kanzihuang/feature-export-databases
feat: 批量导出数据库(仅支持 MySQL 数据库)
2023-09-05 15:28:07 +08:00
wanli
52f72400ba refactor: 获取数据库连接调整 2023-09-05 15:22:16 +08:00
wanli
0eaff33168 fix: 由于批量导出数据库不支持 PostgreSQL,非 MySQL 数据库限制使用该功能。 2023-09-05 14:50:45 +08:00
wanli
086dbf278b feature: 导出数据库时采用gzip压缩 2023-09-05 14:50:45 +08:00
wanli
57a5e237ae fix: 执行脚SQL脚本时解析SQL失败
SQL脚本中包含use mayfly-go,应为use `mayfly-go`。
2023-09-05 14:50:45 +08:00
wanli
eee6cf7b14 fix: 导出数据库时获取表结构失败
原因是表名为 group, 应在表名前后添加 `` 符号
2023-09-05 14:50:45 +08:00
wanli
b9c6ac8d6d feature: 批量导出数据库 2023-09-05 14:50:45 +08:00
meilin.huang
618d782af3 refactor: 获取数据库连接调整 2023-09-05 14:41:12 +08:00
meilin.huang
d0ac7de4cb review: 数据库实例管理调整 2023-09-05 12:49:12 +08:00
may-fly
baf8053613 Merge pull request #55 from kanzihuang/feature-instance
feature: 从数据库管理模块中拆分数据库实例管理功能
2023-09-05 08:44:58 +08:00
wanli
b973d63331 refactor: 数据库实例表 t_instance 改为 t_db_instance
同时,为避免混淆,将 application.DbInstance 改为 application.DbConnection
2023-09-05 08:13:32 +08:00
kanzihuang
85b64d7e8d feature: 拆分数据库实例的SQL脚本 2023-09-04 23:13:05 +08:00
kanzihuang
86ad183c41 feature: 将数据库实例管理集成到数据库管理模块中 2023-09-04 23:12:50 +08:00
kanzihuang
f7b685cfad feature: 实现数据库实例管理 2023-09-04 23:09:28 +08:00
meilin.huang
649116a0b8 refactor: 日志堆栈描述调整 2023-09-03 13:04:29 +08:00
meilin.huang
899a3a8243 refactor: slog替换logrus、日志操作统一、支持json、text格式等 2023-09-02 17:24:18 +08:00
meilin.huang
d51cd4b289 refactor: 终端重构、系统参数配置调整 2023-08-31 21:49:20 +08:00
meilin.huang
537b179e78 fix: 数据导出精度修复、redis数据操作优化 2023-08-29 21:31:08 +08:00
meilin.huang
1e5b1868ab refactor: redis操作界面重构 2023-08-28 17:25:59 +08:00
may-fly
245406673c Merge pull request #54 from kanzihuang/fix-ldap-login
fix: LDAP login
2023-08-26 12:37:29 +08:00
kanzihuang
51fa197af6 fix: LDAP login 2023-08-26 12:06:29 +08:00
meilin.huang
649b2bb165 refactor: ldap登录配置迁移至系统配置 2023-08-25 23:26:14 +08:00
meilin.huang
3634c902d0 feat: 系统配置新增权限控制 2023-08-25 19:41:52 +08:00
may-fly
756e580469 Merge pull request #53 from kanzihuang/feature-ldap
feat: 实现 LDAP 登录
2023-08-25 12:10:16 +08:00
kanzihuang
4e1350d1cc feat: 实现 LDAP 登录 2023-08-25 03:48:42 +00:00
meilin.huang
2e969d46fb feat: mongo优化 2023-08-25 10:20:32 +08:00
Coder慌
a5bcbe151d !58 feat:sql编写体验优化
Merge pull request !58 from zongyangleo/dev_0823
2023-08-23 05:42:22 +00:00
刘宗洋
c4abba361a feat:sql编写体验优化
1.添加自定义关键字
2.自定义函数注释和模板
3.点击按钮添加limit
2023-08-23 12:41:58 +08:00
meilin.huang
24b46b1133 refactor: 代码小优化 2023-08-16 17:37:33 +08:00
meilin.huang
3ae7e0de75 refactor: validator调整 2023-08-04 12:22:21 +08:00
meilin.huang
c2ee4f9955 refactor: 后端validator校验错误转译 2023-07-31 17:34:32 +08:00
may-fly
2479412334 Merge pull request #46 from CodFrm/dev
ci: 优化镜像构建和docker compose
2023-07-28 16:12:50 +08:00
王一之
6da8d7fd67 ci: 优化镜像构建和docker compose 2023-07-28 13:56:32 +08:00
meilin.huang
0f596a712d feat: 数据库表查询支持页数选择 2023-07-27 16:47:05 +08:00
meilin.huang
8f37b71d7f refactor: oauth2登录优化 2023-07-26 10:24:32 +08:00
meilin.huang
5083b2bdfe refactor: oauth2登录调整 2023-07-24 22:36:07 +08:00
meilin.huang
155ae65b4a refactor: oauth2登录重构 2023-07-22 20:51:46 +08:00
may-fly
ffacfc3ae8 Merge pull request #44 from CodFrm/develop/auth
OAuth2 登录和数据库升级问题优化
2023-07-22 11:53:11 +08:00
王一之
b1ab66ecf9 feat: 优化数据库迁移与添加老表迁移 2023-07-22 00:48:41 +08:00
王一之
f5bb0cad3e fix: 修复记录ip与归属地信息问题 2023-07-21 22:46:55 +08:00
王一之
a0de5afcb0 chore: 处理合并问题 2023-07-21 22:29:41 +08:00
王一之
358d33d60e fix: 修复OAuth2绑定按钮逻辑错误 2023-07-21 22:04:38 +08:00
王一之
062d28b6e6 feat: OAuth2 登录 2023-07-21 22:04:38 +08:00
王一之
513f8ea012 wip: oauth2登录和oauth2 otp登录验证 2023-07-21 22:04:37 +08:00
王一之
179b58e557 wip: 自定义oauth2登录配置 2023-07-21 22:01:53 +08:00
meilin.huang
b7450f8869 fix: 前端界面小问题修复 2023-07-21 21:01:25 +08:00
meilin.huang
7f9e972828 feat: 代码优化、机器计划任务完善 2023-07-21 17:07:04 +08:00
meilin.huang
7b51705f4e feat: 新增机器计划任务、数据物理删除调整为逻辑删除、支持记录登录ip归属地等 2023-07-20 22:41:13 +08:00
Coder慌
6bd9e5333d !55 https://gitee.com/objs/mayfly-go/issues/I7LFVH问题解决
Merge pull request !55 from amell/databases_404_fix
2023-07-16 04:04:22 +00:00
Coder慌
112d735ac0 !56 https://gitee.com/objs/mayfly-go/issues/I7LFXS的BUG修复
Merge pull request !56 from amell/dev
2023-07-16 04:04:00 +00:00
amell
52553ed53f [fix] https://gitee.com/objs/mayfly-go/issues/I7LFXS的缺陷修复 2023-07-16 11:13:52 +08:00
amell
70d84e32d1 [fix] https://gitee.com/objs/mayfly-go/issues/I7LFVH的缺陷修复 2023-07-16 09:44:55 +08:00
meilin.huang
3269dfa5d6 refactor: 后端路由定义方式&请求参数绑定重构 2023-07-08 20:05:55 +08:00
meilin.huang
183a6e4905 refactor: 团队成员分配使用pagetable组件重构 2023-07-07 14:43:51 +08:00
meilin.huang
5463ae9d7e refactor: 前端统一使用prettier格式化&枚举值统一管理 2023-07-06 20:59:22 +08:00
Coder慌
f25bdb07ce !54 fix: 修复字段粘连无法提示的问题
Merge pull request !54 from zongyangleo/dev_0706
2023-07-06 06:54:28 +00:00
刘宗洋
aa5c08d564 fix: 修复字段粘连无法提示的问题 2023-07-06 11:08:00 +08:00
meilin.huang
dc9a2985f3 refactor: 分页组件统一替换&其他优化 2023-07-05 22:06:32 +08:00
meilin.huang
f4ac6d8360 refactor: 机器文件操作优化 2023-07-05 00:26:00 +08:00
meilin.huang
3266039aaf fix: 双击修改表sql问题修复 2023-07-04 15:07:03 +08:00
meilin.huang
e3f4c298b0 feat: 系统日志支持按描述搜索 2023-07-04 14:13:47 +08:00
meilin.huang
fa58f6d2de fix: 无法添加成员 2023-07-03 21:58:37 +08:00
meilin.huang
ae5a1fd7de refactor: code review 2023-07-03 21:42:04 +08:00
meilin.huang
c240079df4 refactor: 前端代码优化 2023-07-02 17:06:00 +08:00
meilin.huang
aca4e6751e refactor: 前端风格统一 2023-07-01 21:24:07 +08:00
meilin.huang
ce32fc7f2c refactor: 代码重构、分页数据组件支持多选 2023-07-01 14:34:42 +08:00
meilin.huang
d423572e01 fix: 新增资源选择标签问题修复、项目文档转移 2023-06-29 20:20:58 +08:00
meilin.huang
d9807b1bf0 feat: 数据库表数据支持字段设置、表格宽度自适应调整 2023-06-29 11:49:14 +08:00
meilin.huang
1bc53b4c80 refactor: 表格宽度自适应宽度计算方式调整 2023-06-29 00:40:42 +08:00
meilin.huang
6bc2603a4d fix: 查询label宽度自动适配 2023-06-28 21:50:04 +08:00
meilin.huang
e2c929aae1 feat: 统一分页表格组件、修复系统配置无法配置单个属性 2023-06-28 21:35:03 +08:00
Coder慌
0d155d592b !53 feat:修改表结构:主键默认自增,自动生成索引名
Merge pull request !53 from zongyangleo/dev_0625
2023-06-26 01:26:51 +00:00
Coder慌
ae510ff1ff !52 update server/mayfly-go.sql.
Merge pull request !52 from noday/N/A
2023-06-26 01:24:48 +00:00
noday
5b0654ad2c update server/mayfly-go.sql.
sql values字段不匹配
2023-06-26 01:18:48 +00:00
刘宗洋
466f97ecbe feat:修改表结构:主键默认自增,自动生成索引名 2023-06-25 21:12:28 +08:00
meilin.huang
27a14c22d7 fix: sql遗漏调整 2023-06-25 19:10:37 +08:00
meilin.huang
4709edcd1c fix: 遗漏sql补充 2023-06-21 15:18:43 +08:00
meilin.huang
414de9f2eb fix: 数据库提示问题修复 2023-06-20 17:08:13 +08:00
Coder慌
a53e7e7dab !50 SQL字段补全的BUG修复
Merge pull request !50 from amell/sql_autocomplete
2023-06-17 11:42:39 +00:00
amell
7fa6628dc5 [bugfix] issue修复: https://gitee.com/objs/mayfly-go/issues/I7E9LF 2023-06-17 19:29:19 +08:00
Coder慌
62c25afea8 !49 对于update和delete的SQL操作,建议增加where条件检测,缺失where条件时不执行相应的SQL
Merge pull request !49 from amell/sql_where
2023-06-17 09:25:02 +00:00
amell
481b622e3b [fix] https://gitee.com/objs/mayfly-go/issues/I7E8ZF的修复 2023-06-17 16:57:06 +08:00
meilin.huang
64f8f9a200 fix: meta_sql文件中windows换行符不同问题 2023-06-17 16:04:21 +08:00
meilin.huang
0eca951465 Merge branch 'dev' 2023-06-17 15:15:56 +08:00
meilin.huang
ef4e34c584 feat: 新增双因素校验(OTP) 2023-06-17 15:15:03 +08:00
Coder慌
d91acbc7ee !45 修复同时执行多条SQL的bug
Merge pull request !45 from amell/mutil_sql
2023-06-17 04:47:48 +00:00
amell
b397d1022e [fix] issue: I7E6QQ的缺陷修复 2023-06-17 08:59:37 +08:00
meilin.huang
b42a98aff5 fix: sql脚本问题修复 2023-06-16 08:59:22 +08:00
439 changed files with 19637 additions and 17726 deletions

6
.gitignore vendored
View File

@@ -17,4 +17,10 @@
*/node_modules/
**/vendor/
.idea
.vscode
out
server/docs/docker-compose
server/config.yml
server/ip2region.xdb
mayfly-go.log

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# 构建前端资源
FROM node:18-alpine3.16 as fe-builder
WORKDIR /mayfly
COPY mayfly_go_web .
RUN yarn
RUN yarn build
# 构建后端资源
FROM golang:1.21.0 as be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
# Copy the go source for building server
COPY server .
RUN go mod download
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \
-o mayfly-go main.go
FROM alpine:3.16
RUN apk add --no-cache ca-certificates bash expat
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /mayfly
COPY --from=be-builder /mayfly/mayfly-go /usr/local/bin/mayfly-go
CMD ["mayfly-go"]

3
Makefile Normal file
View File

@@ -0,0 +1,3 @@
docker:
docker build . -t mayfly-go

View File

@@ -13,75 +13,91 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.18%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.20%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 统一管理操作平台**
### 开发语言与主要框架
- 前端typescript、vue3、element-plus
- 后端golang、gin、gorm
### 交流及问题反馈加 QQ 群
<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://www.yuque.com/may-fly/mayfly-go
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
### 演示环境
http://go.mayfly.run
账号/密码test/test123.
### 系统核心功能截图
##### 记录操作记录
![记录操作记录](https://images.gitee.com/uploads/images/2021/0508/204608_83ef7c33_1240250.png "屏幕截图.png")
![记录操作记录](https://objs.gitee.io/mayfly-go-docs/home/log.jpg "屏幕截图.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://www.yuque.com/may-fly/mayfly-go
**其他更多功能&操作指南可查看在线文档**: https://objs.gitee.io/mayfly-go-docs
#### 💌 支持作者
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/may-fly/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/objs/mayfly-go">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。

View File

@@ -74,8 +74,8 @@ function build() {
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml ${toFolder}
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
@@ -104,9 +104,8 @@ function buildMac() {
function buildDocker() {
echo_yellow "-------------------构建docker镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="mayflygo/mayfly-go:${imageVersion}"
docker build -t "${imageName}" .
docker build --platform linux/amd64 -t "${imageName}" .
echo_green "docker镜像构建完成->[${imageName}]"
echo_yellow "-------------------构建docker镜像结束-------------------"
}
@@ -114,7 +113,6 @@ function buildDocker() {
function buildxDocker() {
echo_yellow "-------------------docker buildx构建镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${imageVersion}"
docker buildx build --push --platform linux/amd64,linux/arm64 -t "${imageName}" .
echo_green "docker多版本镜像构建完成->[${imageName}]"
@@ -147,6 +145,11 @@ function runBuild() {
# 进入目标路径,并赋值全路径
cd ${toPath}
toPath=`pwd`
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
runBuildWeb="2"
# 编译web前端
buildWeb ${runBuildWeb}
fi
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
@@ -157,12 +160,6 @@ function runBuild() {
fi
fi
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
runBuildWeb="2"
# 编译web前端
buildWeb ${runBuildWeb}
case ${buildType} in
"1")
buildLinuxAmd64 ${toPath} ${copyDocScript}
@@ -190,11 +187,13 @@ function runBuild() {
;;
esac
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
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
fi
}
runBuild

View File

@@ -2,19 +2,15 @@ version: "3.9"
services:
mysql:
image: "mysql:5.7"
image: "mysql:8"
container_name: mayfly-go-mysql
environment:
MYSQL_ROOT_PASSWORD: 111049
MYSQL_DATABASE: mayfly-go
TZ: Asia/Shanghai
volumes:
- ./docs/docker-compose/mysql/data/mydir:/mydir
- ./docs/docker-compose/mysql/data/datadir:/var/lib/mysql
# 在宿主机编写 /apps/mysql/conf/my.cnf
- ./docs/docker-compose/mysql/my.cnf:/etc/my.cnf
# 数据库还原目录 可将需要还原的sql文件放在这里
- ./docs/docker-compose/mysql/init:/docker-entrypoint-initdb.d
- ./server/docs/docker-compose/mysql/data/mydir:/mydir
- ./server/docs/docker-compose/mysql/data/datadir:/var/lib/mysql
restart: always
server:
@@ -28,6 +24,8 @@ services:
environment:
TZ: Asia/Shanghai
WAIT_HOSTS: mysql:3306
volumes:
- ./server/config.yml.example:/mayfly/config.yml
depends_on:
- mysql
restart: always

View File

@@ -1,6 +1,6 @@
module.exports = {
// 一行最多多少个字符
printWidth: 150,
printWidth: 160,
// 指定每个缩进级别的空格数
tabWidth: 4,
// 使用制表符而不是空格缩进行

File diff suppressed because it is too large Load Diff

View File

@@ -10,28 +10,31 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.3.0",
"axios": "^1.4.0",
"countup.js": "^2.0.7",
"asciinema-player": "^3.5.0",
"axios": "^1.5.0",
"countup.js": "^2.7.0",
"cropperjs": "^1.5.11",
"echarts": "^5.4.0",
"element-plus": "^2.3.5",
"element-plus": "^2.3.12",
"jsencrypt": "^3.3.1",
"lodash": "^4.17.21",
"mitt": "^3.0.0",
"monaco-editor": "^0.38.0",
"mitt": "^3.0.1",
"monaco-editor": "^0.43.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.3",
"pinia": "^2.1.6",
"qrcode.vue": "^3.4.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sql-formatter": "^12.1.2",
"vue": "^3.3.4",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.2.2",
"xterm": "^5.2.1",
"xterm-addon-fit": "^0.7.0"
"vue-router": "^4.2.4",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-web-links": "^0.9.0"
},
"devDependencies": {
"@types/lodash": "^4.14.178",
@@ -49,7 +52,7 @@
"sass": "^1.62.0",
"sass-loader": "^13.2.0",
"typescript": "^5.0.2",
"vite": "^4.3.9",
"vite": "^4.4.9",
"vue-eslint-parser": "^9.1.1"
},
"browserslist": [

View File

@@ -1,24 +1,25 @@
window.globalConfig = {
// 默认为空以访问根目录为api请求地址。若前后端分离部署可单独配置该后端api请求地址
"BaseApiUrl": "",
"BaseWsUrl": ""
}
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)
// !(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
}
}
}()
// console.log(location);
// 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

@@ -44,7 +44,7 @@ onMounted(() => {
});
// 获取缓存中的布局配置
if (getLocal('themeConfig')) {
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') })
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
}
});

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import request from './request'
import request from './request';
/**
* 可用于各模块定义各自api请求
@@ -27,23 +27,13 @@ class Api {
}
/**
* 操作该权限,即请求对应的url
* 请求对应的该api
* @param {Object} param 请求该api的参数
*/
request(param: any = null, options: any = null): Promise<any> {
return request.send(this, param, options);
request(param: any = null, options: any = null, headers: any = null): Promise<any> {
return request.request(this.method, this.url, param, headers, options);
}
/**
* 操作该权限即请求对应的url
* @param {Object} param 请求该api的参数
* @param headers headers
*/
requestWithHeaders(param: any, headers: any): Promise<any> {
return request.sendWithHeaders(this, param, headers);
}
/** 静态方法 **/
/**
@@ -88,5 +78,4 @@ class Api {
}
}
export default Api
export default Api;

View File

@@ -1,37 +1,94 @@
/**
* 枚举类
* @author meilin.huang
*/
export class Enum {
/**
* 添加枚举字段
*
* @param {string} field 枚举字段名
* @param {string} label 枚举名称
* @param {Object} value 枚举值
*/
add(field: string, label: string, value: any) {
this[field] = { label, value }
return this
export interface EnumValueTag {
color?: string;
type?: string;
}
/**
* 根据枚举value获取其label
*
* @param {Object} value
* 枚举值
*/
getLabelByValue(value: any) {
// 字段不存在返回‘’
if (value === undefined || value === null) {
return ''
export class EnumValue {
/**
* 枚举值
*/
value: any;
/**
* 枚举描述
*/
label: string;
/**
* 展示的标签信息
*/
tag: EnumValueTag;
constructor(value: any, label: string) {
this.value = value;
this.label = label;
}
for (const i in this) {
const e: any = this[i]
if (e && e.value === value) {
return e.label
setTagType(type: string = 'primary'): EnumValue {
this.tag = { type };
return this;
}
tagTypeInfo(): EnumValue {
return this.setTagType('info');
}
tagTypeSuccess(): EnumValue {
return this.setTagType('success');
}
tagTypeDanger(): EnumValue {
return this.setTagType('danger');
}
tagTypeWarning(): EnumValue {
return this.setTagType('warning');
}
setTagColor(color: string): EnumValue {
this.tag = { color };
return this;
}
public static of(value: any, label: string): EnumValue {
return new EnumValue(value, label);
}
/**
* 根据枚举值获取指定枚举值对象
*
* @param enumValues 所有枚举值
* @param value 需要匹配的枚举值
* @returns 枚举值对象
*/
static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null {
for (let enumValue of enumValues) {
if (enumValue.value == value) {
return enumValue;
}
}
return null;
}
/**
* 根据枚举值获取枚举描述
*
* @param enums 枚举对象
* @param value 枚举值
* @returns 枚举描述
*/
static getLabelByValue(enums: any, value: any) {
const enumValues = Object.values(enums) as any;
for (let enumValue of enumValues) {
if (enumValue['value'] == value) {
return enumValue['label'];
}
}
return '';
}
}
return ''
}
}
export default EnumValue;

View File

@@ -1,9 +1,8 @@
class SocketBuilder {
websocket: WebSocket;
constructor(url: string) {
if (typeof (WebSocket) === "undefined") {
if (typeof WebSocket === 'undefined') {
throw new Error('不支持websocket');
}
if (!url) {
@@ -41,6 +40,4 @@ class SocketBuilder {
}
}
export default SocketBuilder;

View File

@@ -5,7 +5,7 @@ class AssertError extends Error {
constructor(message: string) {
super(message);
// 错误类名
this.name = "AssertError";
this.name = 'AssertError';
}
}
@@ -28,7 +28,7 @@ class AssertError extends Error {
* @param msg 错误消息
*/
export function notBlank(obj: any, msg: string) {
isTrue(obj, msg)
isTrue(obj, msg);
}
/**
@@ -50,7 +50,7 @@ class AssertError extends Error {
*/
export function notNull(obj: any, msg: string) {
if (obj == null || obj == undefined) {
throw new AssertError(msg)
throw new AssertError(msg);
}
}

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
interface BaseEnum {
name: string
value: any
}
const success: BaseEnum = {
name: 'success',
value: 200
}
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501
}
// /**
// * 全局公共枚举类
// */
// export default {
// // uri请求方法
// requestMethod: new Enum().add('GET', 'GET', 1).add('POST', 'POST', 2).add('PUT', 'PUT', 3).add('DELETE', 'DELETE', 4),
// // 结果枚举
// ResultEnum: new Enum().add('SUCCESS', '操作成功', 200).add('ERROR', '操作失败', 400).add('PARAM_ERROR', '参数错误', 405).add('SERVER_ERROR', '服务器异常', 500)
// .add('NO_PERMISSION', '没有权限', 501)
// }

View File

@@ -1,11 +1,16 @@
import Api from './Api'
import request from './request';
export default {
login: Api.newPost("/sys/accounts/login"),
changePwd: Api.newPost("/sys/accounts/change-pwd"),
getPublicKey: Api.newGet("/common/public-key"),
getConfigValue: Api.newGet("/sys/configs/value"),
captcha: Api.newGet("/sys/captcha"),
logout: Api.newPost("/sys/accounts/logout/{token}"),
getPermissions: Api.newGet("/sys/accounts/permissions")
}
login: (param: any) => request.post('/auth/accounts/login', param),
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
oauth2LoginConfig: () => request.get('/auth/oauth2-config'),
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
captcha: () => request.get('/sys/captcha'),
logout: () => request.post('/auth/accounts/logout'),
getPermissions: () => request.get('/sys/accounts/permissions'),
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
};

View File

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

View File

@@ -1,7 +1,5 @@
import router from "../router";
import router from '../router';
import Axios from 'axios';
import { ResultEnum } from './enums'
import Api from './Api';
import config from './config';
import { getSession } from './utils/storage';
import { templateResolve } from './utils/string';
@@ -22,8 +20,16 @@ export interface Result {
data?: any;
}
const baseUrl: string = config.baseApiUrl
const baseWsUrl: string = config.baseWsUrl
enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501,
}
const baseUrl: string = config.baseApiUrl;
const baseWsUrl: string = config.baseWsUrl;
/**
* 通知错误消息
@@ -37,60 +43,72 @@ function notifyErrorMsg(msg: string) {
// create an axios instance
const service = Axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 20000 // request timeout
})
timeout: 20000, // request timeout
});
// request interceptor
service.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getSession("token")
const token = getSession('token');
if (token) {
// 设置token
config.headers['Authorization'] = token
config.headers['Authorization'] = token;
}
return config
return config;
},
error => {
return Promise.reject(error)
(error) => {
return Promise.reject(error);
}
)
);
// response interceptor
service.interceptors.response.use(
response => {
(response) => {
// 获取请求返回结果
const data: Result = response.data;
if (data.code === ResultEnum.SUCCESS) {
return data.data;
}
// 如果提示没有权限则移除token使其重新登录
if (data.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
if (data.code === ResultEnum.SUCCESS) {
return data.data;
} else {
return Promise.reject(data);
}
},
(e: any) => {
const rejectPromise = Promise.reject(e);
const statusCode = e.response?.status;
if (statusCode == 500) {
notifyErrorMsg('服务器未知异常');
return rejectPromise;
}
if (statusCode == 404) {
notifyErrorMsg('请求接口未找到');
return rejectPromise;
}
if (e.message) {
// 对响应错误做点什么
if (e.message.indexOf('timeout') != -1) {
notifyErrorMsg('网络超时');
} else if (e.message == 'Network Error') {
notifyErrorMsg('网络请求超时');
return rejectPromise;
}
if (e.message == 'Network Error') {
notifyErrorMsg('网络连接错误');
} else if (e.message.indexOf('404')) {
notifyErrorMsg('请求接口找不到');
} else {
if (e.response.data) ElMessage.error(e.response.statusText);
else notifyErrorMsg('接口路径找不到');
return rejectPromise;
}
}
return Promise.reject(e)
notifyErrorMsg('网络请求错误');
return rejectPromise;
}
)
);
/**
* 请求uri
@@ -101,19 +119,18 @@ service.interceptors.response.use(
* @param {Object} params 参数
*/
function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
if (!url)
throw new Error('请求url不能为空');
if (!url) throw new Error('请求url不能为空');
// 简单判断该url是否是restful风格
if (url.indexOf("{") != -1) {
if (url.indexOf('{') != -1) {
url = templateResolve(url, params);
}
const query: any = {
method,
url: url,
...options
...options,
};
if (headers) {
query.headers = headers
query.headers = headers;
}
// post和put使用json格式传参
@@ -122,32 +139,39 @@ function request(method: string, url: string, params: any = null, headers: any =
} else {
query.params = params;
}
return service.request(query).then(res => res)
.catch(e => {
return service
.request(query)
.then((res) => res)
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可
if (e.msg) {
notifyErrorMsg(e.msg)
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
});
}
/**
* 根据api执行对应接口
* @param api Api实例
* @param params 请求参数
* get请求uri
* 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
*
* @param {Object} url uri
* @param {Object} params 参数
*/
function send(api: Api, params: any, options: any): Promise<any> {
return request(api.method, api.url, params, null, options);
function get(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('get', url, params, headers, options);
}
/**
* 根据api执行对应接口
* @param api Api实例
* @param params 请求参数
*/
function sendWithHeaders(api: Api, params: any, headers: any): Promise<any> {
return request(api.method, api.url, params, headers, null);
function post(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('post', url, params, headers, options);
}
function put(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('put', url, params, headers, options);
}
function del(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('delete', url, params, headers, options);
}
function getApiUrl(url: string) {
@@ -155,10 +179,11 @@ function getApiUrl(url: string) {
return baseUrl + url + '?token=' + getSession('token');
}
export default {
request,
send,
sendWithHeaders,
getApiUrl
}
get,
post,
put,
del,
getApiUrl,
};

View File

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

View File

@@ -1,8 +1,7 @@
import Config from './config'
import { ElNotification } from 'element-plus'
import Config from './config';
import { ElNotification } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getSession } from '@/common/utils/storage.ts';
import { getSession } from '@/common/utils/storage';
export default {
/**
@@ -38,8 +37,9 @@ export default {
title: message.title,
message: message.msg,
type: mtype as any,
});
})
})
.open((event: any) => console.log(event)).build();
}
}
.open((event: any) => console.log(event))
.build();
},
};

View File

@@ -1,8 +1,9 @@
import openApi from './openApi';
// 登录是否使用验证码配置key
const UseLoginCaptchaConfigKey = "UseLoginCaptcha"
const UseWartermarkConfigKey = "UseWartermark"
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWartermarkConfigKey = 'UseWartermark';
/**
* 获取系统配置值
@@ -11,7 +12,7 @@ const UseWartermarkConfigKey = "UseWartermark"
* @returns 配置值
*/
export async function getConfigValue(key: string): Promise<string> {
return await openApi.getConfigValue.request({key}) as string
return (await openApi.getConfigValue({ key })) as string;
}
/**
@@ -22,11 +23,24 @@ export async function getConfigValue(key: string) : Promise<string> {
* @returns 是否为ture1: true其他: false
*/
export async function getBoolConfigValue(key: string, defaultValue: boolean): Promise<boolean> {
const value = await getConfigValue(key)
if (!value) {
return defaultValue;
const value = await getConfigValue(key);
return convertBool(value, defaultValue);
}
return value == "1";
/**
* 获取账号登录安全配置
*
* @returns
*/
export async function getAccountLoginSecurity(): Promise<any> {
const value = await getConfigValue(AccountLoginSecurity);
if (!value) {
return null;
}
const jsonValue = JSON.parse(value);
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
return jsonValue;
}
/**
@@ -35,7 +49,7 @@ export async function getBoolConfigValue(key :string, defaultValue :boolean) : P
* @returns
*/
export async function useLoginCaptcha(): Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true)
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true);
}
/**
@@ -44,5 +58,23 @@ export async function useLoginCaptcha() : Promise<boolean> {
* @returns
*/
export async function useWartermark(): Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true)
return await getBoolConfigValue(UseWartermarkConfigKey, true);
}
function convertBool(value: string, defaultValue: boolean) {
if (!value) {
return defaultValue;
}
return value == '1' || value == 'true';
}
/**
* 获取LDAP登录配置
*
* @returns
*/
export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}

View File

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

View File

@@ -6,21 +6,25 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
let dataValueArr: any = [];
for (let column of columns) {
let val: any = data[column];
if (val == null || val == undefined) {
dataValueArr.push('');
continue;
}
if (typeof val == 'string' && val) {
// csv格式如果有逗号整体用双引号括起来如果里面还有双引号就替换成两个双引号这样导出来的格式就不会有问题了
if (val.indexOf(',') != -1) {
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
if (val.indexOf('"') != -1) {
val = val.replace(/\"/g, "\"\"");
val = val.replace(/\"/g, '""');
}
// 再将逗号转义
val = `"${val}"`;
}
dataValueArr.push(val);
dataValueArr.push(val + '\t');
} else {
dataValueArr.push(val);
dataValueArr.push(val + '\t');
}
}
cvsData.push(dataValueArr);
}
@@ -29,7 +33,7 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
let link = document.createElement('a');
let exportContent = '\uFEFF';
let blob = new Blob([exportContent + csvString], {
type: 'text/plain;charset=utrf-8',
type: 'text/plain;charset=utf-8',
});
link.id = 'download-csv';
link.setAttribute('href', URL.createObjectURL(blob));

View File

@@ -116,8 +116,7 @@ export function formatDate(date: Date, format: string) {
'3': '三',
'4': '四',
};
if (/(W+)/.test(format))
format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
for (let k in opt) {
let r = new RegExp('(' + k + ')').exec(format);

View File

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

View File

@@ -1,7 +1,5 @@
// 字体图标 url
const cssCdnUrlList: Array<string> = [
];
const cssCdnUrlList: Array<string> = [];
// 第三方 js url
const jsCdnUrlList: Array<string> = [];

View File

@@ -36,21 +36,19 @@ export function clearSession() {
window.sessionStorage.clear();
}
export function getUserInfo4Session() {
return getSession("userInfo")
return getSession('userInfo');
}
export function setUserInfo2Session(userinfo: any) {
setSession("userInfo", userinfo)
setSession('userInfo', userinfo);
}
// 获取是否开启水印
export function getUseWatermark4Session() {
return getSession("useWatermark")
return getSession('useWatermark');
}
export function setUseWatermark2Session(useWatermark: boolean) {
setSession("useWatermark", useWatermark)
setSession('useWatermark', useWatermark);
}

View File

@@ -12,7 +12,7 @@ export function templateResolve(template: string, param: any) {
if (value != null || value != undefined) {
return value;
}
return "";
return '';
});
}
@@ -21,11 +21,34 @@ export function letterAvatar(name: string, size = 60, color = '') {
name = name || '';
size = size || 60;
var colours = [
"#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50",
"#f1c40f", "#e67e22", "#e74c3c", "#00bcd4", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#7f8c8d"
'#1abc9c',
'#2ecc71',
'#3498db',
'#9b59b6',
'#34495e',
'#16a085',
'#27ae60',
'#2980b9',
'#8e44ad',
'#2c3e50',
'#f1c40f',
'#e67e22',
'#e74c3c',
'#00bcd4',
'#95a5a6',
'#f39c12',
'#d35400',
'#c0392b',
'#bdc3c7',
'#7f8c8d',
],
nameSplit = String(name).split(' '),
initials, charIndex, colourIndex, canvas, context, dataURI;
initials,
charIndex,
colourIndex,
canvas,
context,
dataURI;
if (nameSplit.length == 1) {
initials = nameSplit[0] ? nameSplit[0].charAt(0) : '?';
@@ -33,23 +56,76 @@ export function letterAvatar(name: string, size = 60, color = '') {
initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0);
}
if (window.devicePixelRatio) {
size = (size * window.devicePixelRatio);
size = size * window.devicePixelRatio;
}
initials = initials.toLocaleUpperCase()
initials = initials.toLocaleUpperCase();
charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64;
colourIndex = charIndex % 20;
canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
context = canvas.getContext("2d") as any;
context = canvas.getContext('2d') as any;
context.fillStyle = color ? color : colours[colourIndex - 1];
context.fillRect(0, 0, canvas.width, canvas.height);
context.font = Math.round(canvas.width / 2) + "px 'Microsoft Yahei'";
context.textAlign = "center";
context.fillStyle = "#FFF";
context.textAlign = 'center';
context.fillStyle = '#FFF';
context.fillText(initials, size / 2, size / 1.5);
dataURI = canvas.toDataURL();
canvas = null;
return dataURI;
};
}
/**
* 计算文本所占用的宽度px -> 该种方式较为准确
* 使用span标签包裹内容然后计算span的宽度 width px
* @param str
*/
export function getTextWidth(str: string) {
let width = 0;
let html = document.createElement('span');
html.innerText = str;
html.className = 'getTextWidth';
document?.querySelector('body')?.appendChild(html);
width = (document?.querySelector('.getTextWidth') as any).offsetWidth;
document?.querySelector('.getTextWidth')?.remove();
return width;
}
/**
* 获取内容所需要占用的宽度
*/
export function getContentWidth(content: any): number {
if (!content) {
return 50;
}
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 小写字母、数字字符
flexWidth += 9.3;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 20;
} else {
// 其他种类字符
flexWidth += 8;
}
}
// if (flexWidth > 450) {
// // 设置最大宽度
// flexWidth = 450;
// }
return flexWidth;
}

View File

@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import * as svg from '@element-plus/icons-vue';
import iconfontJson from '@/assets/iconfont/iconfont.json'
import iconfontJson from '@/assets/iconfont/iconfont.json';
import SvgIcon from '@/components/svgIcon/index.vue';
/**
@@ -52,7 +52,7 @@ const getLocalAliIconfont = () => {
resolve(iconfontJson.glyphs.map((x: any) => prefix + x.font_class));
});
});
}
};
// 初始化获取 css 样式,获取 element plus 自带图标
const elementPlusIconfont = () => {

View File

@@ -163,8 +163,7 @@ export function verifyPasswordStrength(val: string) {
// 中:字母+数字,字母+特殊字符,数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
// 强:字母+数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
v = '强';
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
// 返回结果
return v;
}
@@ -172,11 +171,7 @@ export function verifyPasswordStrength(val: string) {
// IP地址
export function verifyIPAddress(val: string) {
// false: IP地址不正确
if (
!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(
val
)
)
if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
return false;
// true: IP地址正确
else return true;

View File

@@ -1,5 +1,5 @@
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage.ts';
import { dateFormat2 } from '@/common/utils/date.ts'
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage';
import { dateFormat2 } from '@/common/utils/date';
// 页面添加水印效果
const setWatermark = (str: any) => {
@@ -44,17 +44,17 @@ function del() {
const watermark = {
use: () => {
setTimeout(() => {
const userinfo = getUserInfo4Session()
const userinfo = getUserInfo4Session();
if (userinfo && getUseWatermark4Session()) {
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`)
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`);
} else {
del();
}
}, 1500)
}, 1500);
},
// 设置水印
set: (str: any) => {
set(str)
set(str);
},
// 删除水印
del: () => {

View File

@@ -0,0 +1,25 @@
import { useUserInfo } from '@/store/userInfo';
/**
* 判断当前用户是否拥有指定权限
* @param code 权限code
* @returns
*/
export function hasPerm(code: string) {
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
}
/**
* 判断用户是否拥有权限对象里对应的code
* @param perms { save: "xxx:save"}
* @returns {"xxx:save": true} key->permission code
*/
export function hasPerms(permCodes: any[]) {
const res = {};
for (let permCode of permCodes) {
if (hasPerm(permCode)) {
res[permCode] = true;
}
}
return res;
}

View File

@@ -1,12 +1,24 @@
<template>
<transition name="el-zoom-in-center">
<div aria-hidden="true" class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu" role="tooltip"
data-popper-placement="bottom" :style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`" :key="Math.random()"
v-show="state.isShow">
<div
aria-hidden="true"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="state.isShow"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li class="el-dropdown-menu__item" aria-disabled="false" tabindex="-1" :key="k" v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)">
<li
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
@@ -43,9 +55,7 @@ const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
],
dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
item: {} as any,
arrowLeft: 10,
});
@@ -100,7 +110,7 @@ watch(
watch(
() => props.items,
(x: any) => {
state.dropdownList = x
state.dropdownList = x;
},
{
deep: true,

View File

@@ -0,0 +1,60 @@
<template>
<el-tag v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue';
import EnumValue from '@/common/Enum';
const props = defineProps({
enums: {
type: Object, // 需要为EnumValue类型
required: true,
},
value: {
type: [Object, String, Number],
required: true,
},
});
const defaultType = 'primary';
const state = reactive({
type: defaultType,
color: '',
enumLabel: '',
});
const { type, color, enumLabel } = toRefs(state);
// 监听该值是否改变,改变则需要将其枚举值与标签进行调整
watch(
() => props.value,
(newValue: any) => {
convert(newValue);
}
);
onMounted(() => {
convert(props.value);
});
const convert = (value: any) => {
const enumValue = EnumValue.getEnumByValue(Object.values(props.enums as any) as any, value) as any;
if (!enumValue) {
state.enumLabel = '-';
state.type = 'danger';
state.color = '';
return;
}
state.enumLabel = enumValue?.label || '';
if (enumValue.tag) {
state.color = enumValue.tag.color;
state.type = enumValue.tag.type;
} else {
state.type = defaultType;
}
};
</script>
<style scoped lang="scss"></style>

View File

@@ -12,10 +12,7 @@
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon
:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
class="font14"
/>
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
</template>
</el-input>
<el-popover

View File

@@ -1,5 +1,5 @@
<template>
<div class="monaco-editor" style="border: 1px solid #ccc;">
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5)">
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
@@ -9,10 +9,33 @@
<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 * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/dockerfile/dockerfile.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/css/css.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/python/python.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/java/java.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
// 右键菜单
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js';
import 'monaco-editor/esm/vs/editor/contrib/clipboard//browser/clipboard.js';
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
// 提示
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
import { editor, languages } from 'monaco-editor';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
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'
@@ -21,10 +44,15 @@ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
// 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 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';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
const { themeConfig } = storeToRefs(useThemeConfig());
const props = defineProps({
modelValue: {
type: String,
@@ -49,10 +77,10 @@ const props = defineProps({
type: Object,
default: null,
},
})
});
//定义事件
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue']);
const languageArr = [
{
@@ -125,15 +153,13 @@ const options = {
minimap: {
enabled: false, // 不要小地图
},
}
};
const state = reactive({
languageMode: 'shell',
})
});
const {
languageMode,
} = toRefs(state)
const { languageMode } = toRefs(state);
onMounted(() => {
state.languageMode = props.language;
@@ -149,19 +175,33 @@ onBeforeUnmount(() => {
if (completionItemProvider) {
completionItemProvider.dispose();
}
})
});
watch(() => props.modelValue, (newValue: any) => {
watch(
() => props.modelValue,
(newValue: any) => {
if (!monacoEditorIns.hasTextFocus()) {
state.languageMode = props.language;
monacoEditorIns?.setValue(newValue);
}
})
}
);
watch(() => props.language, (newValue: any) => {
watch(
() => props.language,
(newValue: any) => {
changeLanguage(newValue);
})
}
);
// 监听 themeConfig editorTheme配置文件的变化
watch(
() => themeConfig.value.editorTheme,
(val) => {
console.log('monaco editor theme change: ', val);
monaco?.editor?.setTheme(val);
}
);
const monacoTextarea: any = ref(null);
@@ -171,53 +211,49 @@ let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker()
return new JsonWorker();
}
return new EditorWorker();
}
},
};
const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器')
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';
options.theme = themeConfig.value.editorTheme;
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()
let oldModel = monacoEditorIns.getModel();
if (!oldModel) {
return;
}
// 创建一个新的文档模型
let newModel = monaco.editor.createModel(oldModel.getValue(), value)
let newModel = monaco.editor.createModel(oldModel.getValue(), value);
// 设置成新的
monacoEditorIns.setModel(newModel)
monacoEditorIns.setModel(newModel);
// 销毁旧的模型
if (oldModel) {
oldModel.dispose()
oldModel.dispose();
}
registerCompletionItemProvider();
}
};
const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value)
}
monacoEditorIns.getModel()?.setValue(value);
};
/**
* 注册联想补全提示
@@ -227,44 +263,43 @@ const registerCompletionItemProvider = () => {
completionItemProvider.dispose();
}
if (state.languageMode == 'shell') {
registeShell()
}
registeShell();
}
};
const registeShell = () => {
completionItemProvider = monaco.languages.registerCompletionItemProvider('shell', {
provideCompletionItems: async () => {
let suggestions: languages.CompletionItem[] = []
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
suggestions: suggestions,
};
}
})
},
});
};
const format = () => {
/*
触发自动格式化;
*/
monacoEditorIns.trigger('', 'editor.action.formatDocument', '')
}
defineExpose({ format })
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
};
defineExpose({ format });
</script>
<style lang="scss">

View File

@@ -0,0 +1,437 @@
<template>
<div class="page-table">
<!--
实现通过我们配置好的 查询条件
首先去创建form表单根据我们配置的查询条件去做一个循环判断展示出不用类型所对应不同的输入框
比如text对应普通的输入框select对应下拉选择dateTime对应日期时间选择器
在使用时父组件会传来一个queryForm空的对象
循环出来的输入框会绑定表格配置中的prop字段绑定在queryForm对象中
-->
<el-card>
<div class="query" ref="queryRef">
<div>
<div v-if="props.query.length > 0">
<el-form :model="props.queryForm" label-width="auto" :size="props.size">
<el-row
v-for="i in Math.ceil((props.query.length + 1) / (defaultQueryCount + 1))"
:key="i"
v-show="i == 1 || isOpenMoreQuery"
:class="i > 1 && isOpenMoreQuery ? 'is-open' : ''"
>
<el-form-item
:label="item.label"
style="margin-right: 12px; margin-bottom: 0px"
v-for="item in getRowQueryItem(i)"
:key="item.prop"
>
<!-- 这里只获取指定个数的筛选条件 -->
<el-input
v-model="queryForm[item.prop]"
:placeholder="'输入' + item.label + '关键字'"
clearable
v-if="item.type == 'text'"
></el-input>
<el-select-v2
v-model="queryForm[item.prop]"
:options="item.options"
clearable
:placeholder="'选择' + item.label + '关键字'"
v-else-if="item.type == 'select'"
/>
<el-date-picker
v-model="queryForm[item.prop]"
clearable
type="datetimerange"
format="YYYY-MM-DD hh:mm:ss"
value-format="x"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
v-else-if="item.type == 'date'"
/>
<template v-else-if="item.slot == 'queryBtns'">
<template v-if="props.query?.length > defaultQueryCount">
<el-button
@click="isOpenMoreQuery = !isOpenMoreQuery"
v-if="!isOpenMoreQuery"
icon="ArrowDownBold"
circle
></el-button>
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-else icon="ArrowUpBold" circle></el-button>
</template>
<el-button @click="queryData()" type="primary" icon="search" plain>查询</el-button>
<el-button @click="reset()" icon="RefreshRight">重置</el-button>
</template>
<slot :name="item.slot"></slot>
</el-form-item>
</el-row>
</el-form>
</div>
</div>
<div class="slot">
<!-- 查询栏右侧slot插槽用来添加表格其他操作比如新增数据删除数据等其他操作 -->
<slot name="queryRight"></slot>
<!--
动态表头显示根据表格每条配置项中的show字段来决定改列是否显示或者隐藏
columns 就是我们表格配置的数组对象
-->
<el-popover
placement="bottom"
title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
width="auto"
trigger="click"
>
<div v-for="(item, index) in props.columns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
</div>
<template #reference>
<!-- 一个Element Plus中的图标 -->
<el-button icon="Operation" :size="props.size"></el-button>
</template>
</el-popover>
</div>
</div>
<el-table
v-bind="$attrs"
:max-height="tableMaxHeight"
@selection-change="handleSelectionChange"
:data="props.data"
highlight-current-row
v-loading="loadingData"
:size="props.size"
>
<el-table-column v-if="props.showSelection" type="selection" width="40" />
<template v-for="(item, index) in columns">
<el-table-column
:key="index"
v-if="item.show"
:prop="item.prop"
:label="item.label"
:fixed="item.fixed"
:align="item.align"
:show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth"
:sortable="item.sortable || false"
:type="item.type"
:width="item.width"
>
<!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot">
<slot :name="item.prop" :data="scope.row"></slot>
</template>
<!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
effect="light"
trigger="click"
placement="top"
width="600px"
>
<template #default>
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
</template>
<template #reference>
<el-link
@click="formatText(scope.row[item.prop])"
:underline="false"
type="success"
icon="MagicStick"
class="mr5"
></el-link>
</template>
</el-popover>
<span>{{ item.getValueByData(scope.row) }}</span>
</template>
</el-table-column>
</template>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination
:small="props.size == 'small'"
@current-change="handlePageChange"
@size-change="handleSizeChange"
style="text-align: right"
layout="prev, pager, next, total, sizes, jumper"
:total="props.total"
v-model:current-page="state.pageNum"
v-model:page-size="state.pageSize"
:page-sizes="pageSizes"
/>
</el-row>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue';
import { TableColumn, TableQuery } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue';
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
const props = defineProps({
size: {
type: String,
default: '',
},
inputWidth: {
type: [Number, String],
default: 0,
},
// 是否显示选择列
showSelection: {
type: Boolean,
default: false,
},
// 当前选择的数据
selectionData: {
type: Array<any>
},
// 列信息
columns: {
type: Array<TableColumn>,
default: function () {
return [];
},
required: true,
},
// 表格数据
data: {
type: Array,
required: true,
},
total: {
type: [Number],
default: 0,
},
pageNum: {
type: Number,
default: 1,
},
pageSize: {
type: [Number],
default: 10,
},
// 查询条件配置
query: {
type: Array<TableQuery>,
default: function () {
return [];
}
},
// 绑定的查询表单
queryForm: {
type: Object,
default: function () {
return {};
}
},
})
const state = reactive({
pageSizes: [] as any, // 可选每页显示的数据量
pageSize: 10,
pageNum: 1,
isOpenMoreQuery: false,
defaultQueryCount: 2, // 默认显示的查询参数个数展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
queryForm: {} as any,
loadingData: false,
// 输入框宽度
inputWidth: "200px" as any,
formatVal: '', // 格式化后的值
tableMaxHeight: window.innerHeight - 240 + 'px',
})
const {
pageSizes,
isOpenMoreQuery,
defaultQueryCount,
queryForm,
loadingData,
inputWidth,
formatVal,
tableMaxHeight,
} = toRefs(state)
watch(() => props.queryForm, (newValue: any) => {
state.queryForm = newValue;
})
watch(() => props.pageNum, (newValue: any) => {
state.pageNum = newValue;
})
watch(() => props.pageSize, (newValue: any) => {
state.pageSize = newValue;
})
watch(() => props.data, (newValue: any) => {
if (newValue && newValue.length > 0) {
props.columns.forEach(item => {
if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(props.data);
}
})
}
})
onMounted(() => {
const pageSize = props.pageSize;
state.pageNum = props.pageNum;
state.pageSize = pageSize;
state.queryForm = props.queryForm;
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
// 如果没传输入框宽度则根据组件size设置默认宽度
if (!props.inputWidth) {
state.inputWidth = props.size == 'small' ? '150px' : '200px';
} else {
state.inputWidth = props.inputWidth;
}
window.addEventListener('resize', () => {
calcuTableHeight();
});
})
const calcuTableHeight = () => {
state.tableMaxHeight = window.innerHeight - 240 + 'px';
}
const formatText = (data: any)=> {
state.formatVal = '';
try {
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) {
state.formatVal = data;
}
}
const getRowQueryItem = (row: number) => {
// 第一行需要加个查询等按钮列
if (row === 1) {
const res = props.query.slice(row - 1, defaultQueryCount.value);
// 查询等按钮列
res.push(TableQuery.slot("", "", "queryBtns"));
return res
}
const columnCount = defaultQueryCount.value + 1;
return props.query.slice((row - 1) * columnCount - 1, row * columnCount - 1);
}
const handleSelectionChange = (val: any) => {
emit('update:selectionData', val);
}
const handlePageChange = () => {
emit('update:pageNum', state.pageNum);
execQuery();
}
const handleSizeChange = () => {
changePageNum(1);
emit('update:pageSize', state.pageSize);
execQuery();
}
const queryData = () => {
changePageNum(1);
execQuery();
}
const reset = () => {
// 将查询参数绑定的值置空,并重新粗发查询接口
for (let qi of props.query) {
state.queryForm[qi.prop] = null;
}
changePageNum(1);
emit('update:queryForm', state.queryForm);
execQuery();
}
const changePageNum = (pageNum: number) => {
state.pageNum = pageNum;
emit('update:pageNum', state.pageNum);
}
const execQuery = () => {
emit('pageChange');
}
/**
* 是否正在加载数据
*/
const loading = (loading: boolean) => {
state.loadingData = loading;
}
defineExpose({ loading })
</script>
<style scoped lang="scss">
.page-table {
.query {
margin-bottom: 10px;
overflow: hidden;
.is-open {
// padding: 10px 0;
max-height: 200px;
margin-top: 10px;
}
display: flex;
align-items: flex-start;
justify-content: space-between;
.slot {
display: flex;
justify-content: flex-end;
}
}
.page {
margin-top: 10px;
}
}
::v-deep(.el-form-item__label) {
font-weight: bold;
}
.el-select-v2 {
width: v-bind(inputWidth);
}
.el-input {
width: v-bind(inputWidth);
}
.el-select {
width: v-bind(inputWidth);
}
.el-date-editor {
width: 380px !important;
}
</style>

View File

@@ -0,0 +1,309 @@
import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date';
import { getTextWidth } from '@/common/utils/string';
export class TableColumn {
/**
* 属性字段
*/
prop: string;
/**
* 显示表头
*/
label: string;
/**
* 是否自动计算宽度
*/
autoWidth: boolean = true;
/**
* 自动计算宽度时,累加该值(可能列值会进行转换 如添加图标等,宽度需要比计算出来的更宽些)
*/
addWidth: number = 0;
/**
* 最小宽度
*/
minWidth: number | string;
/**
* 是否插槽是的话插槽名则为prop属性名
*/
slot: boolean = false;
showOverflowTooltip: boolean = true;
sortable: boolean = false;
/**
* 官方:对应列的类型。 如果设置了selection则显示多选框
* 如果设置了 index 则显示该行的索引(从 1 开始计算);
*
* 新增 tag类型用于枚举值转换后用tag进行展示
*
*/
type: string;
/**
* 类型展示需要的额外参数如枚举转换的EnumValue值等
*/
typeParam: any;
width: number | string;
fixed: any;
align: string = 'left';
/**
* 指定格式化函数对原始值进行格式化,如时间格式化等
* param1: data, param2: prop
*/
formatFunc: Function;
/**
* 是否显示该列
*/
show: boolean = true;
/**
* 是否展示美化按钮主要用于美化json文本等
*/
isBeautify: boolean = false;
constructor(prop: string, label: string) {
this.prop = prop;
this.label = label;
}
/**
* 获取该列在指定行数据中的值
* @param rowData 该行对应的数据
* @returns 该列对应的值
*/
getValueByData(rowData: any) {
if (this.formatFunc) {
return this.formatFunc(rowData, this.prop);
}
return rowData[this.prop];
}
static new(prop: string, label: string): TableColumn {
return new TableColumn(prop, label);
}
noShowOverflowTooltip(): TableColumn {
this.showOverflowTooltip = false;
return this;
}
setMinWidth(minWidth: number | string): TableColumn {
this.minWidth = minWidth;
this.autoWidth = false;
return this;
}
setAddWidth(addWidth: number): TableColumn {
this.addWidth = addWidth;
return this;
}
/**
* 居中对齐
* @returns this
*/
alignCenter(): TableColumn {
this.align = 'center';
return this;
}
/**
* 使用标签类型展示该列(用于枚举值友好展示)
* @param param 枚举对象
* @returns this
*/
typeTag(param: any): TableColumn {
this.type = 'tag';
this.typeParam = param;
return this;
}
typeText(): TableColumn {
this.type = 'text';
return this;
}
typeJson(): TableColumn {
this.type = 'jsonText';
return this;
}
/**
* 标识该列为插槽
* @returns this
*/
isSlot(): TableColumn {
this.slot = true;
return this;
}
/**
* 设置该列的格式化回调函数
* @param func 格式化回调函数(参数为 -> data: 该行对应的数据prop: 该列对应的prop属性值)
* @returns
*/
setFormatFunc(func: Function): TableColumn {
this.formatFunc = func;
return this;
}
/**
* 为时间字段,则使用默认时间格式函数
* @returns this
*/
isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return dateFormat(data[prop]);
});
return this;
}
/**
* 标识该列枚举类,需进行枚举值转换
* @returns this
*/
isEnum(enums: any): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return EnumValue.getLabelByValue(enums, data[prop]);
});
return this;
}
fixedRight(): TableColumn {
this.fixed = 'right';
return this;
}
fixedLeft(): TableColumn {
this.fixed = 'left';
return this;
}
canBeautify(): TableColumn {
this.isBeautify = true;
return this;
}
/**
* 自动计算最小宽度
* @param str 字符串
* @param tableData 表数据
* @param label 表头label也参与宽度计算
* @returns 列宽度
*/
autoCalculateMinWidth = (tableData: any) => {
const prop = this.prop;
const label = this.label;
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return 0;
}
let maxWidthText = '';
let maxWidthValue;
// 为了兼容formatFunc格式化回调函数
let maxData;
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i];
let nowValue = nowData[prop];
if (!nowValue) {
continue;
}
// 转为字符串比较长度
let nowText = nowValue + '';
if (nowText.length > maxWidthText.length) {
maxWidthText = nowText;
maxWidthValue = nowValue;
maxData = nowData;
}
}
if (this.formatFunc && maxWidthValue) {
maxWidthText = this.formatFunc(maxData, prop) + '';
}
// 需要加上表格的内间距等,视情况加
const contentWidth: number = getTextWidth(maxWidthText) + 30;
// 获取label的宽度取较大的宽度
const columnWidth: number = getTextWidth(label) + 60;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
// 设置上限与累加需要额外增加的宽度
this.minWidth = (flexWidth > 400 ? 400 : flexWidth) + this.addWidth;
};
}
export class TableQuery {
/**
* 属性字段
*/
prop: string;
/**
* 显示表头
*/
label: string;
/**
* 查询类型text、select、date
*/
type: string;
/**
* select可选值
*/
options: any;
/**
* 插槽名
*/
slot: string;
constructor(prop: string, label: string) {
this.prop = prop;
this.label = label;
}
static new(prop: string, label: string): TableQuery {
return new TableQuery(prop, label);
}
static text(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'text';
return tq;
}
static select(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'select';
return tq;
}
static date(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.type = 'date';
return tq;
}
static slot(prop: string, label: string, slotName: string): TableQuery {
const tq = new TableQuery(prop, label);
tq.slot = slotName;
return tq;
}
setOptions(options: any): TableQuery {
this.options = options;
return this;
}
}

View File

@@ -35,7 +35,7 @@ const props = defineProps({
isEle: {
type: Boolean,
default: true,
}
},
});
// 在线链接、本地引入地址前缀
@@ -48,7 +48,7 @@ const getIconName = computed(() => {
// 用于判断 element plus 自带 svg 图标的显示、隐藏。不存在 空格分隔的icon name即为element plus自带icon
const isShowIconSvg = computed(() => {
const ss = props?.name?.split(" ")
const ss = props?.name?.split(' ');
if (!ss) {
return true;
}
@@ -56,13 +56,13 @@ const isShowIconSvg = computed(() => {
});
const isIconfont = () => {
return props?.name?.startsWith("iconfont")
}
return props?.name?.startsWith('iconfont');
};
const getIconfontName = () => {
// iconfont icon-xxxx 获取icon-xxx即可
return props?.name?.split(" ")[1]
}
return props?.name?.split(' ')[1];
};
// 用于判断在线链接、本地引入等图标显示、隐藏
const isShowIconImg = computed(() => {

View File

@@ -0,0 +1,286 @@
<template>
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
<div ref="terminalRef" class="terminal" />
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
</div>
</template>
<script lang="ts" setup>
import 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
import TerminalSearch from './TerminalSearch.vue';
import { debounce } from 'lodash';
import { TerminalStatus } from './common';
const props = defineProps({
/**
* 初始化执行命令
*/
cmd: { type: String },
/**
* 连接url
*/
socketUrl: {
type: String,
},
/**
* 高度
*/
height: {
type: [String, Number],
default: '100%',
},
});
const emit = defineEmits(['statusChange']);
const terminalRef: any = ref(null);
const terminalSearchRef: any = ref(null);
const { themeConfig } = storeToRefs(useThemeConfig());
// 终端实例
let term: Terminal;
let socket: WebSocket;
let pingInterval: any;
const state = reactive({
// 插件
addon: {
fit: null as any,
search: null as any,
weblinks: null as any,
},
status: TerminalStatus.NoConnected,
});
onMounted(() => {
nextTick(() => {
init();
});
});
watch(
() => state.status,
() => {
emit('statusChange', state.status);
}
);
onBeforeUnmount(() => {
close();
});
function init() {
if (term) {
console.log('重新连接...');
close();
}
term = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15,
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
cursorBlink: true,
disableStdin: false,
allowProposedApi: true,
theme: {
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
background: themeConfig.value.terminalBackground || '#002833', //背景色
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
// cursorAccent: "red", // 光标停止颜色
} as any,
});
term.open(terminalRef.value);
// 注册自适应组件
const fitAddon = new FitAddon();
state.addon.fit = fitAddon;
term.loadAddon(fitAddon);
fitTerminal();
// 注册搜索组件
const searchAddon = new SearchAddon();
state.addon.search = searchAddon;
term.loadAddon(searchAddon);
// 注册 url link组件
const weblinks = new WebLinksAddon();
state.addon.weblinks = weblinks;
term.loadAddon(weblinks);
// 初始化websocket
initSocket();
}
/**
* 连接成功
*/
const onConnected = () => {
// 注册心跳
pingInterval = setInterval(sendPing, 15000);
// 注册 terminal 事件
term.onResize((event) => sendResize(event.cols, event.rows));
term.onData((event) => sendCmd(event));
// 注册自定义快捷键
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
// 注册搜索键 ctrl + f
if (event.key === 'f' && (event.ctrlKey || event.metaKey) && event.type === 'keydown') {
event.preventDefault();
terminalSearchRef.value.open();
}
return true;
});
state.status = TerminalStatus.Connected;
// resize
sendResize(term.cols, term.rows);
// 注册窗口大小监听器
window.addEventListener('resize', debounce(fitTerminal, 400));
focus();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendCmd(props.cmd + ' \r');
}
};
// 自适应终端
const fitTerminal = () => {
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
if (!dimensions) {
return;
}
if (dimensions?.cols && dimensions?.rows) {
term.resize(dimensions.cols, dimensions.rows);
}
};
const focus = () => {
setTimeout(() => term.focus(), 400);
};
const clear = () => {
term.clear();
term.clearSelection();
term.focus();
};
function initSocket() {
if (props.socketUrl) {
socket = new WebSocket(props.socketUrl);
}
// 监听socket连接
socket.onopen = () => {
onConnected();
};
// 监听socket错误信息
socket.onerror = (e: Event) => {
term.writeln('\r\n\x1b[31m提示: 连接错误...');
state.status = TerminalStatus.Error;
console.log('连接错误', e);
};
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);
// 关闭窗口大小监听器
window.removeEventListener('resize', debounce(fitTerminal, 100));
// 清除 ping
pingInterval && clearInterval(pingInterval);
state.status = TerminalStatus.Disconnected;
};
// 监听socket消息
socket.onmessage = getMessage;
}
function getMessage(msg: any) {
// msg.data是真正后端返回的数据
term.write(msg.data);
}
enum MsgType {
Resize = 1,
Data = 2,
Ping = 3,
}
const send = (msg: any) => {
state.status == TerminalStatus.Connected && socket.send(JSON.stringify(msg));
};
const sendResize = (cols: number, rows: number) => {
send({
type: MsgType.Resize,
Cols: cols,
Rows: rows,
});
};
const sendPing = () => {
send({
type: MsgType.Ping,
msg: 'ping',
});
};
function sendCmd(key: any) {
send({
type: MsgType.Data,
msg: key,
});
}
function closeSocket() {
// 关闭 websocket
socket && socket.readyState === 1 && socket.close();
// 清除 ping
pingInterval && clearInterval(pingInterval);
}
function close() {
console.log('in terminal body close');
closeSocket();
if (term) {
state.addon.search.dispose();
state.addon.fit.dispose();
state.addon.weblinks.dispose();
term.dispose();
}
}
const getStatus = (): TerminalStatus => {
return state.status;
};
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
</script>
<style lang="scss">
#terminal-body {
background: #212529;
width: 100%;
.terminal {
width: 100%;
height: 100%;
.xterm .xterm-viewport {
overflow-y: hidden;
}
}
}
</style>

View File

@@ -0,0 +1,309 @@
<template>
<div>
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
<el-dialog
title="终端"
v-model="openTerminal.visible"
top="32px"
class="terminal-dialog"
width="75%"
:close-on-click-modal="false"
:modal="true"
:show-close="false"
:fullscreen="openTerminal.fullscreen"
>
<template #header>
<div class="terminal-title-wrapper">
<!-- 左侧 -->
<div class="title-left-fixed">
<!-- title信息 -->
<div>
<slot name="headerTitle" :terminalInfo="openTerminal">
{{ openTerminal.headerTitle }}
</slot>
</div>
</div>
<!-- 右侧 -->
<div class="title-right-fixed">
<el-popconfirm @confirm="reConnect(openTerminal.terminalId)" title="确认重新连接?">
<template #reference>
<div class="mr15 pointer">
<el-tag v-if="openTerminal.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
<el-tag v-else type="danger" effect="light" round> 未连接 </el-tag>
</div>
</template>
</el-popconfirm>
<el-popover placement="bottom" :width="200" trigger="hover">
<template #reference>
<SvgIcon name="QuestionFilled" :size="20" class="pointer-icon mr10" />
</template>
<div>ctrl | command + f (搜索)</div>
<div class="mt5">点击连接状态可重连</div>
</el-popover>
<SvgIcon
name="ArrowDown"
v-if="props.visibleMinimize"
@click="minimize(openTerminal.terminalId)"
:size="20"
class="pointer-icon mr10"
title="最小化"
/>
<!-- <SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" /> -->
<SvgIcon name="Close" class="pointer-icon" @click="close(openTerminal.terminalId)" title="关闭" :size="20" />
</div>
</div>
</template>
<div class="terminal-wrapper" style="height: calc(100vh - 215px)">
<TerminalBody
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
:cmd="openTerminal.cmd"
:socket-url="openTerminal.socketUrl"
/>
</div>
</el-dialog>
</div>
<!-- 终端最小化 -->
<div class="terminal-minimize-container">
<el-card
v-for="minimizeTerminal of minimizeTerminals"
:key="minimizeTerminal.terminalId"
:class="`terminal-minimize-item pointer ${minimizeTerminal.styleClass}`"
size="small"
@click="maximize(minimizeTerminal.terminalId)"
>
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
<span>
{{ minimizeTerminal.title }}
</span>
</el-tooltip>
<!-- 关闭按钮 -->
<SvgIcon name="CloseBold" @click.stop="closeMinimizeTerminal(minimizeTerminal.terminalId)" class="ml10 pointer-icon fr" :size="20" />
</el-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive } from 'vue';
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { TerminalStatus } from './common';
const props = defineProps({
visibleMinimize: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['close', 'minimize']);
const openTerminalRefs: any = {};
/**
terminal对象信息:
visible: false,
machineId: null as any,
terminalId: null as any,
machine: {} as any,
fullscreen: false,
*/
const state = reactive({
terminals: {} as any, // key -> terminalId value -> terminal
minimizeTerminals: {} as any, // key -> terminalId value -> 简易terminal
});
const { terminals, minimizeTerminals } = toRefs(state);
const setTerminalRef = (el: any, terminalId: any) => {
if (terminalId) {
openTerminalRefs[terminalId] = el;
}
};
function open(terminalInfo: any, cmd: string = '') {
let terminalId = terminalInfo.terminalId;
if (!terminalId) {
terminalId = Date.now();
}
state.terminals[terminalId] = {
...terminalInfo,
terminalId,
visible: true,
cmd,
status: TerminalStatus.NoConnected,
};
}
const terminalStatusChange = (terminalId: string, status: TerminalStatus) => {
const terminal = state.terminals[terminalId];
if (terminal) {
terminal.status = status;
}
const minTerminal = state.minimizeTerminals[terminalId];
if (!minTerminal) {
return;
}
minTerminal.styleClass = getTerminalStatysStyleClass(terminalId, status);
};
const getTerminalStatysStyleClass = (terminalId: any, status: any = null) => {
if (status == null) {
status = openTerminalRefs[terminalId].getStatus();
}
if (status == TerminalStatus.Connected) {
return 'terminal-status-success';
}
if (status == TerminalStatus.NoConnected) {
return 'terminal-status-no-connect';
}
return 'terminal-status-error';
};
const reConnect = (terminalId: any) => {
openTerminalRefs[terminalId].init();
};
function close(terminalId: any) {
console.log('in terminal dialog close');
delete state.terminals[terminalId];
// 关闭终端并删除终端ref
const terminalRef = openTerminalRefs[terminalId];
terminalRef && terminalRef.close();
delete openTerminalRefs[terminalId];
emit('close', terminalId);
}
function minimize(terminalId: number) {
console.log('in terminal dialog minimize: ', terminalId);
const terminal = state.terminals[terminalId];
if (!terminal) {
console.warn('不存在该终端信息: ', terminalId);
return;
}
terminal.visible = false;
const minTerminalInfo = {
terminalId: terminal.terminalId,
title: terminal.minTitle, // 截取terminalId最后两位区分多个terminal
desc: terminal.minDesc,
styleClass: getTerminalStatysStyleClass(terminalId),
};
state.minimizeTerminals[terminalId] = minTerminalInfo;
emit('minimize', minTerminalInfo);
}
function maximize(terminalId: any) {
console.log('in terminal dialog maximize: ', terminalId);
const minTerminal = state.minimizeTerminals[terminalId];
if (!minTerminal) {
console.log('no min terminal...');
return;
}
delete state.minimizeTerminals[terminalId];
// 显示终端信息
state.terminals[terminalId].visible = true;
const terminalRef = openTerminalRefs[terminalId];
// fit
setTimeout(() => {
terminalRef.fitTerminal();
terminalRef.focus();
}, 250);
}
const closeMinimizeTerminal = (terminalId: any) => {
delete state.minimizeTerminals[terminalId];
close(terminalId);
};
defineExpose({
open,
close,
minimize,
maximize,
});
</script>
<style lang="scss">
.terminal-dialog-container {
.el-dialog__header {
padding: 10px;
}
// .terminal-dialog {
// height: calc(100vh - 200px) !important;
// }
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
padding: 0px !important;
}
.terminal-title-wrapper {
display: flex;
justify-content: space-between;
font-size: 16px;
.title-right-fixed {
display: flex;
align-items: center;
font-size: 20px;
text-align: end;
}
}
}
.terminal-minimize-container {
position: absolute;
right: 16px;
bottom: 16px;
z-index: 10;
display: flex;
flex-wrap: wrap-reverse;
justify-content: flex-end;
.terminal-minimize-item {
min-width: 120px;
// box-shadow: 0 3px 4px #dee2e6;
border-radius: 4px;
margin: 1px 1px;
}
.terminal-status-error {
box-shadow: 0 3px 4px var(--el-color-danger);
border-color: var(--el-color-danger);
}
.terminal-status-no-connect {
box-shadow: 0 3px 4px var(--el-color-warning);
border-color: var(--el-color-warning);
}
.terminal-status-success {
box-shadow: 0 3px 4px var(--el-color-success);
border-color: var(--el-color-success);
}
.el-card__body {
padding: 15px !important;
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div id="search-card" v-show="search.visible" @keydown.esc="closeSearch">
<el-card title="搜索" size="small">
<!-- 搜索框 -->
<el-input
class="search-input"
ref="searchInputRef"
placeholder="请输入查找内容,回车搜索"
v-model="search.value"
@keyup.enter.native="searchKeywords(true)"
clearable
>
</el-input>
<!-- 选项 -->
<div class="search-options">
<el-row>
<el-col :span="12">
<el-checkbox class="usn" v-model="search.regex"> 正则匹配 </el-checkbox>
</el-col>
<el-col :span="12">
<el-checkbox class="usn" v-model="search.words"> 单词全匹配 </el-checkbox>
</el-col>
<el-col :span="12">
<el-checkbox class="usn" v-model="search.matchCase"> 区分大小写 </el-checkbox>
</el-col>
<el-col :span="12">
<el-checkbox class="usn" v-model="search.incremental"> 增量查找 </el-checkbox>
</el-col>
</el-row>
</div>
<!-- 按钮 -->
<div class="search-buttons">
<el-button class="terminal-search-button search-button-prev" type="primary" size="small" @click="searchKeywords(false)"> 上一个 </el-button>
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="searchKeywords(true)"> 下一个 </el-button>
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="closeSearch"> 关闭 </el-button>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, nextTick, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
const props = defineProps({
searchAddon: {
type: [SearchAddon],
require: true,
},
});
const state = reactive({
search: {
visible: false,
value: '',
regex: false,
words: false,
matchCase: false,
incremental: false,
},
});
const { search } = toRefs(state);
const emit = defineEmits(['close']);
const searchInputRef: any = ref(null);
function open() {
const visible = state.search.visible;
state.search.visible = !visible;
console.log(state.search.visible);
if (!visible) {
nextTick(() => {
searchInputRef.value.focus();
});
}
}
function closeSearch() {
state.search.visible = false;
state.search.value = '';
props.searchAddon?.clearDecorations();
emit('close');
}
function searchKeywords(direction: any) {
if (!state.search.value) {
return;
}
const option = {
regex: state.search.regex,
wholeWord: state.search.words,
caseSensitive: state.search.matchCase,
incremental: state.search.incremental,
};
let res;
if (direction) {
res = props.searchAddon?.findNext(state.search.value, getSearchOptions(option));
} else {
res = props.searchAddon?.findPrevious(state.search.value, getSearchOptions(option));
}
if (!res) {
ElMessage.info('未查询到匹配项');
}
}
const getSearchOptions = (searchOptions?: ISearchOptions): ISearchOptions => {
return {
...searchOptions,
decorations: {
matchOverviewRuler: '#888888',
activeMatchColorOverviewRuler: '#ffff00',
matchBackground: '#888888',
activeMatchBackground: '#ffff00',
},
};
};
defineExpose({ open });
</script>
<style lang="scss" scoped>
#search-card {
position: absolute;
top: 60px;
right: 20px;
z-index: 1200;
width: 270px;
.search-input {
width: 240px;
}
.search-options {
margin: 12px 0;
}
.search-buttons {
margin-top: 5px;
display: flex;
justify-content: flex-end;
}
.terminal-search-button {
margin-left: 10px;
}
}
</style>

View File

@@ -0,0 +1,6 @@
export enum TerminalStatus {
Error = -1,
NoConnected = 0,
Connected = 1,
Disconnected = 2,
}

View File

@@ -1,21 +1,22 @@
import type { App } from 'vue';
import { useUserInfo } from '@/store/userInfo';
import { judementSameArr } from '@/common/utils/arrayOperation';
import { hasPerm } from '@/components/auth/auth';
// 用户权限指令
export function authDirective(app: App) {
// 单个权限验证v-auth="xxx"
app.directive('auth', {
mounted(el, binding) {
if (!useUserInfo().userInfo.permissions.some((v: any) => v === binding.value)) {
if (!hasPerm(binding.value)) {
parseNoAuth(el, binding);
};
}
},
});
// 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
app.directive('auths', {
mounted(el, binding) {
const value = binding.value
const value = binding.value;
let flag = false;
useUserInfo().userInfo.permissions.map((val: any) => {
value.map((v: any) => {
@@ -32,7 +33,7 @@ export function authDirective(app: App) {
mounted(el, binding) {
if (!judementSameArr(binding.value, useUserInfo().userInfo.permissions)) {
parseNoAuth(el, binding);
};
}
},
});
}
@@ -54,8 +55,8 @@ const parseNoAuth = (el: any, binding: any) => {
// 移除该元素
el.parentNode.removeChild(el);
}
}
};
const disableClickFn = (event: any) => {
event && event.stopImmediatePropagation();
}
};

View File

@@ -9,22 +9,20 @@ import { registElSvgIcon } from '@/common/utils/svgIcons';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/theme-chalk/dark/css-vars.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { ElMessage } from 'element-plus';
import '@/theme/index.scss';
import '@/assets/font/font.css'
import '@/assets/iconfont/iconfont.js'
import '@/assets/font/font.css';
import '@/assets/iconfont/iconfont.js';
const app = createApp(App);
registElSvgIcon(app);
directive(app);
app.use(pinia)
.use(router)
.use(ElementPlus, { size: globalComponentSize, locale: zhCn })
.mount('#app');
app.use(pinia).use(router).use(ElementPlus, { size: globalComponentSize, locale: zhCn }).mount('#app');
// 屏蔽警告信息
app.config.warnHandler = () => null;
@@ -32,8 +30,8 @@ app.config.warnHandler = () => null;
app.config.errorHandler = function (err: any, vm, info) {
// 如果是断言错误,则进行提示即可
if (err.name == 'AssertError') {
ElMessage.error(err.message)
ElMessage.error(err.message);
} else {
console.error(err, info)
}
console.error(err, info);
}
};

View File

@@ -2,9 +2,9 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { getSession, clearSession } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string'
import { templateResolve } from '@/common/utils/string';
import { NextLoading } from '@/common/utils/loading';
import { dynamicRoutes, staticRoutes, pathMatch } from './route'
import { dynamicRoutes, staticRoutes, pathMatch } from './route';
import openApi from '@/common/openApi';
import sockets from '@/common/sockets';
import pinia from '@/store/index';
@@ -18,7 +18,7 @@ import { useKeepALiveNames } from '@/store/keepAliveNames';
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}', '!../views/layout/**/*.{vue,tsx}']);
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 添加静态路由
@@ -33,17 +33,17 @@ export function initAllFun() {
const token = getSession('token'); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false
return false;
}
useUserInfo().setUserInfo({});
router.addRoute(pathMatch); // 添加404界面
resetRoute(); // 删除/重置路由
// 添加动态路由
setFilterRouteEnd().forEach((route: any) => {
router.addRoute((route as unknown) as RouteRecordRaw);
router.addRoute(route as unknown as RouteRecordRaw);
});
// 过滤权限菜单
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus))
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus));
}
// 后端控制路由:执行路由数据初始化
@@ -52,7 +52,7 @@ export async function initBackEndControlRoutesFun() {
const token = getSession('token'); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false
return false;
}
useUserInfo().setUserInfo({});
// 获取路由
@@ -63,59 +63,59 @@ export async function initBackEndControlRoutesFun() {
resetRoute(); // 删除/重置路由
// 添加动态路由
formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => {
router.addRoute((route as unknown) as RouteRecordRaw);
router.addRoute(route as unknown as RouteRecordRaw);
});
useRoutesList().setRoutesList(dynamicRoutes[0].children)
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
export async function getBackEndControlRoutes() {
try {
const menuAndPermission = await openApi.getPermissions.request();
const menuAndPermission = await openApi.getPermissions();
// 赋值权限码,用于控制按钮等
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
return menuAndPermission.menus;
} catch (e: any) {
console.error(e);
return []
return [];
}
}
// 后端控制路由,后端返回路由 转换为vue route
export function backEndRouterConverter(routes: any, parentPath: string = "/") {
export function backEndRouterConverter(routes: any, parentPath: string = '/') {
if (!routes) return;
return routes.map((item: any) => {
if (!item.meta) {
return item
return item;
}
// 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta)
item.meta = JSON.parse(item.meta);
// 将meta.comoponet 解析为route.component
if (item.meta.component) {
item.component = dynamicImport(dynamicViewsModules, item.meta.component)
delete item.meta['component']
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
delete item.meta['component'];
}
// route.path == resource.code
let path = item.code
let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith("/")) {
path = parentPath + "/" + path;
if (!path.startsWith('/')) {
path = parentPath + '/' + path;
}
item.path = path
delete item['code']
item.path = path;
delete item['code'];
// route.meta.title == resource.name
item.meta.title = item.name
delete item['name']
item.meta.title = item.name;
delete item['name'];
// route.name == resource.meta.routeName
item.name = item.meta.routeName
delete item.meta['routeName']
item.name = item.meta.routeName;
delete item.meta['routeName'];
// route.redirect == resource.meta.redirect
if (item.meta.redirect) {
item.redirect = item.meta.redirect
delete item.meta['redirect']
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
}
item.children && backEndRouterConverter(item.children, item.path);
return item;
@@ -178,9 +178,9 @@ export function formatTwoStageRoutes(arr: any) {
// 判断路由code 是否包含当前登录用户menus字段中menus为字符串code数组
export function hasAnth(menus: any, route: any) {
if (route.meta && route.meta.code) {
return menus.includes(route.meta.code)
return menus.includes(route.meta.code);
}
return true
return true;
}
// 递归过滤有权限的路由
@@ -190,7 +190,7 @@ export function setFilterMenuFun(routes: any, menus: any) {
const item = { ...route };
if (hasAnth(menus, item)) {
if (item.children) {
item.children = setFilterMenuFun(item.children, menus)
item.children = setFilterMenuFun(item.children, menus);
}
menu.push(item);
}
@@ -206,11 +206,11 @@ export function setFilterRoute(chil: any) {
if (route.meta.code) {
useUserInfo().userInfo.menus.forEach((m: any) => {
if (route.meta.code == m) {
filterRoute.push({ ...route })
filterRoute.push({ ...route });
}
})
});
} else {
filterRoute.push({ ...route })
filterRoute.push({ ...route });
}
});
return filterRoute;
@@ -253,11 +253,11 @@ router.beforeEach(async (to, from, next) => {
// 如果有标题参数,则再原标题后加上参数来区别
if (to.meta.titleRename) {
to.meta.title = templateResolve(to.meta.title as string, to.query)
to.meta.title = templateResolve(to.meta.title as string, to.query);
}
const token = getSession('token');
if (to.path === '/login' && !token) {
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
next();
NProgress.done();
return;

View File

@@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/views/layout/index.vue'
import Layout from '@/views/layout/index.vue';
// 定义动态路由
export const dynamicRoutes = [
@@ -11,7 +11,7 @@ export const dynamicRoutes = [
meta: {
isKeepAlive: true,
},
children: []
children: [],
// children: [
// {
// path: '/home',
@@ -143,6 +143,14 @@ export const staticRoutes: Array<RouteRecordRaw> = [
title: '没有权限',
},
},
{
path: '/oauth2/callback',
name: 'oauth2Callback',
component: () => import('@/views/oauth/Oauth2Callback.vue'),
meta: {
title: 'oauth2回调',
},
},
{
path: '/machine/terminal',
name: 'machineTerminal',

View File

@@ -24,13 +24,13 @@ export const useThemeConfig = defineStore('themeConfig', {
// 默认顶栏导航背景颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
topBar: '#ffffff',
// 默认菜单导航背景颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
menuBar: '#545c64',
menuBar: '#FFFFFF',
// 默认分栏菜单背景颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
columnsMenuBar: '#545c64',
// 默认顶栏导航字体颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
topBarColor: '#606266',
// 默认菜单导航字体颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
menuBarColor: '#eaeaea',
menuBarColor: '#606266',
// 默认分栏菜单字体颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
columnsMenuBarColor: '#e6e6e6',
// 是否开启顶栏背景颜色渐变
@@ -81,6 +81,8 @@ export const useThemeConfig = defineStore('themeConfig', {
isSortableTagsView: true,
// 是否开启 Footer 底部版权信息
isFooter: false,
// 是否暗模式
isDark: false,
// 是否开启灰色模式
isGrayscale: false,
// 是否开启色弱模式
@@ -116,7 +118,6 @@ export const useThemeConfig = defineStore('themeConfig', {
// 编辑器主题
editorTheme: 'vs',
/* 后端控制路由
------------------------------- */
// 是否开启后端控制路由
@@ -140,4 +141,4 @@ export const useThemeConfig = defineStore('themeConfig', {
this.themeConfig = data.themeConfig;
},
},
})
});

View File

@@ -8,12 +8,12 @@ export const useUserInfo = defineStore('userInfo', {
actions: {
// 设置用户信息
async setUserInfo(data: object) {
const ui = getSession('userInfo')
const ui = getSession('userInfo');
if (ui) {
this.userInfo = ui;
} else {
this.userInfo = data;
}
},
}
})
},
});

View File

@@ -7,6 +7,24 @@
outline: none !important;
}
:root {
--color-white: #ffffff;
--bg-main-color: #f8f8f8;
--bg-color: #f5f5ff;
--bg-menuBarActiveColor: #0000000a; // 菜单栏激活时的背景色
--border-color-light: #f1f2f3;
--el-color-primary-lighter: #ecf5ff;
--color-success-lighter: #f0f9eb;
--color-warning-lighter: #fdf6ec;
--color-danger-lighter: #fef0f0;
--color-dark-hover: #0000001a;
--color-menu-hover: rgba(0, 0, 0, 0.2);
--color-user-hover: rgba(0, 0, 0, 0.04);
--color-seting-main: #e9eef3;
--color-seting-aside: #d3dce6;
--color-seting-header: #b3c0d1;
}
html,
body,
#app {
@@ -18,7 +36,7 @@ body,
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
background-color: var(--bg-main-color);
font-size: 14px;
overflow: hidden;
position: relative;
@@ -53,7 +71,7 @@ body,
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
background-color: var(--bg-main-color);
}
.el-scrollbar {
@@ -65,11 +83,11 @@ body,
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
border: 1px solid var(--el-border-color-light, #ebeef5);
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
border-right: 1px solid var(--el-border-color-light, #ebeef5);
}
.layout-aside-width-default {
@@ -116,7 +134,7 @@ body,
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
.el-divider {
@@ -128,7 +146,7 @@ body,
------------------------------- */
#nprogress {
.bar {
background: var(--color-primary) !important;
background: var(--el-color-primary) !important;
z-index: 9999999 !important;
}
}
@@ -195,23 +213,23 @@ body,
/* 颜色值
------------------------------- */
.color-primary {
color: var(--color-primary);
color: var(--el-color-primary);
}
.color-success {
color: var(--color-success);
color: var(--el-color-success);
}
.color-warning {
color: var(--color-warning);
color: var(--el-color-warning);
}
.color-danger {
color: var(--color-danger);
color: var(--el-color-danger);
}
.color-info {
color: var(--color-info);
color: var(--el-color-info);
}
/* 字体大小全局样式
@@ -262,17 +280,17 @@ body,
::-webkit-scrollbar {
width: 4px;
height: 8px;
background-color: #F5F5F5;
background-color: var(--el-border-color-light, #ebeef5);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #F5F5F5;
background-color: var(--el-border-color-light, #ebeef5);
}
::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #F5F5F5;
background-color: var(--el-border-color-light, #ebeef5);
}
.el-menu .fa {
@@ -317,11 +335,10 @@ body,
.toolbar {
width: 100%;
padding: 6px;
background-color: #ffffff;
padding: 4px;
overflow: hidden;
line-height: 32px;
border: 1px solid #e6ebf5;
line-height: 24px;
border: 1px solid var(--el-border-color-light, #ebeef5);
}
.fl {
@@ -345,3 +362,15 @@ body,
.f12 {
font-size: 12px
}
.pointer {
cursor: pointer;
}
.pointer-icon {
cursor: pointer;
transition: color 0.3s;
}
.pointer-icon:hover {
color: var(--el-color-primary); /* 鼠标移动到图标时的颜色 */
}

View File

@@ -1,2 +1 @@
@import 'common/transition.scss';
@import 'common/var.scss';

View File

@@ -1,129 +0,0 @@
/**
* scss 怎么动态创建变量
* 本来想用 @function@for 好像不可以动态创建
* 2020.12.19 lyt 记录
**/
/* 定义初始颜色
------------------------------- */
$--color-primary: #409eff !default;
$--color-whites: #ffffff !default;
$--color-blacks: #000000 !default;
$--color-primary-light-1: mix($--color-whites, $--color-primary, 10%) !default;
$--color-primary-light-2: mix($--color-whites, $--color-primary, 20%) !default;
$--color-primary-light-3: mix($--color-whites, $--color-primary, 30%) !default;
$--color-primary-light-4: mix($--color-whites, $--color-primary, 40%) !default;
$--color-primary-light-5: mix($--color-whites, $--color-primary, 50%) !default;
$--color-primary-light-6: mix($--color-whites, $--color-primary, 60%) !default;
$--color-primary-light-7: mix($--color-whites, $--color-primary, 70%) !default;
$--color-primary-light-8: mix($--color-whites, $--color-primary, 80%) !default;
$--color-primary-light-9: mix($--color-whites, $--color-primary, 90%) !default;
$--color-success: #67c23a !default;
$--color-success-light-1: mix($--color-whites, $--color-success, 10%) !default;
$--color-success-light-2: mix($--color-whites, $--color-success, 20%) !default;
$--color-success-light-3: mix($--color-whites, $--color-success, 30%) !default;
$--color-success-light-4: mix($--color-whites, $--color-success, 40%) !default;
$--color-success-light-5: mix($--color-whites, $--color-success, 50%) !default;
$--color-success-light-6: mix($--color-whites, $--color-success, 60%) !default;
$--color-success-light-7: mix($--color-whites, $--color-success, 70%) !default;
$--color-success-light-8: mix($--color-whites, $--color-success, 80%) !default;
$--color-success-light-9: mix($--color-whites, $--color-success, 90%) !default;
$--color-info: #909399 !default;
$--color-info-light-1: mix($--color-whites, $--color-info, 10%) !default;
$--color-info-light-2: mix($--color-whites, $--color-info, 20%) !default;
$--color-info-light-3: mix($--color-whites, $--color-info, 30%) !default;
$--color-info-light-4: mix($--color-whites, $--color-info, 40%) !default;
$--color-info-light-5: mix($--color-whites, $--color-info, 50%) !default;
$--color-info-light-6: mix($--color-whites, $--color-info, 60%) !default;
$--color-info-light-7: mix($--color-whites, $--color-info, 70%) !default;
$--color-info-light-8: mix($--color-whites, $--color-info, 80%) !default;
$--color-info-light-9: mix($--color-whites, $--color-info, 90%) !default;
$--color-warning: #e6a23c !default;
$--color-warning-light-1: mix($--color-whites, $--color-warning, 10%) !default;
$--color-warning-light-2: mix($--color-whites, $--color-warning, 20%) !default;
$--color-warning-light-3: mix($--color-whites, $--color-warning, 30%) !default;
$--color-warning-light-4: mix($--color-whites, $--color-warning, 40%) !default;
$--color-warning-light-5: mix($--color-whites, $--color-warning, 50%) !default;
$--color-warning-light-6: mix($--color-whites, $--color-warning, 60%) !default;
$--color-warning-light-7: mix($--color-whites, $--color-warning, 70%) !default;
$--color-warning-light-8: mix($--color-whites, $--color-warning, 80%) !default;
$--color-warning-light-9: mix($--color-whites, $--color-warning, 90%) !default;
$--color-danger: #f56c6c !default;
$--color-danger-light-1: mix($--color-whites, $--color-danger, 10%) !default;
$--color-danger-light-2: mix($--color-whites, $--color-danger, 20%) !default;
$--color-danger-light-3: mix($--color-whites, $--color-danger, 30%) !default;
$--color-danger-light-4: mix($--color-whites, $--color-danger, 40%) !default;
$--color-danger-light-5: mix($--color-whites, $--color-danger, 50%) !default;
$--color-danger-light-6: mix($--color-whites, $--color-danger, 60%) !default;
$--color-danger-light-7: mix($--color-whites, $--color-danger, 70%) !default;
$--color-danger-light-8: mix($--color-whites, $--color-danger, 80%) !default;
$--color-danger-light-9: mix($--color-whites, $--color-danger, 90%) !default;
$--bg-topBar: #ffffff;
$--bg-menuBar: #545c64;
$--bg-columnsMenuBar: #545c64;
$--bg-topBarColor: #606266;
$--bg-menuBarColor: #eaeaea;
$--bg-columnsMenuBarColor: #e6e6e6;
/* 赋值给:root
------------------------------- */
:root {
--color-primary: #{$--color-primary};
--color-whites: #{$--color-whites};
--color-blacks: #{$--color-blacks};
--color-primary-light-1: #{$--color-primary-light-1};
--color-primary-light-2: #{$--color-primary-light-2};
--color-primary-light-3: #{$--color-primary-light-3};
--color-primary-light-4: #{$--color-primary-light-4};
--color-primary-light-5: #{$--color-primary-light-5};
--color-primary-light-6: #{$--color-primary-light-6};
--color-primary-light-7: #{$--color-primary-light-7};
--color-primary-light-8: #{$--color-primary-light-8};
--color-primary-light-9: #{$--color-primary-light-9};
--color-success: #{$--color-success};
--color-success-light-1: #{$--color-success-light-1};
--color-success-light-2: #{$--color-success-light-2};
--color-success-light-3: #{$--color-success-light-3};
--color-success-light-4: #{$--color-success-light-4};
--color-success-light-5: #{$--color-success-light-5};
--color-success-light-6: #{$--color-success-light-6};
--color-success-light-7: #{$--color-success-light-7};
--color-success-light-8: #{$--color-success-light-8};
--color-success-light-9: #{$--color-success-light-9};
--color-info: #{$--color-info};
--color-info-light-1: #{$--color-info-light-1};
--color-info-light-2: #{$--color-info-light-2};
--color-info-light-3: #{$--color-info-light-3};
--color-info-light-4: #{$--color-info-light-4};
--color-info-light-5: #{$--color-info-light-5};
--color-info-light-6: #{$--color-info-light-6};
--color-info-light-7: #{$--color-info-light-7};
--color-info-light-8: #{$--color-info-light-8};
--color-info-light-9: #{$--color-info-light-9};
--color-warning: #{$--color-warning};
--color-warning-light-1: #{$--color-warning-light-1};
--color-warning-light-2: #{$--color-warning-light-2};
--color-warning-light-3: #{$--color-warning-light-3};
--color-warning-light-4: #{$--color-warning-light-4};
--color-warning-light-5: #{$--color-warning-light-5};
--color-warning-light-6: #{$--color-warning-light-6};
--color-warning-light-7: #{$--color-warning-light-7};
--color-warning-light-8: #{$--color-warning-light-8};
--color-warning-light-9: #{$--color-warning-light-9};
--color-danger: #{$--color-danger};
--color-danger-light-1: #{$--color-danger-light-1};
--color-danger-light-2: #{$--color-danger-light-2};
--color-danger-light-3: #{$--color-danger-light-3};
--color-danger-light-4: #{$--color-danger-light-4};
--color-danger-light-5: #{$--color-danger-light-5};
--color-danger-light-6: #{$--color-danger-light-6};
--color-danger-light-7: #{$--color-danger-light-7};
--color-danger-light-8: #{$--color-danger-light-8};
--color-danger-light-9: #{$--color-danger-light-9};
--bg-topBar: #{$--bg-topBar};
--bg-menuBar: #{$--bg-menuBar};
--bg-columnsMenuBar: #{$--bg-columnsMenuBar};
--bg-topBarColor: #{$--bg-topBarColor};
--bg-menuBarColor: #{$--bg-menuBarColor};
--bg-columnsMenuBarColor: #{$--bg-columnsMenuBarColor};
}

View File

@@ -0,0 +1,27 @@
html.dark {
// 变量(自定义时,只需修改这里的值)
--next-bg-main: #1f1f1f;
--next-color-white: #ffffff;
--next-color-disabled: #191919;
--next-color-bar: #dadada;
--next-color-primary: #303030;
--next-border-color: #424242;
--next-border-black: #333333;
--next-border-columns: #2a2a2a;
--next-color-seting: #505050;
--next-text-color-regular: #9b9da1;
--next-text-color-placeholder: #7a7a7a;
--next-color-hover: #3c3c3c;
--next-color-hover-rgba: rgba(0, 0, 0, 0.3);
/* 自定义深色背景颜色 */
// root
--bg-main-color: var(--next-bg-main) !important;
--bg-topBar: var(--next-color-disabled) !important;
--bg-topBarColor: var(--next-color-bar) !important;
--bg-menuBar: var(--next-color-disabled) !important;
--bg-menuBarColor: var(--next-color-bar) !important;
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
--bg-columnsMenuBar: var(--next-color-disabled) !important;
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,4 +4,5 @@
@import './element.scss';
@import './media/media.scss';
@import './waves.scss';
@import './dark.scss';
@import './iconSelector.scss';

View File

@@ -15,7 +15,7 @@
.loading-next .loading-next-box-warp .loading-next-box-item {
width: 33.333333%;
height: 33.333333%;
background: var(--color-primary);
background: var(--el-color-primary);
float: left;
animation: loading-next-animation 1.2s infinite ease;
border-radius: 1px;

View File

@@ -9,7 +9,7 @@
height: 3px !important;
}
::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
@@ -40,7 +40,7 @@
height: 7px;
}
::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {

View File

@@ -1,34 +0,0 @@
/* Button 按钮
------------------------------- */
@mixin Button($main, $c1, $c2) {
color: set-color($main);
background: set-color($c1);
border-color: set-color($c2);
}
/* Radio 单选框、Checkbox 多选框
------------------------------- */
@mixin RadioCheckbox($name) {
background-color: set-color($name);
border-color: set-color($name);
}
/* Tag 标签
------------------------------- */
@mixin Tag($main, $c1, $c2) {
color: set-color($main);
background-color: set-color($c1);
border-color: set-color($c2);
}
@mixin TagDark($main, $c1) {
color: set-color($main);
background-color: set-color($c1);
}
/* Alert 警告
------------------------------- */
@mixin Alert($main, $c1, $c2) {
color: set-color($main);
background: set-color($c1);
border: 1px solid set-color($c2);
}

View File

@@ -1,5 +0,0 @@
/* 颜色调用函数
------------------------------- */
@function set-color($key) {
@return var(--color-#{$key});
}

View File

@@ -1,3 +1,15 @@
/* 第三方图标字体间距/大小设置
------------------------------- */
@mixin generalIcon {
font-size: 14px !important;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
justify-content: center;
}
/* 文本不换行
------------------------------- */
@mixin text-no-wrap() {

View File

@@ -1,5 +1,5 @@
declare interface UserInfoState<T = any> {
userInfo: any
userInfo: any;
}
declare interface ThemeConfigState {
@@ -37,6 +37,7 @@ declare interface ThemeConfigState {
isCacheTagsView: boolean;
isSortableTagsView: boolean;
isFooter: boolean;
isDark: boolean;
isGrayscale: boolean;
isInvert: boolean;
isWartermark: boolean;

View File

@@ -7,8 +7,7 @@
<img :src="userInfo.photo" />
<div class="home-card-first-right ml15">
<div class="flex-margin">
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}`
}}</div>
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div>
</div>
</div>
</div>
@@ -31,7 +30,7 @@
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
// import * as echarts from 'echarts';
import { CountUp } from 'countup.js';
import { formatAxis } from '@/common/utils/format.ts';
import { formatAxis } from '@/common/utils/format';
import { indexApi } from './api';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
@@ -65,9 +64,7 @@ const state = reactive({
],
});
const {
topCardItemList,
} = toRefs(state)
const { topCardItemList } = toRefs(state);
// 当前时间提示语
const currentTime = computed(() => {
@@ -179,8 +176,8 @@ onMounted(() => {
}
.home-card-first {
background: white;
border: 1px solid #ebeef5;
background: var(--bg-main-color);
border: 1px solid var(--el-border-color-light, #ebeef5);
display: flex;
align-items: center;
@@ -188,7 +185,7 @@ onMounted(() => {
width: 60px;
height: 60px;
border-radius: 100%;
border: 2px solid var(--color-primary-light-5);
border: 2px solid var(--el-color-primary-light-5);
}
.home-card-first-right {
@@ -247,7 +244,8 @@ onMounted(() => {
.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;
@@ -262,7 +260,7 @@ onMounted(() => {
position: relative;
i {
color: var(--color-primary);
color: var(--el-color-primary);
font-size: 12px;
position: absolute;
top: 1px;
@@ -284,7 +282,7 @@ onMounted(() => {
border-radius: 100%;
padding: 3px 2px 2px;
text-align: center;
color: var(--color-primary);
color: var(--el-color-primary);
}
}

View File

@@ -2,11 +2,18 @@
<div class="layout-columns-aside">
<el-scrollbar>
<ul>
<li v-for="(v, k) in state.columnsAsideList" :key="k" @click="onColumnsAsideMenuClick(v, k)" :ref="
<li
v-for="(v, k) in state.columnsAsideList"
:key="k"
@click="onColumnsAsideMenuClick(v, k)"
:ref="
(el) => {
if (el) columnsAsideOffsetTopRefs[k] = el;
}
" :class="{ 'layout-columns-active': state.liIndex === k }" :title="v.meta.title">
"
:class="{ 'layout-columns-active': state.liIndex === k }"
:title="v.meta.title"
>
<div class="layout-columns-aside-li-box" v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
<i :class="v.meta.icon"></i>
<div class="layout-columns-aside-li-box-title font12">
@@ -166,7 +173,7 @@ onBeforeRouteUpdate((to) => {
}
.columns-round {
background: var(--color-primary);
background: var(--el-color-primary);
color: #ffffff;
position: absolute;
left: 50%;

View File

@@ -67,7 +67,7 @@ import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
// 定义变量内容
const layoutLockScreenDateRef = ref<null>();
const layoutLockScreenDateRef = ref<any>();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
@@ -213,7 +213,7 @@ onUnmounted(() => {
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background-image: url('@/assets/image/bg-login.png');
background: url('@/assets/image/bg-login.png') no-repeat;
background-size: 100% 100%;
z-index: 9999991;
}

View File

@@ -3,7 +3,9 @@
<img src="@/assets/image/logo.svg" class="layout-logo-medium-img" />
<span>
{{ `${themeConfig.globalTitle}` }}
<sub><span style="font-size: 10px;color:goldenrod">{{ ` ${config.version}` }}</span></sub>
<sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${config.version}` }}</span></sub
>
</span>
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
@@ -41,14 +43,14 @@ const onThemeConfigChange = () => {
align-items: center;
justify-content: center;
box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
color: var(--color-primary);
color: var(--el-color-primary);
font-size: 16px;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&:hover {
span {
color: var(--color-primary-light-2);
color: var(--el-color-primary-light-2);
}
}

View File

@@ -32,7 +32,7 @@ export default {
watch(
() => route.path,
() => {
proxy.$refs.layoutDefaultsScrollbarRef.wrap$.scrollTop = 0;
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
}
);
return {

View File

@@ -20,7 +20,6 @@ import Logo from '@/views/layout/logo/index.vue';
import Horizontal from '@/views/layout/navMenu/horizontal.vue';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());
const { routesList } = storeToRefs(useRoutesList());
const route = useRoute();
@@ -106,6 +105,6 @@ onUnmounted(() => {
padding-right: 15px;
background: var(--bg-topBar);
overflow: hidden;
border-bottom: 1px solid #f1f2f3;
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
</style>

View File

@@ -1,47 +1,48 @@
<template>
<div class="layout-breadcrumb-seting">
<el-drawer title="布局设置" v-model="themeConfig.isDrawer" direction="rtl" destroy-on-close size="240px"
@close="onDrawerClose">
<el-drawer title="布局设置" v-model="themeConfig.isDrawer" direction="rtl" destroy-on-close size="240px" @close="onDrawerClose">
<el-scrollbar class="layout-breadcrumb-seting-bar">
<!-- ssh终端主题 -->
<el-divider content-position="left">终端主题</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-color-picker v-model="themeConfig.terminalForeground" size="small"
@change="onColorPickerChange('terminalForeground')">
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
</el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.terminalBackground" size="small"
@change="onColorPickerChange('terminalBackground')">
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
</el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalCursor" size="small"
@change="onColorPickerChange('terminalCursor')">
</el-color-picker>
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
</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-input-number v-model="themeConfig.terminalFontSize" controls-position="right" :min="12"
:max="24" @change="setLocalThemeConfig" size="small" style="width: 90px">
<el-input-number
v-model="themeConfig.terminalFontSize"
controls-position="right"
:min="12"
:max="24"
@change="setLocalThemeConfig"
size="small"
style="width: 90px"
>
</el-input-number>
</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-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small"
style="width: 90px">
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
<el-option label="normal" value="normal"> </el-option>
<el-option label="bold" value="bold"> </el-option>
</el-select>
@@ -52,8 +53,7 @@
<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="themeConfig.editorTheme" size="small"
style="width: 130px">
<el-select @change="setLocalThemeConfig" v-model="themeConfig.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>
@@ -66,36 +66,31 @@
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">primary</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.primary" size="small"
@change="onColorPickerChange('primary')"> </el-color-picker>
<el-color-picker v-model="themeConfig.primary" size="small" @change="onColorPickerChange('primary')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">success</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.success" size="small"
@change="onColorPickerChange('success')"> </el-color-picker>
<el-color-picker v-model="themeConfig.success" size="small" @change="onColorPickerChange('success')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">info</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.info" size="small" @change="onColorPickerChange('info')">
</el-color-picker>
<el-color-picker v-model="themeConfig.info" size="small" @change="onColorPickerChange('info')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">warning</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.warning" size="small"
@change="onColorPickerChange('warning')"> </el-color-picker>
<el-color-picker v-model="themeConfig.warning" size="small" @change="onColorPickerChange('warning')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">danger</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.danger" size="small" @change="onColorPickerChange('danger')">
</el-color-picker>
<el-color-picker v-model="themeConfig.danger" size="small" @change="onColorPickerChange('danger')"> </el-color-picker>
</div>
</div>
@@ -104,46 +99,37 @@
<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-color-picker v-model="themeConfig.topBar" size="small"
@change="onBgColorPickerChange('topBar')"> </el-color-picker>
<el-color-picker v-model="themeConfig.topBar" size="small" @change="onBgColorPickerChange('topBar')"> </el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.menuBar" size="small"
@change="onBgColorPickerChange('menuBar')"> </el-color-picker>
<el-color-picker v-model="themeConfig.menuBar" size="small" @change="onBgColorPickerChange('menuBar')"> </el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.columnsMenuBar" size="small"
@change="onBgColorPickerChange('columnsMenuBar')">
</el-color-picker>
<el-color-picker v-model="themeConfig.columnsMenuBar" size="small" @change="onBgColorPickerChange('columnsMenuBar')"> </el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.topBarColor" size="small"
@change="onBgColorPickerChange('topBarColor')">
</el-color-picker>
<el-color-picker v-model="themeConfig.topBarColor" size="small" @change="onBgColorPickerChange('topBarColor')"> </el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.menuBarColor" size="small"
@change="onBgColorPickerChange('menuBarColor')">
</el-color-picker>
<el-color-picker v-model="themeConfig.menuBarColor" size="small" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
</div>
</div>
<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-color-picker v-model="themeConfig.columnsMenuBarColor" size="small"
@change="onBgColorPickerChange('columnsMenuBarColor')">
<el-color-picker v-model="themeConfig.columnsMenuBarColor" size="small" @change="onBgColorPickerChange('columnsMenuBarColor')">
</el-color-picker>
</div>
</div>
@@ -162,15 +148,13 @@
<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-switch v-model="themeConfig.isColumnsMenuBarColorGradual"
@change="onColumnsMenuBarGradualChange"></el-switch>
<el-switch v-model="themeConfig.isColumnsMenuBarColorGradual" @change="onColumnsMenuBarGradualChange"></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-switch v-model="themeConfig.isMenuBarColorHighlight"
@change="onMenuBarHighlightChange"></el-switch>
<el-switch v-model="themeConfig.isMenuBarColorHighlight" @change="onMenuBarHighlightChange"></el-switch>
</div>
</div>
@@ -194,12 +178,10 @@
<el-switch v-model="themeConfig.isFixedHeader" @change="onIsFixedHeaderChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15"
:style="{ opacity: themeConfig.layout !== 'classic' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: themeConfig.layout !== 'classic' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">经典布局分割菜单</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="themeConfig.isClassicSplitMenu" :disabled="themeConfig.layout !== 'classic'"
@change="onClassicSplitMenuChange">
<el-switch v-model="themeConfig.isClassicSplitMenu" :disabled="themeConfig.layout !== 'classic'" @change="onClassicSplitMenuChange">
</el-switch>
</div>
</div>
@@ -212,8 +194,15 @@
<div class="layout-breadcrumb-seting-bar-flex mt11">
<div class="layout-breadcrumb-seting-bar-flex-label">自动锁屏(s/)</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input-number v-model="themeConfig.lockScreenTime" controls-position="right" :min="0" :max="9999"
@change="setLocalThemeConfig" size="small" style="width: 90px">
<el-input-number
v-model="themeConfig.lockScreenTime"
controls-position="right"
:min="0"
:max="9999"
@change="setLocalThemeConfig"
size="small"
style="width: 90px"
>
</el-input-number>
</div>
</div>
@@ -226,12 +215,14 @@
<el-switch v-model="themeConfig.isShowLogo" @change="onIsShowLogoChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15"
:style="{ opacity: themeConfig.layout === 'transverse' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: themeConfig.layout === 'transverse' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">开启Breadcrumb</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="themeConfig.isBreadcrumb" :disabled="themeConfig.layout === 'transverse'"
@change="onIsBreadcrumbChange"></el-switch>
<el-switch
v-model="themeConfig.isBreadcrumb"
:disabled="themeConfig.layout === 'transverse'"
@change="onIsBreadcrumbChange"
></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
@@ -288,8 +279,7 @@
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">Tagsview 风格</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px"
@change="setLocalThemeConfig">
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="风格1" value="tags-style-one"></el-option>
<el-option label="风格2" value="tags-style-two"></el-option>
<el-option label="风格3" value="tags-style-three"></el-option>
@@ -299,8 +289,7 @@
<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 v-model="themeConfig.animation" placeholder="请选择" size="small" style="width: 90px"
@change="setLocalThemeConfig">
<el-select v-model="themeConfig.animation" placeholder="请选择" size="small" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="slide-right" value="slide-right"></el-option>
<el-option label="slide-left" value="slide-left"></el-option>
<el-option label="opacitys" value="opacitys"></el-option>
@@ -310,8 +299,7 @@
<div class="layout-breadcrumb-seting-bar-flex mt15 mb28">
<div class="layout-breadcrumb-seting-bar-flex-label">分栏高亮风格</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.columnsAsideStyle" placeholder="请选择" size="small"
style="width: 90px" @change="setLocalThemeConfig">
<el-select v-model="themeConfig.columnsAsideStyle" placeholder="请选择" size="small" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="圆角" value="columns-round"></el-option>
<el-option label="卡片" value="columns-card"></el-option>
</el-select>
@@ -323,16 +311,14 @@
<div class="layout-drawer-content-flex">
<!-- defaults 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('defaults')">
<section class="el-container el-circular"
:class="{ 'drawer-layout-active': themeConfig.layout === 'defaults' }">
<section class="el-container el-circular" :class="{ 'drawer-layout-active': themeConfig.layout === 'defaults' }">
<aside class="el-aside" style="width: 20px"></aside>
<section class="el-container is-vertical">
<header class="el-header" style="height: 10px"></header>
<main class="el-main"></main>
</section>
</section>
<div class="layout-tips-warp"
:class="{ 'layout-tips-warp-active': themeConfig.layout === 'defaults' }">
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': themeConfig.layout === 'defaults' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">默认</p>
</div>
@@ -340,8 +326,7 @@
</div>
<!-- classic 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('classic')">
<section class="el-container is-vertical el-circular"
:class="{ 'drawer-layout-active': themeConfig.layout === 'classic' }">
<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': themeConfig.layout === 'classic' }">
<header class="el-header" style="height: 10px"></header>
<section class="el-container">
<aside class="el-aside" style="width: 20px"></aside>
@@ -350,8 +335,7 @@
</section>
</section>
</section>
<div class="layout-tips-warp"
:class="{ 'layout-tips-warp-active': themeConfig.layout === 'classic' }">
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': themeConfig.layout === 'classic' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">经典</p>
</div>
@@ -359,8 +343,7 @@
</div>
<!-- transverse 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('transverse')">
<section class="el-container is-vertical el-circular"
:class="{ 'drawer-layout-active': themeConfig.layout === 'transverse' }">
<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': themeConfig.layout === 'transverse' }">
<header class="el-header" style="height: 10px"></header>
<section class="el-container">
<section class="el-container is-vertical">
@@ -368,8 +351,7 @@
</section>
</section>
</section>
<div class="layout-tips-warp"
:class="{ 'layout-tips-warp-active': themeConfig.layout === 'transverse' }">
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': themeConfig.layout === 'transverse' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">横向</p>
</div>
@@ -377,8 +359,7 @@
</div>
<!-- columns 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('columns')">
<section class="el-container el-circular"
:class="{ 'drawer-layout-active': themeConfig.layout === 'columns' }">
<section class="el-container el-circular" :class="{ 'drawer-layout-active': themeConfig.layout === 'columns' }">
<aside class="el-aside-dark" style="width: 10px"></aside>
<aside class="el-aside" style="width: 20px"></aside>
<section class="el-container is-vertical">
@@ -386,8 +367,7 @@
<main class="el-main"></main>
</section>
</section>
<div class="layout-tips-warp"
:class="{ 'layout-tips-warp-active': themeConfig.layout === 'columns' }">
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': themeConfig.layout === 'columns' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">分栏</p>
</div>
@@ -395,10 +375,15 @@
</div>
</div>
<div class="copy-config">
<el-alert title="点击下方按钮,复制布局配置去 /src/store/modules/themeConfig.ts中修改" type="warning" :closable="false">
</el-alert>
<el-button size="small" class="copy-config-btn" icon="el-icon-document-copy" type="primary"
ref="copyConfigBtnRef" @click="onCopyConfigClick($event.target)">一键复制配置
<el-alert title="点击下方按钮,复制布局配置去 /src/store/modules/themeConfig.ts中修改" type="warning" :closable="false"> </el-alert>
<el-button
size="small"
class="copy-config-btn"
icon="el-icon-document-copy"
type="primary"
ref="copyConfigBtnRef"
@click="onCopyConfigClick($event.target)"
>一键复制配置
</el-button>
</div>
</el-scrollbar>
@@ -412,8 +397,8 @@ import { ElMessage } from 'element-plus';
import ClipboardJS from 'clipboard';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLightColor } from '@/common/utils/theme.ts';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage.ts';
import { getLightColor } from '@/common/utils/theme';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
const copyConfigBtnRef = ref();
@@ -428,7 +413,7 @@ const onColorPickerChange = (color: string) => {
const setPropertyFun = (color: string, targetVal: any) => {
document.documentElement.style.setProperty(color, targetVal);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`${color}-light-${i}`, getLightColor(targetVal, i / 10));
document.documentElement.style.setProperty(`${color}-light-${i}`, getLightColor(targetVal, i / 10) as any);
}
};
// 2、菜单 / 顶栏
@@ -449,11 +434,7 @@ const onMenuBarGradualChange = () => {
};
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
const onColumnsMenuBarGradualChange = () => {
setGraduaFun(
'.layout-container .layout-columns-aside',
themeConfig.value.isColumnsMenuBarColorGradual,
themeConfig.value.columnsMenuBar
);
setGraduaFun('.layout-container .layout-columns-aside', themeConfig.value.isColumnsMenuBarColorGradual, themeConfig.value.columnsMenuBar);
};
// 2、菜单 / 顶栏 --> 背景渐变函数
const setGraduaFun = (el: string, bool: boolean, color: string) => {
@@ -522,17 +503,14 @@ const onSortableTagsViewChange = () => {
mittBus.emit('openOrCloseSortable');
setLocalThemeConfig();
};
// 4、界面显示 --> 灰色模式/色弱模式
// 4、界面显示 --> 暗模式/灰色模式/色弱模式
const onAddFilterChange = (attr: string) => {
if (attr === 'grayscale') {
if (themeConfig.value.isGrayscale) themeConfig.value.isInvert = false;
} else {
if (themeConfig.value.isInvert) themeConfig.value.isGrayscale = false;
}
const cssAttr =
attr === 'grayscale'
? `grayscale(${themeConfig.value.isGrayscale ? 1 : 0})`
: `invert(${themeConfig.value.isInvert ? '80%' : '0%'})`;
const cssAttr = attr === 'grayscale' ? `grayscale(${themeConfig.value.isGrayscale ? 1 : 0})` : `invert(${themeConfig.value.isInvert ? '80%' : '0%'})`;
const appEle: any = document.querySelector('#app');
appEle.setAttribute('style', `filter: ${cssAttr}`);
setLocalThemeConfig();
@@ -549,49 +527,37 @@ const onSetLayout = (layout: string) => {
};
// 设置布局切换,重置主题样式
const initSetLayoutChange = () => {
// themeConfig.value.menuBar = '#FFFFFF';
// themeConfig.value.menuBarColor = '#606266';
// themeConfig.value.topBar = '#ffffff';
// themeConfig.value.topBarColor = '#606266';
if (themeConfig.value.layout === 'classic') {
themeConfig.value.isShowLogo = true;
themeConfig.value.isBreadcrumb = true;
themeConfig.value.isCollapse = false;
themeConfig.value.isClassicSplitMenu = false;
themeConfig.value.menuBar = '#FFFFFF';
themeConfig.value.menuBarColor = '#606266';
themeConfig.value.topBar = '#ffffff';
themeConfig.value.topBarColor = '#606266';
initLayoutChangeFun();
} else if (themeConfig.value.layout === 'transverse') {
themeConfig.value.isShowLogo = true;
themeConfig.value.isBreadcrumb = false;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
themeConfig.value.menuBarColor = '#FFFFFF';
themeConfig.value.topBar = '#545c64';
themeConfig.value.topBarColor = '#FFFFFF';
initLayoutChangeFun();
} else if (themeConfig.value.layout === 'columns') {
themeConfig.value.isShowLogo = true;
themeConfig.value.isBreadcrumb = true;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
themeConfig.value.menuBar = '#FFFFFF';
themeConfig.value.menuBarColor = '#606266';
themeConfig.value.topBar = '#ffffff';
themeConfig.value.topBarColor = '#606266';
initLayoutChangeFun();
} else {
themeConfig.value.isShowLogo = false;
themeConfig.value.isBreadcrumb = true;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
themeConfig.value.menuBar = '#545c64';
themeConfig.value.menuBarColor = '#eaeaea';
themeConfig.value.topBar = '#FFFFFF';
themeConfig.value.topBarColor = '#606266';
initLayoutChangeFun();
}
initLayoutChangeFun();
};
// 设置布局切换函数
const initLayoutChangeFun = () => {
@@ -660,6 +626,7 @@ onMounted(() => {
onMenuBarHighlightChange();
themeConfig.value.isCollapse = false;
});
window.addEventListener('load', () => {
// 刷新页面时,设置了值,直接取缓存中的值进行初始化
setTimeout(() => {
@@ -691,7 +658,7 @@ onMounted(() => {
}
// // 语言国际化
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
}, 1100);
}, 100);
});
});
});
@@ -701,7 +668,7 @@ onUnmounted(() => {
mittBus.off('layoutMobileResize');
});
defineExpose({openDrawer})
defineExpose({ openDrawer });
</script>
<style scoped lang="scss">
@@ -767,7 +734,7 @@ defineExpose({openDrawer})
.drawer-layout-active {
border: 1px solid;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
}
.layout-tips-warp,
@@ -778,7 +745,7 @@ defineExpose({openDrawer})
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid;
border-color: var(--color-primary-light-4);
border-color: var(--el-color-primary-light-4);
border-radius: 100%;
padding: 4px;
@@ -788,7 +755,7 @@ defineExpose({openDrawer})
height: 30px;
z-index: 9;
border: 1px solid;
border-color: var(--color-primary-light-4);
border-color: var(--el-color-primary-light-4);
border-radius: 100%;
.layout-tips-txt {
@@ -799,7 +766,7 @@ defineExpose({openDrawer})
line-height: 1;
letter-spacing: 2px;
white-space: nowrap;
color: var(--color-primary-light-4);
color: var(--el-color-primary-light-4);
text-align: center;
transform: rotate(30deg);
left: -1px;
@@ -813,14 +780,14 @@ defineExpose({openDrawer})
.layout-tips-warp-active {
border: 1px solid;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
.layout-tips-box {
border: 1px solid;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
.layout-tips-txt {
color: var(--color-primary) !important;
color: var(--el-color-primary) !important;
background-color: #e9eef3 !important;
}
}
@@ -830,20 +797,20 @@ defineExpose({openDrawer})
.el-circular {
transition: all 0.3s ease-in-out;
border: 1px solid;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
}
.layout-tips-warp {
transition: all 0.3s ease-in-out;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
.layout-tips-box {
transition: inherit;
border-color: var(--color-primary);
border-color: var(--el-color-primary);
.layout-tips-txt {
transition: inherit;
color: var(--color-primary) !important;
color: var(--el-color-primary) !important;
background-color: #e9eef3 !important;
}
}

View File

@@ -1,6 +1,16 @@
<template>
<div class="layout-navbars-breadcrumb-user" :style="{ flex: layoutUserFlexNum }">
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
<div class="layout-navbars-breadcrumb-user-icon">
<el-switch
@change="switchDark(state.isDark)"
v-model="state.isDark"
active-action-icon="Moon"
inactive-action-icon="Sunny"
style="--el-switch-off-color: #c4c9c4; --el-switch-on-color: #2c2c2c"
class="dark-icon"
/>
</div>
<!-- <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
<div class="layout-navbars-breadcrumb-user-icon">
<el-icon title="组件大小">
<plus />
@@ -13,7 +23,7 @@
<el-dropdown-item command="small" :disabled="state.disabledSize === 'small'">小型</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown> -->
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon title="菜单搜索">
<search />
@@ -25,8 +35,7 @@
</el-icon>
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" :visible="state.isShowUserNewsPopover" :width="300"
popper-class="el-popover-pupop-user-news">
<el-popover placement="bottom" trigger="click" :visible="state.isShowUserNewsPopover" :width="300" popper-class="el-popover-pupop-user-news">
<template #reference>
<el-badge :is-dot="false" @click="state.isShowUserNewsPopover = !state.isShowUserNewsPopover">
<el-icon title="消息">
@@ -66,22 +75,24 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted } from 'vue';
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
import { resetRoute } from '@/router/index.ts';
import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig';
import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage.ts';
import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi';
const router = useRouter();
const searchRef = ref();
const state = reactive({
isDark: false,
isScreenfull: false,
isShowUserNewsPopover: false,
disabledI18n: 'zh-cn',
@@ -122,8 +133,9 @@ const onHandleCommandClick = (path: string) => {
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
beforeClose: (action, instance, done) => {
beforeClose: async (action, instance, done) => {
if (action === 'confirm') {
await openApi.logout();
instance.confirmButtonLoading = true;
instance.confirmButtonText = '退出中';
setTimeout(() => {
@@ -151,6 +163,19 @@ const onHandleCommandClick = (path: string) => {
}
};
const switchDark = (isDark: boolean) => {
themeConfig.value.isDark = isDark;
setLocal('themeConfig', themeConfig.value);
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
themeConfig.value.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
themeConfig.value.editorTheme = 'SolarizedLight';
}
};
// // 菜单搜索点击
const onSearchClick = () => {
searchRef.value.openSearch();
@@ -187,6 +212,10 @@ const initComponentSize = () => {
// 页面加载时
onMounted(() => {
if (getLocal('themeConfig')) {
const isDark = themeConfig.value.isDark;
state.isDark = isDark;
switchDark(isDark);
initComponentSize();
}
});
@@ -244,4 +273,5 @@ onMounted(() => {
::v-deep(.el-badge__content.is-fixed) {
top: 12px;
}
}</style>
}
</style>

View File

@@ -39,9 +39,7 @@ export default {
state.newsList = [];
};
// 前往通知中心点击
const toMsgCenter = () => {
};
const toMsgCenter = () => {};
return {
onAllReadClick,
toMsgCenter,
@@ -62,7 +60,7 @@ export default {
height: 35px;
align-items: center;
.head-box-btn {
color: var(--color-primary);
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
@@ -90,7 +88,7 @@ export default {
}
.foot-box {
height: 35px;
color: var(--color-primary);
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;

View File

@@ -2,29 +2,43 @@
<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': themeConfig.layout === 'classic' }">
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<li v-for="(v, k) in state.tagsViewList" :key="k" class="layout-navbars-tagsview-ul-li" :data-name="v.name"
:class="{ 'is-active': isActive(v) }" @contextmenu.prevent="onContextmenu(v, $event)"
@click="onTagsClick(v, k)" :ref="
<li
v-for="(v, k) in state.tagsViewList"
:key="k"
class="layout-navbars-tagsview-ul-li"
:data-name="v.name"
:class="{ 'is-active': isActive(v) }"
@contextmenu.prevent="onContextmenu(v, $event)"
@click="onTagsClick(v, k)"
:ref="
(el) => {
if (el) tagsRefs[k] = el;
}
">
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14"
v-if="isActive(v)" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont"
v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
"
>
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.meta.title }}</span>
<template v-if="isActive(v)">
<SvgIcon name="RefreshRight" class="font14 ml5 layout-navbars-tagsview-ul-li-refresh"
@click.stop="refreshCurrentTagsView($route.fullPath)" />
<SvgIcon name="Close" class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
<SvgIcon
name="RefreshRight"
class="font14 ml5 layout-navbars-tagsview-ul-li-refresh"
@click.stop="refreshCurrentTagsView($route.fullPath)"
/>
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
v-if="!v.meta.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)" />
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
<SvgIcon name="Close" class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.meta.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)" />
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
</ul>
</el-scrollbar>
@@ -107,7 +121,7 @@ const addTagsView = (path: string, to: any = null) => {
}
}
const tagView = { ...to }
const tagView = { ...to };
// 防止Converting circular structure to JSON错误
tagView.matched = null;
tagView.redirectedFrom = null;
@@ -135,7 +149,7 @@ const closeCurrentTagsView = (path: string) => {
let next;
// 最后一个且高亮时
if (state.tagsViewList.length === k) {
next = k !== arr.length ? arr[k] : arr[arr.length - 1]
next = k !== arr.length ? arr[k] : arr[arr.length - 1];
} else {
next = arr[k];
}
@@ -366,8 +380,8 @@ onBeforeRouteUpdate((to) => {
<style scoped lang="scss">
.layout-navbars-tagsview {
background-color: var(--el-color-white);
border-bottom: 1px solid var(--next-border-color-light);
background-color: var(--bg-main-color);
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
position: relative;
z-index: 4;

View File

@@ -2,29 +2,47 @@
<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-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" @keyup.enter="login" show-password>
<el-input
type="password"
placeholder="请输入密码"
prefix-icon="lock"
v-model="loginForm.password"
autocomplete="off"
@keyup.enter="login"
show-password
>
</el-input>
</el-form-item>
<el-form-item v-if="isUseLoginCaptcha" prop="captcha">
<el-form-item v-if="accountLoginSecurity.useCaptcha" 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-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" />
<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 v-if="ldapEnabled" prop="ldapLogin">
<el-checkbox v-model="loginForm.ldapLogin" label="LDAP 登录" size="small" />
</el-form-item>
<span v-if="showLoginFailTips" style="color: #f56c6c; font-size: 12px">
提示登录失败超过{{ accountLoginSecurity.loginFailCount }}次后将被限制{{ accountLoginSecurity.loginFailMin }}分钟内不可再次登录
</span>
<el-form-item>
<el-button type="primary" class="login-content-submit" round @click="login" :loading="loading.signIn">
<span> </span>
@@ -32,20 +50,21 @@
</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-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="auto">
<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-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-input
v-model.trim="changePwdDialog.form.newPassword"
placeholder="须为8位以上且包含字⺟⼤⼩写+数字+特殊符号"
type="password"
autocomplete="new-password"
></el-input>
</el-form-item>
</el-form>
@@ -56,6 +75,55 @@
</div>
</template>
</el-dialog>
<el-dialog
title="OTP校验"
v-model="otpDialog.visible"
@close="loading.signIn = false"
:close-on-click-modal="false"
width="350px"
:destroy-on-close="true"
>
<el-form ref="otpFormRef" :model="otpDialog.form" :rules="otpDialog.rules" @submit.native.prevent label-width="auto">
<el-form-item v-if="otpDialog.otpUrl" label="二维码">
<qrcode-vue :value="otpDialog.otpUrl" :size="200" level="H" />
</el-form-item>
<el-form-item prop="code" label="OTP" required>
<el-input
style="width: 220px"
ref="otpCodeInputRef"
v-model.trim="otpDialog.form.code"
clearable
@keyup.enter="otpVerify"
placeholder="请输入令牌APP中显示的授权码"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="otpVerify" type="primary" :loading="loading.otpConfirm"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog title="修改基本信息" v-model="baseInfoDialog.visible" :close-on-click-modal="false" width="450px" :destroy-on-close="true">
<el-form :model="baseInfoDialog.form" :rules="baseInfoDialog.rules" ref="baseInfoFormRef" label-width="auto">
<el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="baseInfoDialog.form.username"></el-input>
</el-form-item>
<el-form-item prop="name" label="姓名" required>
<el-input v-model.trim="baseInfoDialog.form.name"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="updateUserInfo()" type="primary" :loading="loading.updateUserConfirm"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -64,34 +132,48 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
import { getSession, setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { useLoginCaptcha, useWartermark } from '@/common/sysconfig';
import { getAccountLoginSecurity, getLdapEnabled, useWartermark } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
}
};
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
const changePwdFormRef: any = ref(null);
const otpFormRef: any = ref(null);
const otpCodeInputRef: any = ref(null);
const baseInfoFormRef: any = ref(null);
const state = reactive({
isUseLoginCaptcha: false,
accountLoginSecurity: {
useCaptcha: true,
useOtp: false,
loginFailCount: 5,
loginFailMin: 10,
},
showLoginFailTips: false,
captchaImage: '',
loginForm: {
username: '',
password: '',
captcha: '',
cid: '',
ldapLogin: false,
},
loginRes: {} as any,
changePwdDialog: {
visible: false,
form: {
@@ -110,34 +192,67 @@ const state = reactive({
],
},
},
otpDialog: {
visible: false,
otpUrl: '',
form: {
code: '',
otpToken: '',
},
rules: {
code: [{ required: true, message: '请输入OTP授权码', trigger: 'blur' }],
},
},
baseInfoDialog: {
visible: false,
form: {
username: '',
name: '',
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: AccountUsernamePattern.pattern,
message: AccountUsernamePattern.message,
trigger: ['blur'],
},
],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
},
},
loading: {
signIn: false,
changePwd: false,
otpConfirm: false,
updateUserConfirm: false,
},
ldapEnabled: false,
});
const {
isUseLoginCaptcha,
captchaImage,
loginForm,
changePwdDialog,
loading,
} = toRefs(state)
const { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, baseInfoDialog, loading, ldapEnabled } = toRefs(state);
onMounted(async () => {
nextTick(async () => {
state.isUseLoginCaptcha = await useLoginCaptcha();
const res = await getAccountLoginSecurity();
if (res) {
state.accountLoginSecurity = res;
}
getCaptcha();
const ldap = await getLdapEnabled();
state.ldapEnabled = ldap;
state.loginForm.ldapLogin = ldap;
});
// 移除公钥, 方便后续重新获取
sessionStorage.removeItem('RsaPublicKey');
});
const getCaptcha = async () => {
if (!state.isUseLoginCaptcha) {
if (!state.accountLoginSecurity.useCaptcha) {
return;
}
let res: any = await openApi.captcha.request();
let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid;
};
@@ -158,6 +273,22 @@ const login = () => {
});
};
const otpVerify = async () => {
otpFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.otpConfirm = true;
const accessToken = await openApi.otpVerify(state.otpDialog.form);
await signInSuccess(accessToken);
state.otpDialog.visible = false;
} finally {
state.loading.otpConfirm = false;
}
});
};
// 登录
const onSignIn = async () => {
state.loading.signIn = true;
@@ -166,9 +297,11 @@ const onSignIn = async () => {
try {
const loginReq = { ...state.loginForm };
loginReq.password = await RsaEncrypt(originPwd);
loginRes = await openApi.login.request(loginReq);
// 存储 token 到浏览器缓存
setSession('token', loginRes.token);
if (state.loginForm.ldapLogin) {
loginRes = await openApi.ldapLogin(loginReq);
} else {
loginRes = await openApi.login(loginReq);
}
} catch (e: any) {
state.loading.signIn = false;
state.loginForm.captcha = '';
@@ -180,17 +313,42 @@ const onSignIn = async () => {
state.changePwdDialog.visible = true;
} else {
getCaptcha();
state.showLoginFailTips = true;
}
return;
}
state.showLoginFailTips = false;
loginResDeal(loginRes);
};
const updateUserInfo = async () => {
baseInfoFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.updateUserConfirm = true;
const form = state.baseInfoDialog.form;
await personApi.updateAccount.request(state.baseInfoDialog.form);
state.baseInfoDialog.visible = false;
useUserInfo().userInfo.username = form.username;
useUserInfo().userInfo.name = form.name;
await toIndex();
} finally {
state.loading.updateUserConfirm = false;
}
});
};
const loginResDeal = (loginRes: any) => {
state.loginRes = loginRes;
// 用户信息
const userInfos = {
name: loginRes.name,
username: state.loginForm.username,
username: loginRes.username,
// 头像
photo: letterAvatar(state.loginForm.username),
photo: letterAvatar(loginRes.username),
time: new Date().getTime(),
permissions: loginRes.permissions,
lastLoginTime: loginRes.lastLoginTime,
lastLoginIp: loginRes.lastLoginIp,
};
@@ -199,12 +357,42 @@ const onSignIn = async () => {
setUserInfo2Session(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
useUserInfo().setUserInfo(userInfos);
await initRouter();
signInSuccess();
const token = loginRes.token;
// 如果不需要otp校验则该token即为accessToken否则为otp校验token
if (loginRes.otp == -1) {
signInSuccess(token);
return;
}
state.otpDialog.form.otpToken = token;
state.otpDialog.otpUrl = loginRes.otpUrl;
state.otpDialog.visible = true;
setTimeout(() => {
otpCodeInputRef.value.focus();
}, 400);
};
// 登录成功后的跳转
const signInSuccess = () => {
const signInSuccess = async (accessToken: string = '') => {
if (!accessToken) {
accessToken = getSession('token');
}
// 存储 token 到浏览器缓存
setSession('token', accessToken);
// 初始化路由
await initRouter();
// 判断是否为第一次oauth2登录是的话需要用户填写姓名和用户名
if (state.loginRes.isFirstOauth2Login) {
state.baseInfoDialog.form.username = state.loginRes.username;
state.baseInfoDialog.visible = true;
} else {
await toIndex();
}
};
const toIndex = async () => {
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
@@ -233,7 +421,7 @@ const changePwd = () => {
const changePwdReq: any = { ...form };
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd.request(changePwdReq);
await openApi.changePwd(changePwdReq);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false;
@@ -251,6 +439,10 @@ const cancelChangePwd = () => {
state.changePwdDialog.form.username = '';
getCaptcha();
};
defineExpose({
loginResDeal,
});
</script>
<style scoped lang="scss">

View File

@@ -1,8 +1,7 @@
<template>
<el-form class="login-content-form">
<el-form-item>
<el-input type="text" placeholder="请输入手机号" prefix-icon="el-icon-user" v-model="ruleForm.userName" clearable autocomplete="off">
</el-input>
<el-input type="text" placeholder="请输入手机号" prefix-icon="el-icon-user" v-model="ruleForm.userName" clearable autocomplete="off"> </el-input>
</el-form-item>
<el-form-item>
<el-row :gutter="15">

View File

@@ -9,7 +9,7 @@
<el-tabs v-model="tabsActiveName" @tab-click="onTabsClick">
<el-tab-pane label="账号密码登录" name="account" :disabled="tabsActiveName === 'account'">
<transition name="el-zoom-in-center">
<Account v-show="isTabPaneShow" />
<Account v-show="isTabPaneShow" ref="loginForm" />
</transition>
</el-tab-pane>
<!-- <el-tab-pane label="手机号登录" name="mobile" :disabled="tabsActiveName === 'mobile'">
@@ -18,10 +18,16 @@
</transition>
</el-tab-pane> -->
</el-tabs>
<!-- <div class="mt10">
<el-button type="text" size="small">第三方登录</el-button>
<el-button type="text" size="small">友情链接</el-button>
</div> -->
<div class="mt20" v-show="oauth2LoginConfig.enable">
<el-button link size="small">第三方登录: </el-button>
<el-tooltip :content="oauth2LoginConfig.name" placement="top-start">
<el-button link size="small" type="primary" @click="oauth2Login">
<el-icon :size="18">
<Link />
</el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
<!-- <div class="login-copyright">
@@ -32,27 +38,58 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive } from 'vue';
import { toRefs, reactive, onMounted, h, ref } from 'vue';
import Account from '@/views/login/component/AccountLogin.vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import openApi from '@/common/openApi';
import config from '@/common/config';
const { themeConfig } = storeToRefs(useThemeConfig());
const state = reactive({
tabsActiveName: 'account',
isTabPaneShow: true,
oauth2LoginConfig: {
name: 'OAuth2登录',
enable: false,
},
});
const {
isTabPaneShow,
tabsActiveName,
} = toRefs(state)
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
const { isTabPaneShow, tabsActiveName, oauth2LoginConfig: oauth2LoginConfig } = toRefs(state);
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
};
onMounted(async () => {
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
});
const oauth2Login = () => {
const width = 700;
const height = 500;
var iTop = (window.screen.height - 30 - height) / 2; //获得窗口的垂直位置;
var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置;
// 小窗口打开oauth2鉴权
let oauthWindow = window.open(config.baseApiUrl + '/auth/oauth2/login', 'oauth2', `height=${height},width=${width},top=${iTop},left=${iLeft},location=no`);
if (oauthWindow) {
const handler = (e: any) => {
if (e.data.action === 'oauthLogin') {
window.removeEventListener('message', handler);
loginForm.value!.loginResDeal(e.data);
}
};
window.addEventListener('message', handler);
setInterval(() => {
if (oauthWindow!.closed) {
window.removeEventListener('message', handler);
}
}, 1000);
}
};
</script>
<style scoped lang="scss">
@@ -70,7 +107,7 @@ const onTabsClick = () => {
display: flex;
align-items: center;
font-size: 20px;
color: var(--color-primary);
color: var(--el-color-primary);
letter-spacing: 2px;
width: 90%;
transform: translateX(-50%);
@@ -84,10 +121,10 @@ const onTabsClick = () => {
left: 50%;
transform: translate(-50%, -50%) translate3d(0, 0, 0);
background-color: rgba(255, 255, 255, 0.99);
box-shadow: 0 2px 12px 0 var(--color-primary-light-5);
box-shadow: 0 2px 12px 0 var(--el-color-primary-light-5);
border-radius: 4px;
transition: height 0.2s linear;
height: 480px;
height: 490px;
overflow: hidden;
z-index: 1;

View File

@@ -0,0 +1,39 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import openApi from '@/common/openApi';
const route = useRoute();
onMounted(async () => {
try {
const queryParam = route.query;
// 使用hash路由回调code可能会被设置到search
// 如 localhost:8888/?code=xxxx/oauth2/callback导致route.query获取不到值
if (location.search) {
const searchParams = location.search.split('?')[1];
if (searchParams) {
for (let searchParam of searchParams.split('&')) {
const searchParamSplit = searchParam.split('=');
queryParam[searchParamSplit[0]] = searchParamSplit[1];
}
}
}
const res: any = await openApi.oauth2Callback(queryParam);
ElMessage.success('授权认证成功');
top?.opener.postMessage(res);
window.close();
} catch (e: any) {
setTimeout(() => {
window.close();
}, 1500);
}
});
</script>
<style lang="scss"></style>

View File

@@ -1,10 +1,15 @@
<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
@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>
@@ -17,10 +22,10 @@ const props = defineProps({
modelValue: {
type: Number,
},
})
});
//定义事件
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue']);
const state = reactive({
// 单选则为id多选为id数组
@@ -28,10 +33,7 @@ const state = reactive({
sshTunnelMachineList: [] as any,
});
const {
sshTunnelMachineId,
sshTunnelMachineList,
} = toRefs(state)
const { sshTunnelMachineId, sshTunnelMachineList } = toRefs(state);
onMounted(async () => {
if (!props.modelValue || props.modelValue <= 0) {
@@ -52,7 +54,7 @@ const getSshTunnelMachines = async () => {
const clear = () => {
state.sshTunnelMachineId = null;
change();
}
};
const change = () => {
emit('update:modelValue', state.sshTunnelMachineId);

View File

@@ -1,6 +1,5 @@
<template>
<div
style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer;vertical-align: middle;">
<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>
@@ -25,20 +24,18 @@ const props = defineProps({
type: [String],
required: true,
},
})
});
const state = reactive({
tagPath: '',
tags: [] as any,
})
});
const {
tags,
} = toRefs(state)
const { tags } = toRefs(state);
onMounted(async () => {
state.tagPath = props.tagPath;
})
});
const showTagInfo = async () => {
if (state.tags && state.tags.length > 0) {
@@ -59,10 +56,7 @@ const showTagInfo = async () => {
tagPaths.push(nowTag);
}
state.tags = await tagApi.listByQuery.request({ tagPaths: tagPaths.join(',') });
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -1,12 +1,22 @@
<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"
<el-tree-select
v-bind="$attrs"
@check="changeTag"
style="width: 100%"
: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">
@@ -23,20 +33,12 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { useAttrs, toRefs, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api';
const props = defineProps({
tagId: {
type: Number,
},
tagPath: {
type: String,
},
})
const attrs = useAttrs();
//定义事件
const emit = defineEmits(['changeTag', 'update:tagId', 'update:tagPath'])
const emit = defineEmits(['changeTag', 'update:tagPath']);
const state = reactive({
tags: [],
@@ -44,29 +46,22 @@ const state = reactive({
selectTags: null as any,
});
const {
tags,
selectTags,
} = toRefs(state)
const { tags, selectTags } = toRefs(state);
onMounted(async () => {
if (props.tagId) {
state.selectTags = props.tagId;
if (attrs.modelValue) {
state.selectTags = attrs.modelValue;
}
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>
<style lang="scss"></style>

View File

@@ -1,13 +1,24 @@
<template>
<div class="instances-box">
<div class="tag-tree">
<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" @node-contextmenu="nodeContextmenu">
<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"
@node-contextmenu="nodeContextmenu"
>
<template #default="{ node, data }">
<span>
<span v-if="data.type == TagTreeNode.TagPath">
@@ -24,8 +35,7 @@
</el-tree>
</el-col>
</el-row>
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef"
@currentContextmenuClick="onCurrentContextmenuClick" />
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
</div>
</template>
@@ -38,7 +48,7 @@ import Contextmenu from '@/components/contextmenu/index.vue';
const props = defineProps({
height: {
type: [Number, String],
default: 0
default: 0,
},
load: {
type: Function,
@@ -47,17 +57,17 @@ const props = defineProps({
loadContextmenuItems: {
type: Function,
required: false,
}
})
},
});
const treeProps = {
label: 'name',
children: 'zones',
isLeaf: 'isLeaf',
}
};
const emit = defineEmits(['nodeClick', 'currentContextmenuClick'])
const treeRef: any = ref(null)
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
const treeRef: any = ref(null);
const contextmenuRef = ref();
const state = reactive({
@@ -69,25 +79,30 @@ const state = reactive({
},
contextmenuItems: [],
opend: {},
})
const { filterText } = toRefs(state)
});
const { filterText } = toRefs(state);
onMounted(async () => {
if (!props.height) {
state.height = window.innerHeight - 147 + 'px';
setHeight();
window.onresize = () => setHeight();
} else {
state.height = props.height;
}
})
});
const setHeight = () => {
state.height = window.innerHeight - 157 + 'px';
};
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
treeRef.value?.filter(val);
});
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
if (!value) return true;
return data.label.includes(value);
};
/**
* 加载树节点
@@ -98,20 +113,20 @@ const loadNode = async (node: any, resolve: any) => {
if (typeof resolve !== 'function') {
return;
}
let nodes = []
let nodes = [];
try {
nodes = await props.load(node)
nodes = await props.load(node);
} catch (e: any) {
console.error(e);
}
return resolve(nodes)
return resolve(nodes);
};
const treeNodeClick = (data: any) => {
emit('nodeClick', data);
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
}
};
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
@@ -119,7 +134,7 @@ const nodeContextmenu = (event: any, data: any) => {
return;
}
// 加载当前节点是否需要显示右击菜单
const items = props.loadContextmenuItems(data)
const items = props.loadContextmenuItems(data);
if (!items || items.length == 0) {
return;
}
@@ -128,17 +143,17 @@ const nodeContextmenu = (event: any, data: any) => {
state.dropdown.x = clientX;
state.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data);
}
};
const onCurrentContextmenuClick = (clickData: any) => {
emit('currentContextmenuClick', clickData);
}
};
const reloadNode = (nodeKey: any) => {
let node = getNode(nodeKey);
node.loaded = false;
node.expand();
}
};
const getNode = (nodeKey: any) => {
let node = treeRef.value.getNode(nodeKey);
@@ -146,18 +161,20 @@ const getNode = (nodeKey: any) => {
throw new Error('未找到节点: ' + nodeKey);
}
return node;
}
};
defineExpose({
reloadNode,
})
});
</script>
<style lang="scss">
.instances-box {
<style lang="scss" scoped>
.tag-tree {
overflow: 'auto';
position: relative;
border: 1px solid var(--el-border-color-light, #ebeef5);
.el-tree {
display: inline-block;
min-width: 100%;

View File

@@ -2,17 +2,17 @@ export class TagTreeNode {
/**
* 节点id
*/
key: any
key: any;
/**
* 节点名称
*/
label: string
label: string;
/**
* 树节点类型
*/
type: any
type: any;
isLeaf: boolean = false;

View File

@@ -1,87 +1,66 @@
<template>
<div>
<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-dialog
:title="title"
v-model="dialogVisible"
@open="open"
:before-close="cancel"
:close-on-click-modal="false"
:destroy-on-close="true"
width="38%"
>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="instanceId" label="数据库实例:" required>
<el-select
:disabled="form.id !== undefined"
remote
:remote-method="getInstances"
@change="getAllDatabase"
v-model="form.instanceId"
placeholder="请输入实例名称搜索并选择实例"
filterable
clearable
class="w100"
>
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.username }}
</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-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-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>
@@ -98,10 +77,7 @@
import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api';
import { ElMessage } 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';
const props = defineProps({
visible: {
@@ -113,10 +89,10 @@ const props = defineProps({
title: {
type: String,
},
})
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
tagId: [
@@ -126,6 +102,15 @@ const rules = {
trigger: ['change', 'blur'],
},
],
instanceId: [
{
required: true,
message: '请选择数据库实例',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
@@ -133,27 +118,6 @@ const rules = {
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,
@@ -161,57 +125,40 @@ const rules = {
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,
instanceId: null as any,
},
// 原密码
pwd: '',
btnLoading: false,
instances: [] as any,
});
const {
dialogVisible,
tabActiveName,
allDatabases,
databaseList,
form,
pwd,
btnLoading,
} = toRefs(state)
const { dialogVisible, allDatabases, databaseList, form, 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.form = {} as any;
state.databaseList = [];
}
});
@@ -224,27 +171,34 @@ const changeDatabase = () => {
};
const getAllDatabase = async () => {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
state.allDatabases = await dbApi.getAllDatabase.request(reqForm);
ElMessage.success('获取成功, 请选择需要管理操作的数据库');
if (state.form.instanceId > 0) {
state.allDatabases = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
}
};
const getDbPwd = async () => {
state.pwd = await dbApi.getDbPwd.request({ id: state.form.id });
const getInstances = async (instanceName: string = '', id = 0) => {
if (!id && !instanceName) {
state.instances = [];
return;
}
const data = await dbApi.instances.request({ id, name: instanceName });
if (data) {
state.instances = data.list;
}
};
const open = async () => {
if (state.form.instanceId) {
// 根据id获取因为需要回显实例名称
getInstances('', state.form.instanceId);
}
await getAllDatabase();
};
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);
@@ -265,6 +219,7 @@ const btnOk = async () => {
const resetInputDb = () => {
state.databaseList = [];
state.allDatabases = [];
state.instances = [];
};
const cancel = () => {

View File

@@ -1,328 +1,228 @@
<template>
<div class="db-list">
<el-card>
<el-button v-auth="permissions.saveDb" type="primary" icon="plus" @click="editDb(true)">添加</el-button>
<el-button v-auth="permissions.saveDb" :disabled="chooseId == null" @click="editDb(false)" type="primary"
icon="edit">编辑</el-button>
<el-button v-auth="permissions.delDb" :disabled="chooseId == null" @click="deleteDb(chooseId)" type="danger"
icon="delete">删除</el-button>
<div style="float: right">
<el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" filterable clearable>
<page-table
ref="pageTableRef"
:query="queryConfig"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="state.selectionData"
:data="datas"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #tagPathSelect>
<el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" filterable clearable style="width: 200px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-button type="success" icon="search" @click="search()" class="ml5"></el-button>
</div>
<el-table :data="datas" ref="table" @current-change="choose" show-overflow-tooltip stripe>
<el-table-column label="选择" width="60px">
<template #default="scope">
<el-radio v-model="chooseId" :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" />
<template #instanceSelect>
<el-select
remote
:remote-method="getInstances"
v-model="query.instanceId"
placeholder="输入并选择实例"
filterable
clearable
style="width: 200px"
>
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.username }}
</el-option>
</el-select>
</template>
<template #queryRight>
<el-button v-auth="perms.saveDb" type="primary" icon="plus" @click="editDb(false)">添加</el-button>
<el-button v-auth="perms.delDb" :disabled="selectionData.length < 1" @click="deleteDb()" type="danger" icon="delete">删除</el-button>
</template>
<template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
{{ data.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column min-width="170" label="host:port" show-overflow-tooltip>
<template #default="scope">
{{ `${scope.row.host}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column prop="type" label="类型" min-width="90"></el-table-column>
<el-table-column prop="database" label="数据库" min-width="80">
<template #default="scope">
<template #database="{ data }">
<el-popover placement="right" trigger="click" :width="300">
<template #reference>
<el-link type="primary" :underline="false" plain @click="selectDb(scope.row.dbs)">查看
</el-link>
<el-link type="primary" :underline="false" plain @click="selectDb(data.dbs)">查看 </el-link>
</template>
<el-input v-model="filterDb.param" @keyup="filterSchema" class="w-50 m-2" placeholder="搜索"
size="small">
<el-input v-model="filterDb.param" @keyup="filterSchema" class="w-50 m-2" placeholder="搜索" size="small">
<template #prefix>
<el-icon class="el-input__icon">
<search-icon />
</el-icon>
</template>
</el-input>
<div class="el-tag--plain el-tag--success" v-for="db in filterDb.list" :key="db"
style="border:1px var(--color-success-light-3) solid; margin-top: 3px;border-radius: 5px; padding: 2px;position: relative">
<div
class="el-tag--plain el-tag--success"
v-for="db in filterDb.list"
:key="db"
style="border: 1px var(--color-success-light-3) solid; margin-top: 3px; border-radius: 5px; padding: 2px; position: relative"
>
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
<el-link type="primary" plain size="small" :underline="false"
@click="showTableInfo(scope.row, db)" style="position: absolute; right: 4px">操作
<el-link type="primary" plain size="small" :underline="false" @click="showTableInfo(data, db)" style="position: absolute; right: 4px"
>操作
</el-link>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="100"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" min-width="160" fixed="right">
<template #default="scope">
<el-link plain size="small" :underline="false" @click="showInfo(scope.row)">
详情</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link class="ml5" type="primary" plain size="small" :underline="false"
@click="onShowSqlExec(scope.row)">
SQL执行记录</el-link>
<template #more="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button>
<el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
</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>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>编辑</el-button>
<el-button v-if="data.type == 'mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
</template>
</page-table>
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
<el-row class="mb10">
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
<template #reference>
<el-button class="ml5" type="success" size="small">导出</el-button>
</template>
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
</el-dialog>
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
<el-row justify="space-between">
<el-col :span="9">
<el-form-item label="导出内容: ">
<el-radio-group v-model="dumpInfo.type">
<el-radio :label="1" size="small">结构</el-radio>
<el-radio :label="2" size="small">数据</el-radio>
<el-radio :label="3" size="small">结构数据</el-radio>
<el-checkbox-group v-model="exportDialog.contents" :min="1">
<el-checkbox label="结构" />
<el-checkbox label="数据" />
</el-checkbox-group>
</el-form-item>
</el-col>
<el-col :span="9">
<el-form-item label="扩展名: ">
<el-radio-group v-model="exportDialog.extName">
<el-radio label="sql" />
<el-radio label="gzip" />
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="导出表: ">
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small"
:data="tableInfoDialog.infos">
<el-table-column type="selection" width="45" />
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
</el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
</el-table-column>
</el-table>
<el-form-item>
<el-transfer
v-model="exportDialog.value"
filterable
filter-placeholder="按数据库名称筛选"
:titles="['全部数据库', '导出数据库']"
:data="exportDialog.data"
max-height="300"
size="small"
/>
</el-form-item>
<div style="text-align: right">
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
<template #footer>
<div class="dialog-footer">
<el-button @click="exportDialog.visible = false">取消</el-button>
<el-button @click="dumpDbs()" type="primary">确定</el-button>
</div>
</el-popover>
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
</el-row>
<el-table v-loading="tableInfoDialog.loading" border stripe :data="filterTableInfos" size="small"
max-height="680">
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
<template #header>
<el-input v-model="tableInfoDialog.tableNameSearch" size="small" placeholder="表名: 输入可过滤"
clearable />
</template>
</el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
<template #header>
<el-input v-model="tableInfoDialog.tableCommentSearch" size="small" placeholder="备注: 输入可过滤"
clearable />
</template>
</el-table-column>
<el-table-column prop="tableRows" label="Rows" min-width="70" sortable
:sort-method="(a: any, b: any) => parseInt(a.tableRows) - parseInt(b.tableRows)"></el-table-column>
<el-table-column property="dataLength" label="数据大小" sortable
:sort-method="(a: any, b: any) => parseInt(a.dataLength) - parseInt(b.dataLength)">
<template #default="scope">
{{ formatByteSize(scope.row.dataLength) }}
</template>
</el-table-column>
<el-table-column property="indexLength" label="索引大小" sortable
:sort-method="(a: any, b: any) => parseInt(a.indexLength) - parseInt(b.indexLength)">
<template #default="scope">
{{ formatByteSize(scope.row.indexLength) }}
</template>
</el-table-column>
<el-table-column property="createTime" label="创建时间" min-width="150"> </el-table-column>
<el-table-column label="更多信息" min-width="140">
<template #default="scope">
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
<el-link class="ml5"
v-if="tableCreateDialog.enableEditTypes.indexOf(tableCreateDialog.type) > -1"
@click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80">
<template #default="scope">
<el-link @click.prevent="dropTable(scope.row)" type="danger">删除</el-link>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog width="90%" :title="`${sqlExecLogDialog.title} - SQL执行记录`" :before-close="onBeforeCloseSqlExecDialog"
v-model="sqlExecLogDialog.visible">
<div class="toolbar">
<el-select v-model="sqlExecLogDialog.query.db" placeholder="请选择数据库" filterable clearable>
<el-option v-for="item in sqlExecLogDialog.dbs" :key="item" :label="`${item}`" :value="item">
</el-option>
</el-select>
<el-input v-model="sqlExecLogDialog.query.table" placeholder="请输入表名" clearable class="ml5"
style="width: 180px" />
<el-select v-model="sqlExecLogDialog.query.type" placeholder="请选择操作类型" clearable class="ml5">
<el-option v-for="item in enums.DbSqlExecTypeEnum as any" :key="item.value" :label="item.label"
:value="item.value"> </el-option>
</el-select>
<el-button class="ml5" @click="searchSqlExecLog" type="success" icon="search"></el-button>
</div>
<el-table border stripe :data="sqlExecLogDialog.data" size="small">
<el-table-column prop="db" label="数据库" min-width="60" show-overflow-tooltip> </el-table-column>
<el-table-column prop="table" label="" min-width="60" show-overflow-tooltip> </el-table-column>
<el-table-column prop="type" label="类型" width="85" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['UPDATE'].value" color="#E4F5EB"
size="small">UPDATE</el-tag>
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['DELETE'].value" color="#F9E2AE"
size="small">DELETE</el-tag>
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['INSERT'].value" color="#A8DEE0"
size="small">INSERT</el-tag>
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['QUERY'].value" color="#A8DEE0"
size="small">QUERY</el-tag>
</template>
</el-table-column>
<el-table-column prop="sql" label="SQL" min-width="230" show-overflow-tooltip> </el-table-column>
<el-table-column prop="oldValue" label="原值" min-width="150" show-overflow-tooltip> </el-table-column>
<el-table-column prop="creator" label="执行人" min-width="60" show-overflow-tooltip> </el-table-column>
<el-table-column prop="createTime" label="执行时间" show-overflow-tooltip>
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="60" show-overflow-tooltip> </el-table-column>
<el-table-column label="操作" min-width="50" fixed="right">
<template #default="scope">
<el-link
v-if="scope.row.type == enums.DbSqlExecTypeEnum['UPDATE'].value || scope.row.type == enums.DbSqlExecTypeEnum['DELETE'].value"
type="primary" plain size="small" :underline="false" @click="onShowRollbackSql(scope.row)">
还原SQL</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="handleSqlExecPageChange"
:total="sqlExecLogDialog.total" layout="prev, pager, next, total, jumper"
v-model:current-page="sqlExecLogDialog.query.pageNum" :page-size="sqlExecLogDialog.query.pageSize">
</el-pagination>
</el-row>
<el-dialog
width="90%"
:title="`${sqlExecLogDialog.title} - SQL执行记录`"
:before-close="onBeforeCloseSqlExecDialog"
:close-on-click-modal="false"
v-model="sqlExecLogDialog.visible"
>
<db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
</el-dialog>
<el-dialog width="55%" :title="`还原SQL`" v-model="rollbackSqlDialog.visible">
<el-input type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="rollbackSqlDialog.sql"
size="small"> </el-input>
</el-dialog>
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
<el-table border stripe :data="columnDialog.columns" size="small">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table>
</el-dialog>
<el-dialog width="40%" :title="`${chooseTableName} 索引信息`" v-model="indexDialog.visible">
<el-table border stripe :data="indexDialog.indexs" size="small">
<el-table-column prop="indexName" label="索引名" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnName" label="列名" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="seqInIndex" label="列序列号" show-overflow-tooltip> </el-table-column>
<el-table-column prop="indexType" label="类型"> </el-table-column>
<el-table-column prop="indexComment" label="备注" min-width="130" show-overflow-tooltip>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl"
size="small"> </el-input>
</el-dialog>
<el-dialog v-model="infoDialog.visible">
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
<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="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</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-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</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.type }}</el-descriptions-item>
<el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }}
</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-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">{{ infoDialog.instance?.type }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<db-edit @val-change="valChange" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible"
v-model:db="dbEditDialog.data"></db-edit>
<create-table :title="tableCreateDialog.title" :active-name="tableCreateDialog.activeName" :dbId="dbId" :db="db"
:data="tableCreateDialog.data" v-model:visible="tableCreateDialog.visible" @submit-sql="onSubmitSql">
</create-table>
<db-edit @val-change="valChange" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div>
</template>
<script lang='ts' setup>
import { toRefs, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { dbApi } from './api';
import enums from './enums';
import SqlExecBox from './component/SqlExecBox';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { isTrue } from '@/common/assert';
import { Search as SearchIcon } from '@element-plus/icons-vue'
import { tagApi } from '../tag/api';
import { Search as SearchIcon } from '@element-plus/icons-vue';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import DbSqlExecLog from './DbSqlExecLog.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
const CreateTable = defineAsyncComponent(() => import('./CreateTable.vue'));
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
const permissions = {
const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
}
};
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')];
const columns = ref([
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('database', '数据库').isSlot().setMinWidth(70),
TableColumn.new('remark', '备注'),
TableColumn.new('more', '更多').isSlot().setMinWidth(180).fixedRight(),
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(150).fixedRight().alignCenter();
const pageTableRef: any = ref(null);
const state = reactive({
row: {},
row: {} as any,
dbId: 0,
db: '',
tags: [],
chooseId: null as any,
instances: [] as any,
/**
* 选中的数据
*/
chooseData: null,
selectionData: [],
/**
* 查询条件
*/
query: {
tagPath: null,
instanceId: null,
pageNum: 1,
pageSize: 10,
},
@@ -331,6 +231,10 @@ const state = reactive({
infoDialog: {
visible: false,
data: null as any,
instance: null as any,
query: {
instanceId: 0,
},
},
showDumpInfo: false,
dumpInfo: {
@@ -343,409 +247,199 @@ const state = reactive({
sqlExecLogDialog: {
title: '',
visible: false,
data: [],
total: 0,
dbs: [],
query: {
dbId: 0,
db: '',
table: '',
type: null,
pageNum: 1,
pageSize: 12,
},
},
rollbackSqlDialog: {
visible: false,
sql: '',
},
chooseTableName: '',
tableInfoDialog: {
loading: false,
visible: false,
infos: [],
tableNameSearch: '',
tableCommentSearch: '',
},
columnDialog: {
exportDialog: {
visible: false,
columns: [],
},
indexDialog: {
visible: false,
indexs: [],
},
ddlDialog: {
visible: false,
ddl: '',
dbId: 0,
type: 3,
data: [] as any,
value: [],
contents: [] as any,
extName: '',
},
dbEditDialog: {
visible: false,
data: null as any,
title: '新增数据库',
},
tableCreateDialog: {
title: '创建表',
visible: false,
activeName: '1',
type: '',
enableEditTypes: ['mysql'], // 支持"编辑表"的数据库类型
data: { // 修改表时,传递修改数据
edit: false,
row: {},
indexs: [],
columns: [],
},
},
filterDb: {
param: '',
cache: [],
list: [],
}
},
});
const {
dbId,
db,
tags,
chooseId,
query,
datas,
total,
infoDialog,
showDumpInfo,
dumpInfo,
sqlExecLogDialog,
rollbackSqlDialog,
chooseTableName,
tableInfoDialog,
columnDialog,
indexDialog,
ddlDialog,
dbEditDialog,
tableCreateDialog,
filterDb,
} = toRefs(state)
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
search();
});
const filterTableInfos = computed(() => {
const infos = state.tableInfoDialog.infos;
const tableNameSearch = state.tableInfoDialog.tableNameSearch;
const tableCommentSearch = state.tableInfoDialog.tableCommentSearch;
if (!tableNameSearch && !tableCommentSearch) {
return infos;
}
return infos.filter((data: any) => {
let tnMatch = true;
let tcMatch = true;
if (tableNameSearch) {
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
}
if (tableCommentSearch) {
tcMatch = data.tableComment.includes(tableCommentSearch);
}
return tnMatch && tcMatch;
});
});
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const search = async () => {
try {
pageTableRef.value.loading(true);
let res: any = await dbApi.dbs.request(state.query);
// 切割数据库
res.list.forEach((e: any) => {
res.list?.forEach((e: any) => {
e.popoverSelectDbVisible = false;
e.dbs = e.database.split(' ');
});
state.datas = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;
} finally {
pageTableRef.value.loading(false);
}
};
const showInfo = async (info: any) => {
state.infoDialog.data = info;
state.infoDialog.query.instanceId = info.instanceId;
const res = await dbApi.getInstance.request(state.infoDialog.query);
state.infoDialog.instance = res;
state.infoDialog.visible = true;
};
const onBeforeCloseInfoDialog = () => {
state.infoDialog.visible = false;
state.infoDialog.data = null;
state.infoDialog.instance = null;
};
const getTags = async () => {
state.tags = await tagApi.getAccountTags.request(null);
state.tags = await dbApi.dbTags.request(null);
};
const editDb = async (isAdd = false) => {
if (isAdd) {
const getInstances = async (instanceName = '') => {
if (!instanceName) {
state.instances = [];
return;
}
const data = await dbApi.instances.request({ name: instanceName });
if (data) {
state.instances = data.list;
}
};
const editDb = async (data: any) => {
if (!data) {
state.dbEditDialog.data = null;
state.dbEditDialog.title = '新增数据库资源';
} else {
state.dbEditDialog.data = state.chooseData;
state.dbEditDialog.data = data;
state.dbEditDialog.title = '修改数据库资源';
}
state.dbEditDialog.visible = true;
};
const valChange = () => {
state.chooseData = null;
state.chooseId = null;
search();
};
const deleteDb = async (id: number) => {
const deleteDb = async () => {
try {
await ElMessageBox.confirm(`确定删除该库?`, '提示', {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id });
await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) {}
};
const onShowSqlExec = async (row: any) => {
state.sqlExecLogDialog.title = `${row.name}[${row.host}:${row.port}]`;
state.sqlExecLogDialog.query.dbId = row.id;
state.sqlExecLogDialog.title = `${row.name}`;
state.sqlExecLogDialog.dbId = row.id;
state.sqlExecLogDialog.dbs = row.database.split(' ');
searchSqlExecLog();
state.sqlExecLogDialog.visible = true;
};
const onBeforeCloseSqlExecDialog = () => {
state.sqlExecLogDialog.visible = false;
state.sqlExecLogDialog.data = [];
state.sqlExecLogDialog.dbs = [];
state.sqlExecLogDialog.total = 0;
state.sqlExecLogDialog.query.dbId = 0;
state.sqlExecLogDialog.query.pageNum = 1;
state.sqlExecLogDialog.query.table = '';
state.sqlExecLogDialog.query.db = '';
state.sqlExecLogDialog.query.type = null;
state.sqlExecLogDialog.dbId = 0;
};
const searchSqlExecLog = async () => {
const res = await dbApi.getSqlExecs.request(state.sqlExecLogDialog.query);
state.sqlExecLogDialog.data = res.list;
state.sqlExecLogDialog.total = res.total;
};
const handleSqlExecPageChange = (curPage: number) => {
state.sqlExecLogDialog.query.pageNum = curPage;
searchSqlExecLog();
};
/**
* 选择导出数据库表
*/
const handleDumpTableSelectionChange = (vals: any) => {
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
const onDumpDbs = async (row: any) => {
const dbs = row.database.split(' ');
const data = [];
for (let name of dbs) {
data.push({
key: name,
label: name,
});
}
state.exportDialog.value = [];
state.exportDialog.data = data;
state.exportDialog.dbId = row.id;
state.exportDialog.contents = ['结构', '数据'];
state.exportDialog.extName = 'sql';
state.exportDialog.visible = true;
};
/**
* 数据库信息导出
*/
const dump = (db: string) => {
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
const dumpDbs = () => {
isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
const a = document.createElement('a');
let type = 0;
for (let c of state.exportDialog.contents) {
if (c == '结构') {
type += 1;
} else if (c == '数据') {
type += 2;
}
}
a.setAttribute(
'href',
`${config.baseApiUrl}/dbs/${state.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(
','
)}&token=${getSession('token')}`
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
state.exportDialog.extName
}&token=${getSession('token')}`
);
a.click();
state.showDumpInfo = false;
};
const onShowRollbackSql = async (sqlExecLog: any) => {
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
const primaryKey = getPrimaryKey(columns);
const oldValue = JSON.parse(sqlExecLog.oldValue);
const rollbackSqls = [];
if (sqlExecLog.type == enums.DbSqlExecTypeEnum['UPDATE'].value) {
for (let ov of oldValue) {
const setItems = [];
for (let key in ov) {
if (key == primaryKey) {
continue;
}
setItems.push(`${key} = ${wrapValue(ov[key])}`);
}
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
}
} else if (sqlExecLog.type == enums.DbSqlExecTypeEnum['DELETE'].value) {
const columnNames = columns.map((c: any) => c.columnName);
for (let ov of oldValue) {
const values = [];
for (let column of columnNames) {
values.push(wrapValue(ov[column]));
}
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
}
}
state.rollbackSqlDialog.sql = rollbackSqls.join('\n');
state.rollbackSqlDialog.visible = true;
};
const getPrimaryKey = (columns: any) => {
const col = columns.find((c: any) => c.columnKey == 'PRI');
if (col) {
return col.columnName;
}
return columns[0].columnName;
}
/**
* 包装值如果值类型为number则直接返回其他则需要使用''包装
*/
const wrapValue = (val: any) => {
if (typeof val == 'number') {
return val;
}
return `'${val}'`;
state.exportDialog.visible = false;
};
const showTableInfo = async (row: any, db: string) => {
state.tableInfoDialog.loading = true;
state.tableInfoDialog.visible = true;
try {
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: row.id, db });
state.tableCreateDialog.type = row.type
state.dbId = row.id;
state.row = row;
state.db = db;
} catch (e) {
state.tableInfoDialog.visible = false;
} finally {
state.tableInfoDialog.loading = false;
}
state.tableInfoDialog.visible = true;
};
const onSubmitSql = async (row: { tableName: string }) => {
await openEditTable(row)
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: state.dbId, db: state.db });
}
const closeTableInfo = () => {
state.showDumpInfo = false;
state.tableInfoDialog.visible = false;
state.tableInfoDialog.infos = [];
};
const showColumns = async (row: any) => {
state.chooseTableName = row.tableName;
state.columnDialog.columns = await dbApi.columnMetadata.request({
id: state.chooseId,
db: state.db,
tableName: row.tableName,
});
state.columnDialog.visible = true;
};
const showTableIndex = async (row: any) => {
state.chooseTableName = row.tableName;
state.indexDialog.indexs = await dbApi.tableIndex.request({
id: state.chooseId,
db: state.db,
tableName: row.tableName,
});
state.indexDialog.visible = true;
};
const showCreateDdl = async (row: any) => {
state.chooseTableName = row.tableName;
const res = await dbApi.tableDdl.request({
id: state.chooseId,
db: state.db,
tableName: row.tableName,
});
state.ddlDialog.ddl = res;
state.ddlDialog.visible = true;
};
/**
* 删除表
*/
const dropTable = async (row: any) => {
try {
const tableName = row.tableName;
await ElMessageBox.confirm(`确定删除'${tableName}'表?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
SqlExecBox({
sql: `DROP TABLE ${tableName}`,
dbId: state.chooseId,
db: state.db,
runSuccessCallback: async () => {
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: state.chooseId, db: state.db });
},
});
} catch (err) { }
};
// 点击查看时初始化数据
const selectDb = (row: any) => {
state.filterDb.param = ''
state.filterDb.param = '';
state.filterDb.cache = row;
state.filterDb.list = row;
}
};
// 输入字符过滤schema
const filterSchema = () => {
if (state.filterDb.param) {
state.filterDb.list = state.filterDb.cache.filter((a) => { return String(a).toLowerCase().indexOf(state.filterDb.param) > -1 })
state.filterDb.list = state.filterDb.cache.filter((a) => {
return String(a).toLowerCase().indexOf(state.filterDb.param) > -1;
});
} else {
state.filterDb.list = state.filterDb.cache;
}
}
// 打开编辑表
const openEditTable = async (row: any) => {
state.tableCreateDialog.visible = true
state.tableCreateDialog.activeName = '1'
if (row === false) {
state.tableCreateDialog.data = { edit: false, row: {}, indexs: [], columns: [] }
state.tableCreateDialog.title = '创建表'
}
if (row.tableName) {
state.tableCreateDialog.title = '修改表'
let indexs = await dbApi.tableIndex.request({
id: state.chooseId,
db: state.db,
tableName: row.tableName,
});
let columns = await dbApi.columnMetadata.request({
id: state.chooseId,
db: state.db,
tableName: row.tableName,
});
state.tableCreateDialog.data = { edit: true, row, indexs, columns }
}
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -0,0 +1,168 @@
<template>
<div class="db-sql-exec-log">
<page-table
height="100%"
ref="sqlExecDialogPageTableRef"
:query="queryConfig"
v-model:query-form="query"
:data="data"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="searchSqlExecLog()"
>
<template #dbSelect>
<el-select v-model="query.db" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in dbs" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #action="{ data }">
<el-link
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
type="primary"
plain
size="small"
:underline="false"
@click="onShowRollbackSql(data)"
>
还原SQL</el-link
>
</template>
</page-table>
<el-dialog width="55%" :title="`还原SQL`" v-model="rollbackSqlDialog.visible">
<el-input type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="rollbackSqlDialog.sql" size="small"> </el-input>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs,watch, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
import { dbApi } from './api';
import { DbSqlExecTypeEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbs: {
type: [Array<String>],
required: true,
},
});
const queryConfig = [
TableQuery.slot('db', '数据库', 'dbSelect'),
TableQuery.text('table', '表名'),
TableQuery.select('type', '操作类型').setOptions(Object.values(DbSqlExecTypeEnum)),
];
const columns = [
TableColumn.new('db', '数据库'),
TableColumn.new('table', '表'),
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
TableColumn.new('creator', '执行人'),
TableColumn.new('sql', 'SQL').canBeautify(),
TableColumn.new('oldValue', '原值').canBeautify(),
TableColumn.new('createTime', '执行时间').isTime(),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
];
const state = reactive({
data: [],
total: 0,
dbs: [],
query: {
dbId: 0,
db: '',
table: '',
type: null,
pageNum: 1,
pageSize: 10,
},
rollbackSqlDialog: {
visible: false,
sql: '',
},
filterDb: {
param: '',
cache: [],
list: [],
},
});
const { data, query, total, rollbackSqlDialog } = toRefs(state);
onMounted(async () => {
searchSqlExecLog();
});
watch(props, async (newValue: any) => {
await searchSqlExecLog();
});
const searchSqlExecLog = async () => {
state.query.dbId = props.dbId
const res = await dbApi.getSqlExecs.request(state.query);
state.data = res.list;
state.total = res.total;
};
const onShowRollbackSql = async (sqlExecLog: any) => {
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
const primaryKey = getPrimaryKey(columns);
const oldValue = JSON.parse(sqlExecLog.oldValue);
const rollbackSqls = [];
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
for (let ov of oldValue) {
const setItems = [];
for (let key in ov) {
if (key == primaryKey) {
continue;
}
setItems.push(`${key} = ${wrapValue(ov[key])}`);
}
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
}
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
const columnNames = columns.map((c: any) => c.columnName);
for (let ov of oldValue) {
const values = [];
for (let column of columnNames) {
values.push(wrapValue(ov[column]));
}
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
}
}
state.rollbackSqlDialog.sql = rollbackSqls.join('\n');
state.rollbackSqlDialog.visible = true;
};
const getPrimaryKey = (columns: any) => {
const col = columns.find((c: any) => c.columnKey == 'PRI');
if (col) {
return col.columnName;
}
return columns[0].columnName;
};
/**
* 包装值如果值类型为number则直接返回其他则需要使用''包装
*/
const wrapValue = (val: any) => {
if (typeof val == 'number') {
return val;
}
return `'${val}'`;
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,215 @@
<template>
<div>
<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="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<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 v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5"
>原密码
</el-link>
</template>
</el-popover>
</template>
</el-input>
</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>
<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 { dbApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
const props = defineProps({
visible: {
type: Boolean,
},
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
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'],
},
],
};
const dbForm: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
form: {
id: null,
type: null,
name: null,
host: '',
port: 3306,
username: null,
password: null,
params: null,
remark: '',
sshTunnelMachineId: null as any,
},
// 原密码
pwd: '',
// 原用户名
oldUserName: null,
btnLoading: false,
});
const { dialogVisible, tabActiveName, form, pwd, btnLoading } = toRefs(state);
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.data) {
state.form = { ...newValue.data };
state.oldUserName = state.form.username;
} else {
state.form = { port: 3306 } as any;
state.oldUserName = null;
}
});
const getDbPwd = async () => {
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
} else if (state.form.username != state.oldUserName) {
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.saveInstance.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,179 @@
<template>
<div class="db-list">
<page-table
ref="pageTableRef"
:query="queryConfig"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="state.selectionData"
:data="datas"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #queryRight>
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">添加</el-button>
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete"
>删除</el-button
>
</template>
<template #more="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button>
</template>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
</template>
</page-table>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</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.type }}</el-descriptions-item>
<el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </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>
<instance-edit
@val-change="valChange"
:title="instanceEditDialog.title"
v-model:visible="instanceEditDialog.visible"
v-model:data="instanceEditDialog.data"
></instance-edit>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import { dateFormat } from '@/common/utils/date';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
const perms = {
saveInstance: 'db:instance:save',
delInstance: 'db:instance:del',
};
const queryConfig = [TableQuery.text('name', '名称')];
const columns = ref([
TableColumn.new('name', '名称'),
TableColumn.new('host', 'host:port').setFormatFunc((data: any, _prop: string) => `${data.host}:${data.port}`),
TableColumn.new('type', '类型'),
TableColumn.new('username', '用户名'),
TableColumn.new('remark', '备注'),
TableColumn.new('more', '更多').isSlot().setMinWidth(50).fixedRight(),
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.saveInstance]);
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter();
const pageTableRef: any = ref(null);
const state = reactive({
row: {},
dbId: 0,
db: '',
/**
* 选中的数据
*/
selectionData: [],
/**
* 查询条件
*/
query: {
name: null,
pageNum: 1,
pageSize: 10,
},
datas: [],
total: 0,
infoDialog: {
visible: false,
data: null as any,
},
instanceEditDialog: {
visible: false,
data: null as any,
title: '新增数据库实例',
},
});
const { selectionData, query, datas, total, infoDialog, instanceEditDialog } = toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
search();
});
const search = async () => {
try {
pageTableRef.value.loading(true);
let res: any = await dbApi.instances.request(state.query);
state.datas = res.list;
state.total = res.total;
} finally {
pageTableRef.value.loading(false);
}
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;
};
const editInstance = async (data: any) => {
if (!data) {
state.instanceEditDialog.data = null;
state.instanceEditDialog.title = '新增数据库实例';
} else {
state.instanceEditDialog.data = data;
state.instanceEditDialog.title = '修改数据库实例';
}
state.instanceEditDialog.visible = true;
};
const valChange = () => {
search();
};
const deleteInstance = async () => {
try {
await ElMessageBox.confirm(`确定删除数据库实例【${state.selectionData.map((x: any) => x.name).join(', ')}】?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
search();
} catch (err) {}
};
</script>
<style lang="scss"></style>

View File

@@ -1,13 +1,13 @@
<template>
<div>
<el-row>
<el-row class="mb5">
<el-col :span="4">
<el-button type="primary" icon="plus"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)"
size="small">新建查询</el-button>
<el-button type="primary" icon="plus" @click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases.split(' ') }, state.db)" size="small"
>新建查询</el-button
>
</el-col>
<el-col :span="20" v-if="state.db">
<el-descriptions :column="4" size="small" border style="height: 10px">
<el-descriptions :column="4" size="small" border style="height: 10px" class="ml5">
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
<el-descriptions-item label="实例" label-align="right">
@@ -23,30 +23,29 @@
</el-col>
</el-row>
<el-row type="flex">
<el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px">
<tag-tree ref="tagTreeRef" @node-click="nodeClick" :load="loadNode"
:load-contextmenu-items="getContextmenuItems" @current-contextmenu-click="onCurrentContextmenuClick"
:height="state.tagTreeHeight">
<el-col :span="4">
<tag-tree
ref="tagTreeRef"
@node-click="nodeClick"
:load="loadNode"
:load-contextmenu-items="getContextmenuItems"
@current-contextmenu-click="onCurrentContextmenuClick"
:height="state.tagTreeHeight"
>
<template #prefix="{ data }">
<span v-if="data.type == NodeType.DbInst">
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
<template #reference>
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres"
:size="18" />
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" />
<SvgIcon name="InfoFilled" v-else />
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="类型:">{{ data.params.type }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}:{{
data.params.port
}}</el-form-item>
<el-form-item label="用户:">{{ data.params.username }}</el-form-item>
<el-form-item v-if="data.params.remark" label="备注:">{{
data.params.remark
}}</el-form-item>
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item v-if="data.params.remark" label="备注:">{{ data.params.remark }}</el-form-item>
</el-form>
</template>
</el-popover>
@@ -56,28 +55,32 @@
<SvgIcon name="Calendar" v-if="data.type == NodeType.TableMenu" color="#409eff" />
<el-tooltip v-if="data.type == NodeType.Table" effect="customized"
:content="data.params.tableComment" placement="top-end">
<el-tooltip v-if="data.type == NodeType.Table" effect="customized" :content="data.params.tableComment" placement="top-end">
<SvgIcon name="Calendar" color="#409eff" />
</el-tooltip>
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql"
color="#f56c6c" />
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" color="#f56c6c" />
</template>
</tag-tree>
</el-col>
<el-col :span="20">
<el-container id="data-exec" style="border-left: 1px solid #eee; margin-top: 10px">
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%"
v-model="state.activeName">
<el-container id="data-exec" class="mt5 ml5">
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" :name="dt.key">
<table-data
v-if="dt.type === TabType.TableData"
@gen-insert-sql="onGenerateInsertSql"
:data="dt"
:table-height="state.dataTabsTableHeight"
></table-data>
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key"
:name="dt.key">
<table-data v-if="dt.type === TabType.TableData" @gen-insert-sql="onGenerateInsertSql"
:data="dt" :table-height="state.dataTabsTableHeight"></table-data>
<query v-else @save-sql-success="reloadSqls" @delete-sql-success="deleteSqlScript(dt)"
:data="dt" :editor-height="state.editorHeight">
<query
v-else
@save-sql-success="reloadSqls"
@delete-sql-success="deleteSqlScript(dt)"
:data="dt"
:editor-height="state.editorHeight"
>
</query>
</el-tab-pane>
</el-tabs>
@@ -85,8 +88,7 @@
</el-col>
</el-row>
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL"
width="1000px">
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
</el-dialog>
</div>
@@ -96,34 +98,29 @@
import { defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import * as monaco from 'monaco-editor';
import { editor, languages, Position } from 'monaco-editor';
import { DbInst, TabInfo, TabType } from './db'
import { DbInst, TabInfo, TabType } from './db';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
const Query = defineAsyncComponent(() => import('./component/tab/Query.vue'));
const TableData = defineAsyncComponent(() => import('./component/tab/TableData.vue'));
/**
* 树节点类型
*/
class NodeType {
static DbInst = 1
static Db = 2
static DbInst = 1;
static Db = 2;
static TableMenu = 3;
static SqlMenu = 4;
static Table = 5;
static Sql = 6;
}
class ContextmenuClickId {
static ReloadTable = 0
static ReloadTable = 0;
}
const tagTreeRef: any = ref(null)
const tagTreeRef: any = ref(null);
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
@@ -144,12 +141,10 @@ const state = reactive({
},
});
const {
nowDbInst,
} = toRefs(state);
const { nowDbInst } = toRefs(state);
onMounted(() => {
self.completionItemProvider?.dispose()
self.completionItemProvider?.dispose();
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
window.onresize = () => setHeight();
@@ -160,7 +155,7 @@ onMounted(() => {
*/
const setHeight = () => {
state.editorHeight = window.innerHeight - 518 + 'px';
state.dataTabsTableHeight = window.innerHeight - 219 - 36 + 'px';
state.dataTabsTableHeight = window.innerHeight - 256 + 'px';
state.tagTreeHeight = window.innerHeight - 165 + 'px';
};
@@ -170,15 +165,15 @@ const setHeight = () => {
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000, })
if (!res.total) return
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return;
for (const db of res.list) {
const tagPath = db.tagPath;
let dbInsts = instMap.get(tagPath) || [];
dbInsts.push(db);
instMap.set(tagPath, dbInsts?.sort());
}
}
};
/**
* 加载树节点
@@ -203,7 +198,7 @@ const loadNode = async (node: any) => {
// 点击tagPath -> 加载数据库实例信息列表
if (nodeType === TagTreeNode.TagPath) {
const dbInfos = instMap.get(data.key)
const dbInfos = instMap.get(data.key);
return dbInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x);
});
@@ -212,6 +207,7 @@ const loadNode = async (node: any) => {
// 点击数据库实例 -> 加载库列表
if (nodeType === NodeType.DbInst) {
const dbs = params.database.split(' ')?.sort();
console.log(dbs);
return dbs.map((x: any) => {
return new TagTreeNode(`${data.key}.${x}`, x, NodeType.Db).withParams({
tagPath: params.tagPath,
@@ -219,15 +215,17 @@ const loadNode = async (node: any) => {
name: params.name,
type: params.type,
dbs: dbs,
db: x
})
})
db: x,
});
});
}
// 点击数据库 -> 加载 表&Sql 菜单
if (nodeType === NodeType.Db) {
return [new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeType.TableMenu).withParams(params),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params)];
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '', NodeType.TableMenu).withParams(params),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params),
];
}
// 点击表菜单 -> 加载表列表
@@ -262,30 +260,28 @@ const nodeClick = async (data: any) => {
if (dataType === NodeType.Sql) {
await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName);
}
}
};
const getContextmenuItems = (data: any) => {
const dataType = data.type;
if (dataType === NodeType.TableMenu) {
return [
{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }
]
return [{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }];
}
return [];
}
};
// 当前右击菜单点击事件
const onCurrentContextmenuClick = (clickData: any) => {
const clickId = clickData.id;
if (clickId == ContextmenuClickId.ReloadTable) {
reloadTables(clickData.item.key)
}
reloadTables(clickData.item.key);
}
};
const getTables = async (params: any) => {
const { id, db } = params;
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false
state.reloadStatus = false;
return tables.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
id,
@@ -293,8 +289,8 @@ const getTables = async (params: any) => {
tableName: x.tableName,
tableComment: x.tableComment,
});
})
}
});
};
/**
* 加载用户保存的sql脚本
@@ -303,7 +299,7 @@ const getTables = async (params: any) => {
* @param schema
*/
const loadSqls = async (id: any, db: string, dbs: any) => {
const sqls = await dbApi.getSqlNames.request({ id: id, db: db, })
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({
id,
@@ -312,17 +308,17 @@ const loadSqls = async (id: any, db: string, dbs: any) => {
sqlName: x.name,
});
});
}
};
// 选择数据库
const changeSchema = (inst: any, schema: string) => {
state.nowDbInst = DbInst.getOrNewInst(inst);
state.db = schema;
}
};
// 加载选中的表数据即新增表数据操作tab
const loadTableData = async (inst: any, schema: string, tableName: string) => {
changeSchema(inst, schema)
changeSchema(inst, schema);
if (tableName == '') {
return;
}
@@ -341,16 +337,16 @@ const loadTableData = async (inst: any, schema: string, tableName: string) => {
tab.db = schema;
tab.type = TabType.TableData;
tab.params = {
table: tableName
}
state.tabs.set(label, tab)
}
table: tableName,
};
state.tabs.set(label, tab);
};
// 新建查询panel
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
if (!db || !inst.id) {
ElMessage.warning('请选择数据库实例及对应的schema')
return
ElMessage.warning('请选择数据库实例及对应的schema');
return;
}
const dbId = inst.id;
@@ -364,7 +360,7 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
if (v.type == TabType.Query && !v.params.sqlName) {
count++;
}
})
});
label = `新查询${count}:${dbId}:${db}`;
}
state.activeName = label;
@@ -381,16 +377,15 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
tab.params = {
sqlName: sqlName,
dbs: inst.dbs,
}
state.tabs.set(label, tab)
registerSqlCompletionItemProvider();
}
};
state.tabs.set(label, tab);
};
const onRemoveTab = (targetName: string) => {
let activeName = state.activeName;
const tabNames = [...state.tabs.keys()]
const tabNames = [...state.tabs.keys()];
for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i]
const tabName = tabNames[i];
if (tabName !== targetName) {
continue;
}
@@ -414,7 +409,7 @@ const onTabChange = () => {
const nowTab = state.tabs.get(state.activeName);
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
state.db = nowTab?.db as string;
}
};
const onGenerateInsertSql = async (sql: string) => {
state.genSqlDialog.sql = sql;
@@ -423,293 +418,21 @@ const onGenerateInsertSql = async (sql: string) => {
const reloadSqls = (dbId: number, db: string) => {
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
}
};
const deleteSqlScript = (ti: TabInfo) => {
reloadSqls(ti.dbId, ti.db);
onRemoveTab(ti.key);
}
};
const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu`
}
return `${dbId}.${db}.sql-menu`;
};
const reloadTables = (nodeKey: string) => {
state.reloadStatus = true
state.reloadStatus = true;
tagTreeRef.value.reloadNode(nodeKey);
}
const registerSqlCompletionItemProvider = () => {
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
self.completionItemProvider = self.completionItemProvider || monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const nowTab = state.tabs.get(state.activeName);
if (!nowTab) {
return;
}
const { db, dbId } = nowTab;
const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position
const { startColumn, endColumn } = word
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return { suggestions: [] }
}
let range = {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
};
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
})
const textBeforePointerMulti = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
})
// 光标后文本
const textAfterPointerMulti = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount())
})
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/)
const lastToken = tokens[tokens.length - 1].toLowerCase()
const secondToken = tokens.length > 2 && tokens[tokens.length - 2].toLowerCase() || ''
const dbs = nowTab.params?.dbs?.split(' ') || [];
// console.log("光标前文本:=>" + textBeforePointerMulti)
// console.log("最后输入的:=>" + lastToken)
let suggestions: languages.CompletionItem[] = []
const tables = await dbInst.loadTables(db);
async function hintTableColumns(tableName: any, db: any) {
let dbHits = await dbInst.loadDbHints(db)
let columns = dbHits[tableName]
let suggestions: languages.CompletionItem[] = []
columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(" ")
const fieldName = nameAndComment[0]
suggestions.push({
label: {
label: a,
description: 'column'
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: fieldName + ' ', // create_time
range,
sortText: 100 + index + '' // 使用表字段声明顺序排序,排序需为字符串类型
});
})
return suggestions
}
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
let str = lastToken.substring(0, lastToken.lastIndexOf('.'))
if (lastToken.trim().startsWith('.')) {
str = secondToken
}
// 库.表名联想
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
let tables = await dbInst.loadTables(str)
let suggestions: languages.CompletionItem[] = []
for (let item of tables) {
const { tableName, tableComment } = item
suggestions.push({
label: {
label: tableName + (tableComment ? ' - ' + tableComment : ''),
description: 'table'
},
kind: monaco.languages.CompletionItemKind.File,
insertText: tableName,
range
});
}
return { suggestions }
}
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
// 表别名.表字段联想
let tableInfo = getTableByAlias(sql, db, str)
if (tableInfo.tableName) {
let tableName = tableInfo.tableName
let db = tableInfo.dbName;
// 取出表名并提示
let suggestions = await hintTableColumns(tableName, db);
if (suggestions.length > 0) {
return { suggestions };
}
}
return { suggestions: [] }
} else {
// 如果sql里含有表名则提示表字段
let mat = textBeforePointerMulti.match(/from\n*\s+\n*(\w+)\n*\s+\n*/i)
if (mat && mat.length > 1) {
let tableName = mat[1]
// 取出表名并提示
let addSuggestions = await hintTableColumns(tableName, db);
if (addSuggestions.length > 0) {
suggestions = suggestions.concat(addSuggestions)
}
}
}
// 表名联想
tables.forEach((tableMeta: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table'
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: tableName + ' ',
range
});
});
// mysql关键字
sqlLanguage.keywords.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'keyword'
},
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
range
});
})
// 操作符
sqlLanguage.operators.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'opt'
},
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item,
range
});
})
// 内置函数
sqlLanguage.builtinFunctions.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'func'
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
range
});
})
// 内置变量
sqlLanguage.builtinVariables.forEach((item: string) => {
suggestions.push({
label: {
label: item,
description: 'var'
},
kind: monaco.languages.CompletionItemKind.Variable,
insertText: item,
range
});
})
// 库名提示
if (dbs && dbs.length > 0) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema'
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a,
range
});
})
}
// 默认提示
return {
suggestions: suggestions
};
},
});
}
/**
* 根据别名获取sql里的表名
* @param sql sql
* @param db 默认数据库
* @param alias 别名
*/
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string, tableName: string } => {
// 表别名:表名
let result = {};
let defName = '';
let defResult = {};
// 正则匹配取出表名和表别名
// 测试sql
/*
`select * from database.Outvisit l
left join patient p on l.patid=p.patientid
join patstatic c on l.patid=c.patid inner join patphone ph on l.patid=ph.patid
where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisitid=l.outvisitid)
unit all
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
*/
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi)
if (match && match.length > 0) {
match.forEach(a => {
// 去掉前缀,取出
let t = a.substring(5, a.length)
.replaceAll(/\s+/g, ' ')
.replaceAll(/\s+as\s+/gi, ' ')
.replaceAll(/\r\n/g, ' ').trim()
.split(/\s+/);
let withDb = t[0].split('.');
// 表名是 db名.表名
let tName = withDb.length > 1 ? withDb[1] : withDb[0]
let dbName = withDb.length > 1 ? withDb[0] : (db || '')
if (t.length == 2) {
// 表别名:表名
result[t[1]] = { tableName: tName, dbName }
} else {
// 只有表名无别名 取第一个无别名的表为默认表
!defName && (defResult = { tableName: tName, dbName: db })
}
})
}
return result[alias] || defResult
}
</script>
<style lang="scss">
@@ -735,11 +458,6 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(
text-align: center;
}
.el-tabs__header {
padding: 0 10px;
background-color: #fff;
}
#data-exec {
min-height: calc(100vh - 155px);
@@ -753,7 +471,7 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(
}
.update_field_active {
background-color: var(--el-color-success)
background-color: var(--el-color-success);
}
.instances-pop-form {

View File

@@ -1,71 +0,0 @@
<template>
<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>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { watch, toRefs, reactive } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
},
title: {
type: String,
},
data: {
type: Object,
},
})
//定义事件
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

@@ -2,27 +2,34 @@ import Api from '@/common/Api';
export const dbApi = {
// 获取权限列表
dbs: Api.newGet("/dbs"),
saveDb: Api.newPost("/dbs"),
getAllDatabase: Api.newPost("/dbs/databases"),
getDbPwd: Api.newGet("/dbs/{id}/pwd"),
deleteDb: Api.newDelete("/dbs/{id}"),
dumpDb: Api.newPost("/dbs/{id}/dump"),
tableInfos: Api.newGet("/dbs/{id}/t-infos"),
tableIndex: Api.newGet("/dbs/{id}/t-index"),
tableDdl: Api.newGet("/dbs/{id}/t-create-ddl"),
tableMetadata: Api.newGet("/dbs/{id}/t-metadata"),
columnMetadata: Api.newGet("/dbs/{id}/c-metadata"),
dbs: Api.newGet('/dbs'),
dbTags: Api.newGet('/dbs/tags'),
saveDb: Api.newPost('/dbs'),
deleteDb: Api.newDelete('/dbs/{id}'),
dumpDb: Api.newPost('/dbs/{id}/dump'),
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
tableIndex: Api.newGet('/dbs/{id}/t-index'),
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
tableMetadata: Api.newGet('/dbs/{id}/t-metadata'),
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
// 获取表即列提示
hintTables: Api.newGet("/dbs/{id}/hint-tables"),
sqlExec: Api.newPost("/dbs/{id}/exec-sql"),
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
sqlExec: Api.newPost('/dbs/{id}/exec-sql'),
// 保存sql
saveSql: Api.newPost("/dbs/{id}/sql"),
saveSql: Api.newPost('/dbs/{id}/sql'),
// 获取保存的sql
getSql: Api.newGet("/dbs/{id}/sql"),
getSql: Api.newGet('/dbs/{id}/sql'),
// 获取保存的sql names
getSqlNames: Api.newGet("/dbs/{id}/sql-names"),
deleteDbSql: Api.newDelete("/dbs/{id}/sql"),
getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
// 获取数据库sql执行记录
getSqlExecs: Api.newGet("/dbs/{dbId}/sql-execs"),
}
getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'),
// 获取权限列表
instances: Api.newGet('/instances'),
getInstance: Api.newGet("/instances/{instanceId}"),
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
saveInstance: Api.newPost('/instances'),
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
deleteInstance: Api.newDelete('/instances/{id}'),
};

View File

@@ -1,20 +1,42 @@
<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
@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"
highlight-current-row
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 v-for="(item, index) in columns">
<el-table-column
min-width="100"
:width="DbInst.flexColumnWidth(item.columnName, datas)"
align="center"
v-if="item.show"
:key="index"
:prop="item.columnName"
:label="item.columnName"
show-overflow-tooltip
:sortable="sortable"
>
<template #header v-if="showColumnTip">
<el-tooltip raw-content placement="top" effect="customized">
<template #content> {{ getColumnTip(item) }} </template>
{{ item }}
{{ item.columnName }}
</el-tooltip>
</template>
</el-table-column>
</template>
</el-table>
</div>
</template>
@@ -45,8 +67,8 @@ const props = defineProps({
data: {
type: Array,
},
columnNames: {
type: Array,
columns: {
type: Array<any>,
},
sortable: {
type: [String, Boolean],
@@ -76,7 +98,6 @@ const state = reactive({
db: '', // 数据库名
table: '', // 当前的表名
datas: [],
columnNames: [],
columns: [],
sortable: false,
loading: false,
@@ -92,7 +113,6 @@ const {
datas,
sortable,
loading,
columnNames,
showColumnTip,
} = toRefs(state);
@@ -114,24 +134,16 @@ const setState = (props: any) => {
state.tableHeight = props.height;
state.sortable = props.sortable;
state.loading = props.loading;
state.columnNames = props.columnNames;
state.columns = props.columns;
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 getColumnTip = (column: any) => {
const comment = column.columnComment;
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
};
/**
* 表排序字段变更
*/
@@ -178,7 +190,7 @@ const cellClick = (row: any, column: any, cell: any) => {
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
const newField = {
div, row,
fieldName: column.rawColumnKey,
fieldName: property,
fieldType: updateColumn.columnType,
oldValue: text,
newValue: input.value
@@ -209,10 +221,10 @@ const cellClick = (row: any, column: any, cell: any) => {
let fields = primaryKeyFields[0].fields
const fieldsParam = fields.filter((a) => {
if (a.fieldName === column.rawColumnKey) {
if (a.fieldName === column.property) {
a.newValue = input.value
}
return a.fieldName === column.rawColumnKey
return a.fieldName === column.property
})
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
@@ -222,7 +234,7 @@ const cellClick = (row: any, column: any, cell: any) => {
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 = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.property) : [];
a.fields.length <= 0 && delIndex.push(i)
}
});
@@ -254,6 +266,7 @@ const cellClick = (row: any, column: any, cell: any) => {
};
const submitUpdateFields = () => {
const dbInst = DbInst.getInst(state.dbId)
let currentUpdatedFields = state.updatedFields;
if (currentUpdatedFields.length <= 0) {
return;
@@ -262,12 +275,12 @@ const submitUpdateFields = () => {
let res = '';
let divs: HTMLElement[] = [];
currentUpdatedFields.forEach(a => {
let sql = `UPDATE ${state.table} SET `;
let sql = `UPDATE ${dbInst.wrapName(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)},`
sql += ` ${dbInst.wrapName(f.fieldName)} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
// 如果修改的字段是主键
if (f.fieldName === primaryKeyName) {
primaryKey = f.oldValue
@@ -275,11 +288,11 @@ const submitUpdateFields = () => {
divs.push(f.div)
})
sql = sql.substring(0, sql.length - 1)
sql += ` WHERE ${primaryKeyName} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
res += sql;
})
DbInst.getInst(state.dbId).promptExeSql(db, res, () => { }, () => {
dbInst.promptExeSql(db, res, () => { }, () => {
currentUpdatedFields = [];
divs.forEach(a => {
a.classList.remove('update_field_active');
@@ -305,10 +318,6 @@ const changeUpdatedField = () => {
emits('changeUpdatedField', state.updatedFields);
}
const getNowDb = () => {
return DbInst.getInst(state.dbId).getDb(state.db);
}
const getNowDbInst = () => {
return DbInst.getInst(state.dbId);
}
@@ -317,11 +326,10 @@ defineExpose({
submitUpdateFields,
cancelUpdateFields
})
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
background-color: var(--el-color-success);
}
</style>

View File

@@ -1,44 +1,44 @@
import { h, render, VNode } from 'vue'
import SqlExecDialog from './SqlExecDialog.vue'
import { h, render, VNode } from 'vue';
import SqlExecDialog from './SqlExecDialog.vue';
export type SqlExecProps = {
sql: string
dbId: number,
db: string,
runSuccessCallback?: Function,
cancelCallback?: Function
}
sql: string;
dbId: number;
db: string;
runSuccessCallback?: Function;
cancelCallback?: Function;
};
const boxId = 'sql-exec-id'
const boxId = 'sql-exec-id';
const renderBox = (): VNode => {
const props: SqlExecProps = {
sql: '',
dbId: 0,
} as any
const container = document.createElement('div')
container.id = boxId
} as any;
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
const boxVNode = h(SqlExecDialog, props)
const boxVNode = h(SqlExecDialog, props);
// 将虚拟dom渲染到 container dom 上
render(boxVNode, container)
render(boxVNode, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container)
document.body.appendChild(container);
return boxVNode
}
return boxVNode;
};
let boxInstance: any
let boxInstance: any;
const SqlExecBox = (props: SqlExecProps): void => {
if (boxInstance) {
const boxVue = boxInstance.component
const boxVue = boxInstance.component;
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed.open(props);
} else {
boxInstance = renderBox()
SqlExecBox(props)
}
boxInstance = renderBox();
SqlExecBox(props);
}
};
export default SqlExecBox;

View File

@@ -36,7 +36,7 @@ const props = defineProps({
sql: {
type: String,
},
})
});
const remarkInputRef = ref<InputInstance>();
const state = reactive({
@@ -48,12 +48,7 @@ const state = reactive({
btnLoading: false,
});
const {
dialogVisible,
sqlValue,
remark,
btnLoading
} = toRefs(state)
const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
state.sqlValue = props.sql as any;
let runSuccessCallback: any;
@@ -81,7 +76,7 @@ const runSql = async () => {
for (let re of res.res) {
if (re.result !== 'success') {
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
throw new Error(re.result)
throw new Error(re.result);
}
}
@@ -129,7 +124,7 @@ const open = (props: SqlExecProps) => {
});
};
defineExpose({ open })
defineExpose({ open });
</script>
<style lang="scss">
.codesql {

View File

@@ -3,43 +3,49 @@
<div>
<div class="toolbar">
<div class="fl">
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay">
</el-link>
<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-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-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-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>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="limit" placement="top">
<el-link @click="onLimit()" type="success" :underline="false" icon="Operation"> </el-link>
</el-tooltip>
</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>
<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 :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }"></div>
</div>
<div class="editor-move-resize" @mousedown="onDragSetHeight">
@@ -50,31 +56,35 @@
<div class="mt5">
<el-row>
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete"
:underline="false"></el-link>
<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>
<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>
<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>
<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="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改" @selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"></db-table>
<db-table
ref="dbTableRef"
:db-id="state.ti.dbId"
:db="state.ti.db"
:data="execRes.data"
:table="state.table"
:columns="execRes.tableColumn"
:loading="loading"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
></db-table>
</div>
</div>
</template>
@@ -87,25 +97,44 @@ 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 { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import { language as addSqlLanguage } from '../../lang/mysql.js';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import * as monaco from 'monaco-editor';
import { editor } from 'monaco-editor';
// import * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor, languages, Position } from 'monaco-editor';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
// 右键菜单
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js';
import 'monaco-editor/esm/vs/editor/contrib/clipboard//browser/clipboard.js';
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
// 主题仓库 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 DbTable from '../DbTable.vue';
import { DbInst, 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 sqlCompletionKeywords = [...sqlLanguage.keywords, ...addSqlLanguage.keywords];
const sqlCompletionOperators = [...sqlLanguage.operators, ...addSqlLanguage.operators];
const sqlCompletionBuiltinFunctions = [...sqlLanguage.builtinFunctions, ...addSqlLanguage.builtinFunctions];
const sqlCompletionBuiltinVariables = [...sqlLanguage.builtinVariables, ...addSqlLanguage.builtinVariables];
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
const props = defineProps({
data: {
type: TabInfo,
required: true
required: true,
},
// sql脚本名若有则去加载该sql内容
sqlName: {
@@ -114,9 +143,9 @@ const props = defineProps({
},
editorHeight: {
type: String,
default: '600'
}
})
default: '600',
},
});
const { themeConfig } = storeToRefs(useThemeConfig());
const token = getSession('token');
@@ -134,7 +163,7 @@ const state = reactive({
loading: false, // 是否在加载数据
execRes: {
data: [],
tableColumn: []
tableColumn: [],
},
selectionDatas: [] as any,
editorHeight: '500',
@@ -142,20 +171,23 @@ const state = reactive({
hasUpdatedFileds: false,
});
const {
tableDataHeight,
editorHeight,
ti,
execRes,
table,
sqlName,
loading,
hasUpdatedFileds,
} = toRefs(state);
const { tableDataHeight, editorHeight, ti, execRes, table, sqlName, loading, hasUpdatedFileds } = toRefs(state);
watch(() => props.editorHeight, (newValue: any) => {
watch(
() => props.editorHeight,
(newValue: any) => {
state.editorHeight = newValue;
});
}
);
// 监听 themeConfig editorTheme配置文件的变化
watch(
() => themeConfig.value.editorTheme,
(val) => {
console.log('monaco editor theme change: ', val);
monaco?.editor?.setTheme(val);
}
);
onMounted(async () => {
console.log('in query mounted');
@@ -170,19 +202,21 @@ onMounted(async () => {
state.sql = res.sql;
}
nextTick(() => {
setTimeout(() => initMonacoEditor(), 50)
})
setTimeout(() => initMonacoEditor(), 50);
});
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
})
});
self.MonacoEnvironment = {
getWorker() {
return new EditorWorker();
}
},
};
const initMonacoEditor = () => {
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement
registerSqlCompletionItemProvider();
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);
@@ -219,7 +253,7 @@ const initMonacoEditor = () => {
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0)
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
@@ -229,9 +263,9 @@ const initMonacoEditor = () => {
try {
await onRunSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
e.message && ElMessage.error(e.message);
}
},
});
// 注册快捷键ctrl + shift + f 格式化sql
@@ -246,7 +280,7 @@ const initMonacoEditor = () => {
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0)
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 2,
@@ -256,14 +290,11 @@ const initMonacoEditor = () => {
try {
await formatSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
e.message && ElMessage.error(e.message);
}
},
});
// 动态设置主题
// monaco.editor.setTheme('hc-black');
// 如果sql有值则默认赋值
if (state.sql) {
monacoEditor.getModel()?.setValue(state.sql);
@@ -277,13 +308,13 @@ const onDragSetHeight = () => {
document.onmousemove = (e) => {
e.preventDefault();
//得到鼠标拖动的宽高距离:取绝对值
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px`
state.tableDataHeight -= e.movementY
}
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px`;
state.tableDataHeight -= e.movementY;
};
document.onmouseup = () => {
document.onmousemove = null;
}
}
};
};
/**
* 执行sql
@@ -324,11 +355,17 @@ const onRunSql = async () => {
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集')
ElMessage.warning('未查询到结果集');
}
state.execRes.data = colAndData.res;
state.execRes.tableColumn = colAndData.colNames;
cancelUpdateFields()
// 兼容表格字段配置
state.execRes.tableColumn = colAndData.colNames.map((x: any) => {
return {
columnName: x,
show: true,
};
});
cancelUpdateFields();
} catch (e: any) {
state.execRes.data = [];
state.execRes.tableColumn = [];
@@ -364,15 +401,15 @@ const getSql = () => {
return res;
}
// 选择选中的sql
let selection = monacoEditor.getSelection()
let selection = monacoEditor.getSelection();
if (selection) {
res = monacoEditor.getModel()?.getValueInRange(selection)
res = monacoEditor.getModel()?.getValueInRange(selection);
}
// 整个编辑器的sql
if (!res) {
return monacoEditor.getModel()?.getValue()
return monacoEditor.getModel()?.getValue();
}
return res
return res;
};
const saveSql = async () => {
@@ -386,8 +423,7 @@ const saveSql = async () => {
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern:
/\w+/,
inputPattern: /\w+/,
inputErrorMessage: '请输入SQL脚本名',
});
sqlName = input.value;
@@ -405,7 +441,7 @@ const saveSql = async () => {
const deleteSql = async () => {
const sqlName = state.sqlName;
notBlank(sqlName, "该sql内容未保存");
notBlank(sqlName, '该sql内容未保存');
const { dbId, db } = state.ti;
try {
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
@@ -423,14 +459,14 @@ const deleteSql = async () => {
* 格式化sql
*/
const formatSql = () => {
let selection = monacoEditor.getSelection()
let selection = monacoEditor.getSelection();
if (!selection) {
return;
}
let sql = monacoEditor.getModel()?.getValueInRange(selection)
let sql = monacoEditor.getModel()?.getValueInRange(selection);
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
if (sql) {
replaceSelection(sqlFormatter(sql), selection)
replaceSelection(sqlFormatter(sql), selection);
return;
}
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
@@ -456,29 +492,40 @@ const replaceSelection = (str: string, selection: any) => {
model.setValue(str);
return;
}
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
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.setValue(textBeforeSelection + str + textAfterSelection);
monacoEditor.focus();
monacoEditor.setPosition({
lineNumber: startLineNumber,
column: 0,
})
}
});
};
const onLimit = () => {
let position = monacoEditor.getPosition() as monaco.Position;
let newText = ' limit 10';
monacoEditor?.getModel()?.applyEdits([
{
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
text: newText,
},
]);
};
/**
* 导出当前页数据
@@ -486,7 +533,11 @@ const replaceSelection = (str: string, selection: any) => {
const exportData = () => {
const dataList = state.execRes.data as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.execRes.tableColumn, dataList)
exportCsv(
`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`,
state.execRes.tableColumn.map((x: any) => x.columnName),
dataList
);
};
const beforeUpload = (file: File) => {
@@ -505,7 +556,6 @@ const getUploadSqlFileUrl = () => {
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
@@ -513,7 +563,7 @@ const onDataSelectionChange = (datas: []) => {
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
};
/**
* 执行删除数据事件
@@ -522,7 +572,7 @@ const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
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, () => {
@@ -535,12 +585,313 @@ const onDeleteData = async () => {
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
};
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
};
const registerSqlCompletionItemProvider = () => {
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
self.completionItemProvider =
self.completionItemProvider ||
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const nowTab = props.data;
if (!nowTab) {
return;
}
const { db, dbId } = nowTab;
const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return { suggestions: [] };
}
let range = {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
};
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
const textBeforePointerMulti = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// 光标后文本
const textAfterPointerMulti = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
});
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/);
let lastToken = tokens[tokens.length - 1].toLowerCase();
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
const dbs = (nowTab.params && nowTab.params.dbs && nowTab.params.dbs) || [];
// console.log("光标前文本:=>" + textBeforePointerMulti)
// console.log("最后输入的:=>" + lastToken)
let suggestions: languages.CompletionItem[] = [];
const tables = await dbInst.loadTables(db);
async function hintTableColumns(tableName: any, db: any) {
let dbHits = await dbInst.loadDbHints(db);
let columns = dbHits[tableName];
let suggestions: languages.CompletionItem[] = [];
columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(' ');
const fieldName = nameAndComment[0];
suggestions.push({
label: {
label: a,
description: 'column',
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: fieldName, // create_time
range,
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
});
});
return suggestions;
}
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
let str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
str = secondToken;
}
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
let aliasArr = lastToken.split(',');
if (aliasArr.length > 1) {
lastToken = aliasArr[aliasArr.length - 1];
str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
str = secondToken;
}
}
// 库.表名联想
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
let tables = await dbInst.loadTables(str);
let suggestions: languages.CompletionItem[] = [];
for (let item of tables) {
const { tableName, tableComment } = item;
suggestions.push({
label: {
label: tableName + (tableComment ? ' - ' + tableComment : ''),
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
insertText: tableName,
range,
});
}
return { suggestions };
}
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
// 表别名.表字段联想
let tableInfo = getTableByAlias(sql, db, str);
if (tableInfo.tableName) {
let tableName = tableInfo.tableName;
let db = tableInfo.dbName;
// 取出表名并提示
let suggestions = await hintTableColumns(tableName, db);
if (suggestions.length > 0) {
return { suggestions };
}
}
return { suggestions: [] };
} else {
// 如果sql里含有表名则提示表字段
let mat = textBeforePointerMulti.match(/[from|update]\n*\s+\n*(\w+)\n*\s+\n*/i);
if (mat && mat.length > 1) {
let tableName = mat[1];
// 取出表名并提示
let addSuggestions = await hintTableColumns(tableName, db);
if (addSuggestions.length > 0) {
suggestions = suggestions.concat(addSuggestions);
}
}
}
// 表名联想
tables.forEach((tableMeta: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: tableName + ' ',
range,
});
});
// mysql关键字
sqlCompletionKeywords.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'keyword',
},
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
range,
});
});
// 操作符
sqlCompletionOperators.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'opt',
},
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item,
range,
});
});
let replacedFunctions = [] as string[];
// 添加的函数
addSqlLanguage.replaceFunctions.forEach((item: any) => {
replacedFunctions.push(item.label);
suggestions.push({
label: {
label: item.label,
description: item.description,
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item.insertText,
range,
});
});
// 内置函数
sqlCompletionBuiltinFunctions.forEach((item: any) => {
replacedFunctions.indexOf(item) < 0 &&
suggestions.push({
label: {
label: item,
description: 'func',
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
range,
});
});
// 内置变量
sqlCompletionBuiltinVariables.forEach((item: string) => {
suggestions.push({
label: {
label: item,
description: 'var',
},
kind: monaco.languages.CompletionItemKind.Variable,
insertText: item,
range,
});
});
// 库名提示
if (dbs && dbs.length > 0) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema',
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a,
range,
});
});
}
// 默认提示
return {
suggestions: suggestions,
};
},
});
};
/**
* 根据别名获取sql里的表名
* @param sql sql
* @param db 默认数据库
* @param alias 别名
*/
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string; tableName: string } => {
// 表别名:表名
let result = {};
let defName = '';
let defResult = {};
// 正则匹配取出表名和表别名
// 测试sql
/*
`select * from database.Outvisit l
left join patient p on l.patid=p.patientid
join patstatic c on l.patid=c.patid inner join patphone ph on l.patid=ph.patid
where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisitid=l.outvisitid)
unit all
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
*/
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi);
if (match && match.length > 0) {
match.forEach((a) => {
// 去掉前缀,取出
let t = a
.substring(5, a.length)
.replaceAll(/\s+/g, ' ')
.replaceAll(/\s+as\s+/gi, ' ')
.replaceAll(/\r\n/g, ' ')
.trim()
.split(/\s+/);
let withDb = t[0].split('.');
// 表名是 db名.表名
let tName = withDb.length > 1 ? withDb[1] : withDb[0];
let dbName = withDb.length > 1 ? withDb[0] : db || '';
if (t.length == 2) {
// 表别名:表名
result[t[1]] = { tableName: tName, dbName };
} else {
// 只有表名无别名 取第一个无别名的表为默认表
!defName && (defResult = { tableName: tName, dbName: db });
}
});
}
return result[alias] || defResult;
};
</script>
<style lang="scss">
@@ -557,12 +908,11 @@ const cancelUpdateFields = () => {
.sqlEditor {
font-size: 8pt;
font-weight: 600;
border: 1px solid #ccc;
border: 1px solid var(--el-border-color-light, #ebeef5);
}
.update_field_active {
background-color: var(--el-color-success)
background-color: var(--el-color-success);
}
.editor-move-resize {

View File

@@ -2,8 +2,29 @@
<div>
<el-row>
<el-col :span="8">
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5">
</el-link>
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5"> </el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="表格字段配置"
trigger="click"
>
<div v-for="(item, index) in columns" :key="index">
<el-checkbox
v-model="item.show"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
:true-label="true"
:false-label="false"
size="small"
/>
</div>
<template #reference>
<el-link icon="Operation" size="small" :underline="false"></el-link>
</template>
</el-popover>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" :underline="false"></el-link>
@@ -13,8 +34,7 @@
<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-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
@@ -37,22 +57,31 @@
</el-tooltip>
</el-col>
<el-col :span="16">
<el-input v-model="condition" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可" clearable
@clear="selectData" size="small" style="width: 100%">
<el-input
v-model="condition"
placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可"
clearable
@clear="selectData"
size="small"
style="width: 100%"
>
<template #prepend>
<el-popover trigger="click" :width="320" placement="right">
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right">
<template #reference>
<el-link type="success" :underline="false">选择列</el-link>
<el-link @click.stop="state.condPopVisible = !state.condPopVisible" type="success" :underline="false">选择列</el-link>
</template>
<el-table :data="columns" max-height="500" size="small" @row-click="
(...event: any) => {
<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>
"
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>
@@ -64,16 +93,36 @@
</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>
<db-table
ref="dbTableRef"
:db-id="state.ti.dbId"
:db="state.ti.db"
:data="datas"
:table="state.table"
:columns="columns"
: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-pagination
small
:total="count"
@size-change="handleSizeChange"
@current-change="pageChange()"
layout="prev, pager, next, total, sizes, jumper"
v-model:current-page="pageNum"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
></el-pagination>
</el-row>
<div style=" font-size: 12px; padding: 0 10px; color: #606266"><span>{{ state.sql }}</span>
<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">
@@ -89,7 +138,7 @@
</el-select>
</el-col>
<el-col :span="19">
<el-input v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
<el-input ref="conditionInputRef" v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
</el-col>
</el-row>
<template #footer>
@@ -101,15 +150,22 @@
</el-dialog>
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
<el-form ref="dataForm" :model="addDataDialog.data" label-width="160px" size="small">
<el-form-item v-for="column in columns" class="w100" :prop="column.columnName" :label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'">
<el-input-number v-if="DbInst.isNumber(column.columnType)"
<el-form ref="dataForm" :model="addDataDialog.data" label-width="auto" size="small">
<el-form-item
v-for="column in columns"
class="w100"
:prop="column.columnName"
:label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
>
<el-input-number
v-if="DbInst.isNumber(column.columnType)"
v-model="addDataDialog.data[`${column.columnName}`]"
:placeholder="`${column.columnType} ${column.columnComment}`" class="w100" />
:placeholder="`${column.columnType} ${column.columnComment}`"
class="w100"
/>
<el-input v-else v-model="addDataDialog.data[`${column.columnName}`]"
:placeholder="`${column.columnType} ${column.columnComment}`" />
<el-input v-else v-model="addDataDialog.data[`${column.columnName}`]" :placeholder="`${column.columnType} ${column.columnComment}`" />
</el-form-item>
</el-form>
<template #footer>
@@ -123,28 +179,29 @@
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs, ref, Ref } from 'vue';
import { onMounted, watch, reactive, toRefs, ref, Ref, onUnmounted } 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'
import DbTable from '../DbTable.vue';
const emits = defineEmits(['genInsertSql'])
const emits = defineEmits(['genInsertSql']);
const dataForm: any = ref(null);
const conditionInputRef: any = ref();
const props = defineProps({
data: {
type: TabInfo,
required: true
required: true,
},
tableHeight: {
type: [String],
default: '600'
}
})
default: '600',
},
});
const dbTableRef = ref(null) as Ref;
@@ -156,11 +213,13 @@ const state = reactive({
orderBy: '',
condition: '', // 当前条件框的条件
loading: false, // 是否在加载数据
columnNames: [],
columns: [] as any,
pageNum: 1,
pageSize: DbInst.DefaultLimit,
pageSizes: [20, 40, 80, 100, 200, 300, 400],
count: 0,
selectionDatas: [] as any,
condPopVisible: false,
conditionDialog: {
title: '',
placeholder: '',
@@ -168,7 +227,7 @@ const state = reactive({
dataTab: null,
visible: false,
condition: '=',
value: null
value: null,
},
addDataDialog: {
data: {},
@@ -180,42 +239,49 @@ const state = reactive({
hasUpdatedFileds: false,
});
const {
datas,
condition,
loading,
columns,
columnNames,
pageNum,
count,
hasUpdatedFileds,
conditionDialog,
addDataDialog,
} = toRefs(state);
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
watch(() => props.tableHeight, (newValue: any) => {
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信息不能为空")
notBlank(state.table, 'TableData组件params.table信息不能为空');
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
columns.forEach((x: any) => {
x.show = true;
});
state.columns = columns;
state.columnNames = columns.map((t: any) => t.columnName);
await onRefresh();
})
// 点击除选择列按钮外,若存在条件弹窗,则关闭该弹窗
window.addEventListener('click', handlerWindowClick);
});
onUnmounted(() => {
window.removeEventListener('click', handlerWindowClick);
});
const handlerWindowClick = () => {
if (state.condPopVisible) {
state.condPopVisible = false;
}
};
const onRefresh = async () => {
// 查询条件置空
state.condition = '';
state.pageNum = 1;
await selectData();
}
};
/**
* 数据tab修改页数
@@ -232,9 +298,9 @@ const selectData = async () => {
const dbInst = state.ti.getNowDbInst();
const { db } = state.ti;
try {
const countRes = await dbInst.runSql(db, DbInst.getDefaultCountSql(state.table, state.condition));
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(state.table, state.condition));
state.count = countRes.res[0].count;
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum);
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql);
@@ -245,7 +311,13 @@ const selectData = async () => {
} finally {
state.loading = false;
}
}
};
const handleSizeChange = async (size: any) => {
state.pageNum = 1;
state.pageSize = size;
await selectData();
};
/**
* 导出当前页数据
@@ -253,10 +325,15 @@ const selectData = async () => {
const exportData = () => {
const dataList = state.datas as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.columnNames, dataList)
let columnNames = [];
for (let column of state.columns) {
if (column.show) {
columnNames.push(column.columnName);
}
}
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
};
/**
* 条件查询,点击列信息后显示输入对应的值
*/
@@ -266,6 +343,9 @@ const onConditionRowClick = (event: any) => {
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
state.conditionDialog.columnRow = row;
state.conditionDialog.visible = true;
setTimeout(() => {
conditionInputRef.value.focus();
}, 100);
};
// 确认条件
@@ -302,7 +382,7 @@ const onSelectByCondition = async () => {
notEmpty(state.condition, '条件不能为空');
state.pageNum = 1;
await selectData();
}
};
/**
* 表排序字段变更
@@ -323,7 +403,7 @@ const onDataSelectionChange = (datas: []) => {
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
};
/**
* 执行删除数据事件
@@ -332,7 +412,7 @@ const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
const dbInst = state.ti.getNowDbInst();
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
onRefresh();
});
@@ -345,40 +425,41 @@ const onGenerateInsertSql = async () => {
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
};
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
}
};
const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${state.table}'表数据`
state.addDataDialog.title = `添加'${state.table}'表数据`;
state.addDataDialog.visible = true;
};
const closeAddDataDialog = () => {
state.addDataDialog.visible = false;
state.addDataDialog.data = {};
}
};
// 添加新数据行
const addRow = async () => {
dataForm.value.validate(async (valid: boolean) => {
if (valid) {
const dbInst = state.ti.getNowDbInst();
const data = state.addDataDialog.data;
// key: 字段名value: 字段名提示
let obj: any = {};
for (let item of state.columns) {
const value = data[item.columnName]
const value = data[item.columnName];
if (!value) {
continue
continue;
}
obj[`${item.columnName}`] = DbInst.wrapValueByType(value);
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
}
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, () => {
let sql = `INSERT INTO ${dbInst.wrapName(state.table)} (${columnNames}) VALUES (${values});`;
dbInst.promptExeSql(state.ti.db, sql, null, () => {
closeAddDataDialog();
onRefresh();
});
@@ -387,13 +468,11 @@ const addRow = async () => {
return false;
}
});
};
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
background-color: var(--el-color-success);
}
</style>

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-unused-vars */
import { dbApi } from './api';
import { getTextWidth } from '@/common/utils/string';
import SqlExecBox from './component/SqlExecBox';
const dbInstCache: Map<number, DbInst> = new Map();
@@ -8,30 +9,30 @@ export class DbInst {
/**
* 标签路径
*/
tagPath: string
tagPath: string;
/**
* 实例id
*/
id: number
id: number;
/**
* 实例名
*/
name: string
name: string;
/**
* 数据库类型, mysql postgres
*/
type: string
type: string;
/**
* schema -> db
*/
dbs: Map<string, Db> = new Map()
dbs: Map<string, Db> = new Map();
/** 数据库schema多个用空格隔开 */
databases: string
databases: string;
/**
* 默认查询分页数量
@@ -45,9 +46,9 @@ export class DbInst {
*/
getDb(dbName: string) {
if (!dbName) {
throw new Error('dbName不能为空')
throw new Error('dbName不能为空');
}
let db = this.dbs.get(dbName)
let db = this.dbs.get(dbName);
if (db) {
return db;
}
@@ -120,7 +121,7 @@ export class DbInst {
return db.tableHints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name, })
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hits;
return hits;
}
@@ -140,9 +141,19 @@ export class DbInst {
});
}
/**
* 获取count sql
* @param table 表名
* @param condition 条件
* @returns count sql
*/
getDefaultCountSql = (table: string, condition?: string) => {
return `SELECT COUNT(*) count FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} limit 1`;
};
// 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
const baseSql = `SELECT * FROM ${table} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`;
const baseSql = `SELECT * FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`;
if (this.type == 'mysql') {
return `${baseSql} LIMIT ${(pageNum - 1) * limit}, ${limit};`;
}
@@ -169,12 +180,12 @@ export class DbInst {
let values = [];
for (let column of columns) {
const colName = column.columnName;
colNames.push(colName);
colNames.push(this.wrapName(colName));
values.push(DbInst.wrapValueByType(data[colName]));
}
sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
}
return sqls.join(';\n') + ';'
return sqls.join(';\n') + ';';
}
/**
@@ -186,7 +197,7 @@ export class DbInst {
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})`;
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
}
/*
@@ -194,12 +205,30 @@ export class DbInst {
*/
promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
SqlExecBox({
sql, dbId: this.id, db,
sql,
dbId: this.id,
db,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
});
};
/**
* 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
* @param table
* @param condition
* @returns
*/
wrapName = (name: string) => {
if (this.type == 'mysql') {
return `\`${name}\``;
}
if (this.type == 'postgres') {
return `"${name}"`;
}
return name;
};
/**
* 获取或新建dbInst如果缓存中不存在则新建否则直接返回
* @param inst 数据库实例,后端返回的列表接口中的信息
@@ -207,7 +236,7 @@ export class DbInst {
*/
static getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空')
throw new Error('inst不能为空');
}
let dbInst = dbInstCache.get(inst.id);
if (dbInst) {
@@ -249,16 +278,6 @@ export class DbInst {
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 值
@@ -282,7 +301,7 @@ export class DbInst {
return value;
}
return `'${value}'`;
};
}
/**
* 判断字段类型是否为数字类型
@@ -291,7 +310,7 @@ export class DbInst {
*/
static isNumber(columnType: string) {
return columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi);
};
}
/**
*
@@ -300,81 +319,37 @@ export class DbInst {
* @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) {
static flexColumnWidth = (prop: any, tableData: any) => {
if (!prop || !prop.length || prop.length === 0 || prop === 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;
// 获取列名称的长度 加上排序图标长度
const columnWidth: number = getTextWidth(prop) + 40;
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return columnWidth;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 如果是小写字母、数字字符分配8个单位宽度
flexWidth += 8.5;
// 获取该列中最长的数据(内容)
let maxWidthText = '';
let maxWidthValue;
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowValue = tableData[i][prop];
if (!nowValue) {
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 16;
} else {
// 其他种类字符为字符分配9个单位宽度
flexWidth += 8;
// 转为字符串比较长度
let nowText = nowValue + '';
if (nowText.length > maxWidthText.length) {
maxWidthText = nowText;
maxWidthValue = nowValue;
}
}
if (flexWidth > 500) {
// 设置最大宽度
flexWidth = 500;
}
return flexWidth;
const contentWidth: number = getTextWidth(maxWidthText) + 15;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth > 500 ? 500 : flexWidth;
};
}
@@ -382,10 +357,10 @@ export class DbInst {
* 数据库实例信息
*/
class Db {
name: string // 库名
tables: [] // 数据库实例表信息
columnsMap: Map<string, any> = new Map // table -> columns
tableHints: any = null // 提示词
name: string; // 库名
tables: []; // 数据库实例表信息
columnsMap: Map<string, any> = new Map(); // table -> columns
tableHints: any = null; // 提示词
/**
* 获取指定表列信息前提需要dbInst.loadColumns
@@ -426,32 +401,32 @@ export class TabInfo {
/**
* tab唯一key。与label、name都一致
*/
key: string
key: string;
/**
* 菜单树节点key
*/
treeNodeKey: string
treeNodeKey: string;
/**
* 数据库实例id
*/
dbId: number
dbId: number;
/**
* 库名
*/
db: string = ''
db: string = '';
/**
* tab 类型
*/
type: TabType
type: TabType;
/**
* tab需要的其他信息
*/
params: any
params: any;
getNowDbInst() {
return DbInst.getInst(this.dbId);
@@ -465,26 +440,26 @@ export class TabInfo {
/** 修改表字段所需数据 */
export type UpdateFieldsMeta = {
// 主键值
primaryKey: string
primaryKey: string;
// 主键名
primaryKeyName: string
primaryKeyName: string;
// 主键类型
primaryKeyType: string
primaryKeyType: string;
// 新值
fields: FieldsMeta[]
}
fields: FieldsMeta[];
};
export type FieldsMeta = {
// 字段所在div
div: HTMLElement
div: HTMLElement;
// 字段名
fieldName: string
fieldName: string;
// 字段所在的表格行数据
row: any
row: any;
// 字段类型
fieldType: string
fieldType: string;
// 原值
oldValue: string
oldValue: string;
// 新值
newValue: string
}
newValue: string;
};

View File

@@ -1,12 +1,10 @@
import { Enum } from '@/common/Enum'
import { EnumValue } 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),
}
export const DbSqlExecTypeEnum = {
Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
Delete: EnumValue.of(2, 'DELETE').setTagColor('#F9E2AE'),
Insert: EnumValue.of(3, 'INSERT').setTagColor('#A8DEE0'),
Query: EnumValue.of(4, 'QUERY').setTagColor('#A8DEE0'),
Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
};

View File

@@ -0,0 +1,63 @@
// src/basic-languages/mysql/mysql.ts
var language = {
keywords: [
"GROUP BY",
"ORDER BY",
"LEFT JOIN",
"RIGHT JOIN",
"INNER JOIN",
"SELECT * FROM",
],
operators: [
],
builtinFunctions: [
],
builtinVariables: [],
replaceFunctions:[ // 自定义修改函数提示
/** 字符串相关函数 */
{ label: 'CONCAT', insertText:'CONCAT(str1,str2,...)', description: '多字符串合并' },
{ label: 'ASCII', insertText:'ASCII(char)', description: '返回字符的ASCII值' },
{ label: 'BIT_LENGTH', insertText:'BIT_LENGTH(str1)', description: '多字符串合并' },
{ label: 'INSTR', insertText:'INSTR(str,substr)', description: '返回字符串substr所在str位置' },
{ label: 'LEFT', insertText:'LEFT(str,len)', description: '返回字符串str的左端len个字符' },
{ label: 'RIGHT', insertText:'RIGHT(str,len)', description: '返回字符串str的右端len个字符' },
{ label: 'MID', insertText:'MID(str,pos,len)', description: '返回字符串str的位置pos起len个字符' },
{ label: 'SUBSTRING', insertText:'SUBSTRING(exp, start, length)', description: '截取字符串' },
{ label: 'REPLACE', insertText:'REPLACE(str,from_str,to_str)', description: '替换字符串' },
{ label: 'REPEAT', insertText:'REPEAT(str,count)', description: '重复字符串count遍' },
{ label: 'UPPER', insertText:'UPPER(str)', description: '返回大写的字符串' },
{ label: 'LOWER', insertText:'LOWER(str)', description: '返回小写的字符串' },
{ label: 'TRIM', insertText:'TRIM(str)', description: '去除字符串首尾空格' },
/** 数学相关函数 */
{ label: 'ABS', insertText:'ABS(n)', description: '返回n的绝对值' },
{ label: 'FLOOR', insertText:'FLOOR(n)', description: '返回不大于n的最大整数' },
{ label: 'CEILING', insertText:'CEILING(n)', description: '返回不小于n的最小整数值' },
{ label: 'ROUND', insertText:'ROUND(n,d)', description: '返回n的四舍五入值,保留d(默认0)位小数' },
{ label: 'RAND', insertText:'RAND()', description: '返回在范围0到1.0内的随机浮点值' },
/** 日期函数 */
{ label: 'DATE', insertText:'DATE(\'date\')', description: '返回指定表达式的日期部分' },
{ label: 'WEEK', insertText:'WEEK(\'date\')', description: '返回指定日期是一年中的第几周' },
{ label: 'MONTH', insertText:'MONTH(\'date\')', description: '返回指定日期的月份' },
{ label: 'QUARTER', insertText:'QUARTER(\'date\')', description: '返回指定日期是一年的第几个季度' },
{ label: 'YEAR', insertText:'YEAR(\'date\')', description: '返回指定日期的年份' },
{ label: 'DATE_ADD', insertText:'DATE_ADD(\'date\', interval 1 day)', description: '日期函数加减运算' },
{ label: 'DATE_SUB', insertText:'DATE_SUB(\'date\', interval 1 day)', description: '日期函数加减运算' },
{ label: 'DATE_FORMAT', insertText:'DATE_FORMAT(\'date\', \'%Y-%m-%d %h:%i:%s\')', description: '' },
{ label: 'CURDATE', insertText:'CURDATE()', description: '返回当前日期' },
{ label: 'CURTIME', insertText:'CURTIME()', description: '返回当前时间' },
{ label: 'NOW', insertText:'NOW()', description: '返回当前日期时间' },
{ label: 'DATEDIFF', insertText:'DATEDIFF(expr1,expr2)', description: '返回结束日expr1和起始日expr2之间的天数' },
{ label: 'UNIX_TIMESTAMP', insertText:'UNIX_TIMESTAMP()', description: '返回指定时间(默认当前)unix时间戳' },
{ label: 'FROM_UNIXTIME', insertText:'FROM_UNIXTIME(timestamp)', description: '把时间戳格式为年月日时分秒' },
/** 逻辑函数 */
{ label: 'IFNULL', insertText:'IFNULL(expression, alt_value)', description: '表达式为空取第二个参数值,否则取表达式值' },
{ label: 'IF', insertText:'IF(expr1, expr2, expr3)', description: 'expr1为true则取expr2否则取expr3' },
{ label: 'CASE', insertText:'(CASE \n WHEN expr1 THEN expr2 \n ELSE expr3) col', description: 'CASE WHEN THEN ELSE' },
]
};
export {
language
};

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