Compare commits

...

82 Commits

Author SHA1 Message Date
meilin.huang
a4d3a4627a refactor: 系统水印优化等 2023-10-14 16:00:16 +08:00
meilin.huang
77ae6e3bab refactor: 系统水印重构 2023-10-14 00:38:51 +08:00
meilin.huang
e0f1f40ba0 fix: 缓存使用redis无法set问题修复&admin账号默认有所有菜单 2023-10-12 21:50:55 +08:00
meilin.huang
d300f604f1 review 2023-10-12 12:14:56 +08:00
may-fly
2c2c0ff40b Merge pull request #69 from kanzihuang/feat-progress-notify-pullrequest
feat: 显示 SQL 文件执行进度
2023-10-10 20:52:24 -05:00
kanzihuang
b4ddbbd38f fix: 使用最新版 vitess sqlparser 解析 SQL 语句
解决 xwb1989/sqlparser 不支持 current_timestamp() 的问题
2023-10-10 23:56:01 +08:00
wanli
7544288451 feat: 前端显示 SQL 文件执行进度 2023-10-10 23:28:25 +08:00
meilin.huang
41443dccc0 feat: 支持sqlite存储数据 2023-10-10 23:21:29 +08:00
meilin.huang
22e218fc5f refactor: 系统消息调整 2023-10-10 17:39:46 +08:00
meilin.huang
4da0d1abaa refactor: form表单label统一去除':' 2023-10-09 17:29:52 +08:00
meilin.huang
6563b53436 refactor: 包依赖升级等 2023-10-08 12:14:19 +08:00
meilin.huang
fac71a4794 fix: 前端代理默认端口调整&水印开关不生效 2023-09-27 17:19:58 +08:00
meilin.huang
92dff6fdc3 refactor: review 2023-09-26 17:38:52 +08:00
meilin.huang
a1eca3d691 refactor: 登录页调整 2023-09-23 22:52:05 +08:00
meilin.huang
6681dc1057 refactor: 数据库sql提示优化&机器终端支持全屏 2023-09-20 20:42:23 +08:00
meilin.huang
829a68feaa fix: 数据库多库切换关键字提示错误修复&sql编辑器组件统一 2023-09-19 23:00:32 +08:00
meilin.huang
72677e270d feat: 前端用户信息迁移至localstorage 2023-09-16 17:07:48 +08:00
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
328 changed files with 13083 additions and 13346 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,74 +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.20%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
</a>
</p>
### 介绍
web版 **linux(终端[终端回放] 文件 脚本 进程)、数据库mysql postgres、redis(单机 哨兵 集群)、mongo统一管理操作平台**
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://www.yuque.com/may-fly/mayfly-go
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
### 演示环境
http://go.mayfly.run
账号/密码test/test123.
### 系统核心功能截图
##### 记录操作记录
![记录操作记录](https://objs.gitee.io/mayfly-go-docs/home/log.jpg "屏幕截图.png")
#### 机器操作
##### 状态查看
![状态查看](https://objs.gitee.io/mayfly-go-docs/home/machine-status.jpg "屏幕截图.png")
##### ssh终端
##### 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 编辑器
![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 操作
#### Redis操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png")
#### Mongo 操作
#### 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://www.yuque.com/may-fly/mayfly-go
#### 💌 支持作者
如果觉得项目不错,或者已经在使用了,希望你可以去 <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,12 +74,13 @@ 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}
cp ${server_folder}/mayfly-go.sql ${toFolder}
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
@@ -104,9 +105,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 +114,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 +146,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 +161,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 +188,13 @@ function runBuild() {
;;
esac
echo_green "删除['${server_folder}/static/static']下静态资源文件."
# 删除静态资源文件保留一个favicon.ico否则后端启动会报错
rm -rf ${server_folder}/static/static/assets
rm -rf ${server_folder}/static/static/config.js
rm -rf ${server_folder}/static/static/index.html
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
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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,48 +10,49 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.5.0",
"axios": "^1.4.0",
"countup.js": "^2.0.7",
"asciinema-player": "^3.6.2",
"axios": "^1.5.0",
"countup.js": "^2.7.0",
"cropperjs": "^1.5.11",
"echarts": "^5.4.0",
"element-plus": "^2.3.8",
"element-plus": "^2.4.0",
"jsencrypt": "^3.3.1",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.40.0",
"monaco-editor": "^0.44.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.4",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sortablejs": "^1.15.0",
"sql-formatter": "^12.1.2",
"vue": "^3.3.4",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.2.4",
"xterm": "^5.2.1",
"xterm-addon-fit": "^0.7.0"
"vue-router": "^4.2.5",
"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",
"@types/node": "^15.6.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.10.6",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.0.11",
"dotenv": "^10.0.0",
"@types/sortablejs": "^1.15.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.3.0",
"sass": "^1.62.0",
"sass-loader": "^13.2.0",
"sass": "^1.69.0",
"typescript": "^5.0.2",
"vite": "^4.4.2",
"vue-eslint-parser": "^9.1.1"
"vite": "^4.4.11",
"vue-eslint-parser": "^9.3.1"
},
"browserslist": [
"> 1%",

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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

View File

@@ -1,7 +0,0 @@
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501,
}

View File

@@ -1,12 +1,16 @@
import request from './request';
export default {
login: (param: any) => request.post('/sys/accounts/login', param),
otpVerify: (param: any) => request.post('/sys/accounts/otp-verify', param),
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
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('/sys/accounts/logout/{token}'),
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,8 +1,7 @@
import router from '../router';
import Axios from 'axios';
import { ResultEnum } from './enums';
import config from './config';
import { getSession } from './utils/storage';
import { getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
@@ -21,8 +20,16 @@ export interface Result {
data?: any;
}
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;
// const baseWsUrl: string = config.baseWsUrl;
/**
* 通知错误消息
@@ -36,14 +43,14 @@ function notifyErrorMsg(msg: string) {
// create an axios instance
const service = Axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 20000, // request timeout
timeout: 60000, // request timeout
});
// request interceptor
service.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getSession('token');
const token = getToken();
if (token) {
// 设置token
config.headers['Authorization'] = token;
@@ -60,34 +67,46 @@ service.interceptors.response.use(
(response) => {
// 获取请求返回结果
const data: Result = response.data;
if (data.code === ResultEnum.SUCCESS) {
return data.data;
}
// 如果提示没有权限则移除token使其重新登录
if (data.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
if (data.code === ResultEnum.SUCCESS) {
return data.data;
} else {
return Promise.reject(data);
}
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;
}
);
@@ -124,8 +143,8 @@ function request(method: string, url: string, params: any = null, headers: any =
.request(query)
.then((res) => res)
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可
if (e.msg) {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (e.msg && e?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
@@ -157,7 +176,7 @@ function del(url: string, params: any = null, headers: any = null, options: any
function getApiUrl(url: string) {
// 只是返回api地址而不做请求用在上传组件之类的
return baseUrl + url + '?token=' + getSession('token');
return baseUrl + url + '?token=' + getToken();
}
export default {

View File

@@ -25,7 +25,7 @@ export async function RsaEncrypt(value: any) {
if (!value) {
return '';
}
if (encryptor != null) {
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();

View File

@@ -1,43 +1,77 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import { ElNotification, NotificationHandle } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import { createVNode, reactive } from "vue";
import { buildProgressProps } from "@/components/progress-notify/progress-notify";
import ProgressNotify from '/src/components/progress-notify/progress-notify.vue';
export default {
/**
* 全局系统消息websocket
*/
sysMsgSocket() {
const token = getSession('token');
const token = getToken();
if (!token) {
return null;
}
const messageTypes = {
0: "error",
1: "success",
2: "info",
}
const notifyMap: Map<Number, any> = new Map()
return SocketBuilder.builder(`${Config.baseWsUrl}/sysmsg?token=${token}`)
.message((event: { data: string }) => {
const message = JSON.parse(event.data);
let mtype: string;
switch (message.type) {
case 0:
mtype = 'error';
break;
case 2:
mtype = 'info';
break;
case 1:
mtype = 'success';
const type = messageTypes[message.type]
switch (message.category) {
case "execSqlFileProgress":
const content = JSON.parse(message.msg)
const id = content.id
let progress = notifyMap.get(id)
if (content.terminated) {
if (progress != undefined) {
progress.notification?.close()
notifyMap.delete(id)
progress = undefined
}
return
}
if (progress == undefined) {
progress = {
props: reactive(buildProgressProps()),
notification: undefined,
}
}
progress.props.progress.sqlFileName = content.sqlFileName
progress.props.progress.executedStatements = content.executedStatements
if (!notifyMap.has(id)) {
const vNodeMessage = createVNode(
ProgressNotify,
progress.props,
null,
)
progress.notification = ElNotification({
duration: 0,
title: message.title,
message: vNodeMessage,
type: type,
showClose: false,
});
notifyMap.set(id, progress)
}
break;
default:
mtype = 'info';
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: type,
});
break;
}
if (mtype == undefined) {
return;
}
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: mtype as any,
});
})
.open((event: any) => console.log(event))
.build();

View File

@@ -3,7 +3,7 @@ import openApi from './openApi';
// 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWartermarkConfigKey = 'UseWartermark';
const UseWatermarkConfigKey = 'UseWatermark';
/**
* 获取系统配置值
@@ -53,12 +53,21 @@ export async function useLoginCaptcha(): Promise<boolean> {
}
/**
* 是否启用水印
* 是否启用水印信息配置
*
* @returns
*/
export async function useWartermark(): Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true);
export async function useWatermark(): Promise<any> {
const value = await getConfigValue(UseWatermarkConfigKey);
if (!value) {
return {
isUse: true,
};
}
const jsonValue = JSON.parse(value);
// 将字符串转为bool
jsonValue.isUse = convertBool(jsonValue.isUse, true);
return jsonValue;
}
function convertBool(value: string, defaultValue: boolean) {
@@ -67,3 +76,13 @@ function convertBool(value: string, defaultValue: boolean) {
}
return value == '1' || value == 'true';
}
/**
* 获取LDAP登录配置
*
* @returns
*/
export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}

View File

@@ -6,6 +6,11 @@ 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) {
@@ -16,9 +21,9 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
// 再将逗号转义
val = `"${val}"`;
}
dataValueArr.push(val);
dataValueArr.push(val + '\t');
} else {
dataValueArr.push(val);
dataValueArr.push(val + '\t');
}
}
cvsData.push(dataValueArr);
@@ -28,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

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

View File

@@ -1,17 +1,70 @@
const TokenKey = 'token';
const UserKey = 'user';
const TagViewsKey = 'tagViews';
// 获取请求token
export function getToken(): string {
return getLocal(TokenKey);
}
// 保存用户访问token
export function saveToken(token: string) {
setLocal(TokenKey, token);
}
// 获取登录用户基础信息
export function getUser() {
return getLocal(UserKey);
}
// 保存用户信息
export function saveUser(userinfo: any) {
setLocal(UserKey, userinfo);
}
export function saveThemeConfig(themeConfig: any) {
setLocal('themeConfig', themeConfig);
}
export function getThemeConfig() {
return getLocal('themeConfig');
}
// 清除用户相关的用户信息
export function clearUser() {
removeLocal(TokenKey);
removeLocal(UserKey);
}
export function getTagViews() {
return getSession(TagViewsKey);
}
export function setTagViews(tagViews: Array<object>) {
setSession(TagViewsKey, tagViews);
}
export function removeTagViews() {
removeSession(TagViewsKey);
}
// 1. localStorage
// 设置永久缓存
export function setLocal(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
}
// 获取永久缓存
export function getLocal(key: string) {
let json: any = window.localStorage.getItem(key);
return JSON.parse(json);
}
// 移除永久缓存
export function removeLocal(key: string) {
window.localStorage.removeItem(key);
}
// 移除全部永久缓存
export function clearLocal() {
window.localStorage.clear();
@@ -22,33 +75,20 @@ export function clearLocal() {
export function setSession(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
}
// 获取临时缓存
export function getSession(key: string) {
let json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
}
// 移除临时缓存
export function removeSession(key: string) {
window.sessionStorage.removeItem(key);
}
// 移除全部临时缓存
export function clearSession() {
clearUser();
window.sessionStorage.clear();
}
export function getUserInfo4Session() {
return getSession('userInfo');
}
export function setUserInfo2Session(userinfo: any) {
setSession('userInfo', userinfo);
}
// 获取是否开启水印
export function getUseWatermark4Session() {
return getSession('useWatermark');
}
export function setUseWatermark2Session(useWatermark: boolean) {
setSession('useWatermark', useWatermark);
}

View File

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

View File

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

View File

@@ -12,13 +12,15 @@ const props = defineProps({
required: true,
},
value: {
type: Object,
type: [Object, String, Number],
required: true,
},
});
const defaultType = 'primary';
const state = reactive({
type: 'primary',
type: defaultType,
color: '',
enumLabel: '',
});
@@ -50,6 +52,8 @@ const convert = (value: any) => {
if (enumValue.tag) {
state.color = enumValue.tag.color;
state.type = enumValue.tag.type;
} else {
state.type = defaultType;
}
};
</script>

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,9 +9,31 @@
<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/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 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/
@@ -25,6 +47,11 @@ 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,
@@ -127,7 +154,22 @@ const options = {
},
};
const monacoTextarea: any = ref();
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
const state = reactive({
editorHeight: '500px',
languageMode: 'shell',
});
@@ -145,6 +187,7 @@ onBeforeUnmount(() => {
monacoEditorIns.dispose();
}
if (completionItemProvider) {
console.log('unmount=> dispose completion item provider');
completionItemProvider.dispose();
}
});
@@ -166,19 +209,14 @@ watch(
}
);
const monacoTextarea: any = ref(null);
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
// 监听 themeConfig editorTheme配置文件的变化
watch(
() => themeConfig.value.editorTheme,
(val) => {
console.log('monaco editor theme change: ', val);
monaco?.editor?.setTheme(val);
}
);
const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器');
@@ -186,17 +224,13 @@ const initMonacoEditorIns = () => {
// 初始化一些主题
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) => {
@@ -227,6 +261,7 @@ const setEditorValue = (value: any) => {
*/
const registerCompletionItemProvider = () => {
if (completionItemProvider) {
console.log('exist competion item provider, dispose now');
completionItemProvider.dispose();
}
if (state.languageMode == 'shell') {
@@ -266,7 +301,11 @@ const format = () => {
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
};
defineExpose({ format });
const getEditor = () => {
return monacoEditorIns;
};
defineExpose({ getEditor, format });
</script>
<style lang="scss">

View File

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

View File

@@ -102,7 +102,7 @@
<el-table
v-bind="$attrs"
max-height="700"
:max-height="tableMaxHeight"
@selection-change="handleSelectionChange"
:data="props.data"
highlight-current-row
@@ -145,13 +145,7 @@
width="600px"
>
<template #default>
<el-input
input-style="color: black;"
:autosize="{ minRows: 3, maxRows: 15 }"
disabled
v-model="formatVal"
type="textarea"
/>
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
</template>
<template #reference>
<el-link
@@ -264,6 +258,7 @@ const state = reactive({
// 输入框宽度
inputWidth: "200px" as any,
formatVal: '', // 格式化后的值
tableMaxHeight: window.innerHeight - 240 + 'px',
})
const {
@@ -274,6 +269,7 @@ const {
loadingData,
inputWidth,
formatVal,
tableMaxHeight,
} = toRefs(state)
watch(() => props.queryForm, (newValue: any) => {
@@ -304,7 +300,7 @@ onMounted(() => {
state.pageNum = props.pageNum;
state.pageSize = pageSize;
state.queryForm = props.queryForm;
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4];
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
// 如果没传输入框宽度则根据组件size设置默认宽度
if (!props.inputWidth) {
@@ -312,8 +308,16 @@ onMounted(() => {
} else {
state.inputWidth = props.inputWidth;
}
window.addEventListener('resize', () => {
calcuTableHeight();
});
})
const calcuTableHeight = () => {
state.tableMaxHeight = window.innerHeight - 240 + 'px';
}
const formatText = (data: any)=> {
state.formatVal = '';
try {
@@ -360,6 +364,7 @@ const reset = () => {
for (let qi of props.query) {
state.queryForm[qi.prop] = null;
}
changePageNum(1);
emit('update:queryForm', state.queryForm);
execQuery();

View File

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

View File

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

View File

@@ -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,321 @@
<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 - ${openTerminal.fullscreen ? '47px' : '200px'})` }">
<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 handlerFullScreen = (terminal: any) => {
terminal.fullscreen = !terminal.fullscreen;
const terminalRef = openTerminalRefs[terminal.terminalId];
// fit
setTimeout(() => {
terminalRef?.fitTerminal();
terminalRef?.focus();
}, 250);
};
const closeMinimizeTerminal = (terminalId: any) => {
delete state.minimizeTerminals[terminalId];
close(terminalId);
};
defineExpose({
open,
close,
minimize,
maximize,
});
</script>
<style lang="scss">
.terminal-dialog-container {
.el-dialog__header {
padding: 10px;
}
// 取消body最大高度否则全屏有问题
.el-dialog__body {
max-height: 100% !important;
overflow: hidden !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

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

View File

@@ -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="
(el) => {
if (el) columnsAsideOffsetTopRefs[k] = el;
}
" :class="{ 'layout-columns-active': state.liIndex === k }" :title="v.meta.title">
<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"
>
<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

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

View File

@@ -1,16 +1,25 @@
<template>
<el-main class="layout-main">
<el-scrollbar class="layout-scrollbar" ref="layoutScrollbarRef"
<el-scrollbar
class="layout-scrollbar"
ref="layoutScrollbarRef"
v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1"
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }">
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }"
>
<LayoutParentView />
<Footer v-if="themeConfig.isFooter" />
</el-scrollbar>
<Link :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
<Iframes :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
<Link
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2"
/>
<Iframes
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta" />
@getCurrentRouteMeta="onGetCurrentRouteMeta"
/>
</el-main>
</template>
@@ -19,10 +28,10 @@ import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LayoutParentView from '@/views/layout/routerView/parent.vue';
import Footer from '@/views/layout/footer/index.vue';
import Link from '@/views/layout/routerView/link.vue';
import Iframes from '@/views/layout/routerView/iframes.vue';
import LayoutParentView from '@/layout/routerView/parent.vue';
import Footer from '@/layout/footer/index.vue';
import Link from '@/layout/routerView/link.vue';
import Iframes from '@/layout/routerView/iframes.vue';
const { proxy } = getCurrentInstance() as any;
const { themeConfig } = storeToRefs(useThemeConfig());

View File

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

View File

@@ -0,0 +1,352 @@
<template>
<div v-show="state.isShowLockScreen">
<div class="layout-lock-screen-mask"></div>
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
<div class="layout-lock-screen">
<div
class="layout-lock-screen-date"
ref="layoutLockScreenDateRef"
@mousedown="onDownPc"
@mousemove="onMovePc"
@mouseup="onEnd"
@touchstart.stop="onDownApp"
@touchmove.stop="onMoveApp"
@touchend.stop="onEnd"
>
<div class="layout-lock-screen-date-box">
<div class="layout-lock-screen-date-box-time">
{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
</div>
<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
</div>
<div class="layout-lock-screen-date-top">
<SvgIcon name="ele-Top" />
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
</div>
</div>
<transition name="el-zoom-in-center">
<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
<div class="layout-lock-screen-login-box">
<div class="layout-lock-screen-login-box-img">
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
</div>
<div class="layout-lock-screen-login-box-name">Administrator</div>
<div class="layout-lock-screen-login-box-value">
<el-input
placeholder="请输入密码"
ref="layoutLockScreenInputRef"
v-model="state.lockScreenPassword"
@keyup.enter.native.stop="onLockScreenSubmit()"
>
<template #append>
<el-button @click="onLockScreenSubmit">
<el-icon class="el-input__icon">
<ele-Right />
</el-icon>
</el-button>
</template>
</el-input>
</div>
</div>
<div class="layout-lock-screen-login-icon">
<SvgIcon name="ele-Microphone" :size="20" />
<SvgIcon name="ele-AlarmClock" :size="20" />
<SvgIcon name="ele-SwitchButton" :size="20" />
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup lang="ts" name="layoutLockScreen">
import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
import { formatDate } from '@/common/utils/format';
import { setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
// 定义变量内容
const layoutLockScreenDateRef = ref<any>();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
transparency: 1,
downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '' as any,
time: {
hm: '',
s: '',
mdq: '',
},
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下 pc
const onDownPc = (down: MouseEvent) => {
state.isFlags = true;
state.downClientY = down.clientY;
};
// 鼠标按下 app
const onDownApp = (down: TouchEvent) => {
state.isFlags = true;
state.downClientY = down.touches[0].clientY;
};
// 鼠标移动 pc
const onMovePc = (move: MouseEvent) => {
state.moveDifference = move.clientY - state.downClientY;
onMove();
};
// 鼠标移动 app
const onMoveApp = (move: TouchEvent) => {
state.moveDifference = move.touches[0].clientY - state.downClientY;
onMove();
};
// 鼠标移动事件
const onMove = () => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = layoutLockScreenDateRef.value;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
setLocal('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
});
</script>
<style scoped lang="scss">
.layout-lock-screen-fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layout-lock-screen-filter {
filter: blur(1px);
}
.layout-lock-screen-mask {
background: var(--el-color-white);
@extend .layout-lock-screen-fixed;
z-index: 9999990;
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background: url('@/assets/image/login-bg-main.svg') no-repeat;
background-size: 100% 100%;
z-index: 9999991;
}
.layout-lock-screen {
@extend .layout-lock-screen-fixed;
z-index: 9999992;
&-date {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
color: var(--el-color-white);
z-index: 9999993;
user-select: none;
&-box {
position: absolute;
left: 30px;
bottom: 50px;
&-time {
font-size: 100px;
color: var(--el-color-white);
}
&-info {
font-size: 40px;
color: var(--el-color-white);
}
&-minutes {
font-size: 16px;
}
}
&-top {
width: 40px;
height: 40px;
line-height: 40px;
border-radius: 100%;
border: 1px solid var(--el-border-color-light, #ebeef5);
background: rgba(255, 255, 255, 0.1);
color: var(--el-color-white);
opacity: 0.8;
position: absolute;
right: 30px;
bottom: 50px;
text-align: center;
overflow: hidden;
transition: all 0.3s ease;
i {
transition: all 0.3s ease;
}
&-text {
opacity: 0;
position: absolute;
top: 150%;
font-size: 12px;
color: var(--el-color-white);
left: 50%;
line-height: 1.2;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
width: 35px;
}
&:hover {
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
color: var(--el-color-white);
opacity: 1;
transition: all 0.3s ease;
i {
transform: translateY(-40px);
transition: all 0.3s ease;
}
.layout-lock-screen-date-top-text {
opacity: 1;
top: 50%;
transition: all 0.3s ease;
}
}
}
}
&-login {
position: relative;
z-index: 9999994;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--el-color-white);
&-box {
text-align: center;
margin: auto;
&-img {
width: 180px;
height: 180px;
margin: auto;
img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
&-name {
font-size: 26px;
margin: 15px 0 30px;
}
}
&-icon {
position: absolute;
right: 30px;
bottom: 30px;
i {
font-size: 20px;
margin-left: 15px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
}
}
:deep(.el-input-group__append) {
background: var(--el-color-white);
padding: 0px 15px;
}
:deep(.el-input__inner) {
border-right-color: var(--el-border-color-extra-light);
&:hover {
border-color: var(--el-border-color-extra-light);
}
}
</style>

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

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

View File

@@ -17,10 +17,10 @@
<script lang="ts">
import { computed } from 'vue';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import ColumnsAside from '@/views/layout/component/columnsAside.vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import ColumnsAside from '@/layout/component/columnsAside.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutColumns',

View File

@@ -15,9 +15,9 @@
<script lang="ts">
import { computed, getCurrentInstance, watch } from 'vue';
import { useRoute } from 'vue-router';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutDefaults',
@@ -32,7 +32,7 @@ export default {
watch(
() => route.path,
() => {
proxy.$refs.layoutDefaultsScrollbarRef.wrap$.scrollTop = 0;
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
}
);
return {

View File

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

View File

@@ -14,13 +14,12 @@ import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Breadcrumb from '@/views/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/views/layout/navBars/breadcrumb/user.vue';
import Logo from '@/views/layout/logo/index.vue';
import Horizontal from '@/views/layout/navMenu/horizontal.vue';
import Breadcrumb from '@/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/layout/navBars/breadcrumb/user.vue';
import Logo from '@/layout/logo/index.vue';
import Horizontal from '@/layout/navMenu/horizontal.vue';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());
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,21 +367,25 @@
<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>
</div>
</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)">一键复制配置
<!-- <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-button>
</div>
</div> -->
</el-scrollbar>
</el-drawer>
</div>
@@ -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="消息">
@@ -47,7 +56,7 @@
<crop />
</el-icon>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link" style="cursor: pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfo.name || userInfo.username }}
@@ -70,25 +79,29 @@ import { ref, computed, reactive, onMounted } 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 UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue';
import { clearSession, removeLocal } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi';
import { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
const router = useRouter();
const searchRef = ref();
const state = reactive({
isDark: false,
isScreenfull: false,
isShowUserNewsPopover: false,
disabledI18n: 'zh-cn',
disabledSize: '',
});
const { userInfo } = storeToRefs(useUserInfo());
const { themeConfig } = storeToRefs(useThemeConfig());
const themeConfigStore = useThemeConfig();
const { themeConfig } = storeToRefs(themeConfigStore);
//
const layoutUserFlexNum = computed(() => {
@@ -122,8 +135,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(() => {
@@ -145,12 +159,17 @@ const onHandleCommandClick = (path: string) => {
ElMessage.success('安全退出成功!');
}, 300);
})
.catch(() => { });
.catch(() => {});
} else {
router.push(path);
}
};
const switchDark = (isDark: boolean) => {
themeConfigStore.switchDark(isDark);
saveThemeConfig(themeConfig.value);
};
// //
const onSearchClick = () => {
searchRef.value.openSearch();
@@ -160,7 +179,7 @@ const onSearchClick = () => {
const onComponentSizeChange = (size: string) => {
removeLocal('themeConfig');
themeConfig.value.globalComponentSize = size;
setLocal('themeConfig', themeConfig.value);
saveThemeConfig(themeConfig.value);
// proxy.$ELEMENT.size = size;
initComponentSize();
window.location.reload();
@@ -168,7 +187,7 @@ const onComponentSizeChange = (size: string) => {
//
const initComponentSize = () => {
switch (getLocal('themeConfig').globalComponentSize) {
switch (getThemeConfig().globalComponentSize) {
case '':
state.disabledSize = '';
break;
@@ -186,8 +205,10 @@ const initComponentSize = () => {
//
onMounted(() => {
if (getLocal('themeConfig')) {
const themeConfig = getThemeConfig();
if (themeConfig) {
initComponentSize();
state.isDark = themeConfig.isDark;
}
});
</script>
@@ -244,4 +265,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

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

View File

@@ -0,0 +1,138 @@
<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"
>
<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)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
// 定义父组件传过来的值
const props = defineProps({
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
{
contextMenuClickId: 4,
txt: '当前页全屏',
affix: false,
icon: 'full-screen',
},
],
item: {} as any,
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.path });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

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 tagsViews"
: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" />
<span>{{ v.meta.title }}</span>
"
>
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.title }}</span>
<template v-if="isActive(v)">
<SvgIcon name="RefreshRight" 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)" />
<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.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
<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)" />
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
</ul>
</el-scrollbar>
@@ -38,17 +52,24 @@ import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getSession, setSession, removeSession } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
import Sortable from 'sortablejs';
import Contextmenu from '@/views/layout/navBars/tagsView/contextmenu.vue';
import Contextmenu from '@/layout/navBars/tagsView/contextmenu.vue';
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
import { useTagsViews } from '@/store/tagsViews';
import { useKeepALiveNames } from '@/store/keepAliveNames';
const { proxy } = getCurrentInstance() as any;
const tagsRefs = ref([]) as any;
const scrollbarRef = ref();
const contextmenuRef = ref();
const tagsUlRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig());
const { tagsViews } = storeToRefs(useTagsViews());
const keepAliveNamesStores = useKeepALiveNames();
const route = useRoute();
const router = useRouter();
@@ -56,7 +77,6 @@ const state = reactive({
routePath: route.fullPath,
dropdown: { x: '', y: '' },
tagsRefsIndex: 0,
tagsViewList: [] as any,
sortable: '' as any,
});
@@ -67,119 +87,153 @@ const setTagsStyle = computed(() => {
// tagsViewList
const addBrowserSetSession = (tagsViewList: Array<object>) => {
setSession('tagsViewList', tagsViewList);
setTagViews(tagsViewList);
};
// vuex tagsViewRoutes
// tagsViewRoutes
const getTagsViewRoutes = () => {
state.routePath = route.fullPath;
state.tagsViewList = [];
if (!themeConfig.value.isCacheTagsView) removeSession('tagsViewList');
tagsViews.value = [];
if (!themeConfig.value.isCacheTagsView) {
removeTagViews();
}
initTagsView();
};
// vuex isAffix
// isAffix
const initTagsView = () => {
if (getSession('tagsViewList') && themeConfig.value.isCacheTagsView) {
state.tagsViewList = getSession('tagsViewList');
const tagViews = getTagViews();
if (tagViews && themeConfig.value.isCacheTagsView) {
tagsViews.value = tagViews;
} else {
state.tagsViewList?.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
tagsViews.value?.map((v: any) => {
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
keepAliveNamesStores.setCacheKeepAlive(v);
}
});
addTagsView(route.fullPath);
}
// (li)
getTagsRefsIndex(route.fullPath);
setTagsRefsIndex(route.fullPath);
//
tagsViewmoveToCurrentTag();
};
// 1 tagsViewisHide tagsView
// pathfullPath
const addTagsView = (path: string, to: any = null) => {
if (!to) {
to = route;
}
path = decodeURI(path);
for (let tv of state.tagsViewList) {
if (tv.fullPath === path) {
return false;
const addTagsView = (path: string, to: any = null, tagViewIndex: number = -1) => {
nextTick(async () => {
if (!to) {
to = route;
}
}
const tagView = { ...to }
// Converting circular structure to JSON
tagView.matched = null;
tagView.redirectedFrom = null;
state.tagsViewList.push(tagView);
addBrowserSetSession(state.tagsViewList);
for (let tv of tagsViews.value) {
if (tv.path === path) {
return false;
}
}
const tagView = {
path: path,
name: to.name,
query: to.query,
title: to.meta.title,
icon: to.meta.icon,
isAffix: to.meta.isAffix,
isKeepAlive: to.meta.isKeepAlive,
};
if (tagViewIndex != -1) {
tagsViews.value.splice(tagViewIndex + 1, 0, tagView);
} else {
tagsViews.value.push(tagView);
}
await keepAliveNamesStores.addCachedView(tagView);
addBrowserSetSession(tagsViews.value);
});
};
// 2 tagsView
// pathfullPath
const refreshCurrentTagsView = (path: string) => {
const refreshCurrentTagsView = async (path: string) => {
const item = getTagsView(path);
await keepAliveNamesStores.delCachedView(item);
keepAliveNamesStores.addCachedView(item);
mittBus.emit('onTagsViewRefreshRouterView', path);
};
const getTagsView = (path: string) => {
return tagsViews.value.find((v: any) => v.path === path);
};
// 3 tagsViewisAffix
// pathfullPath
const closeCurrentTagsView = (path: string) => {
state.tagsViewList.map((v: any, k: number, arr: any) => {
if (!v.meta.isAffix) {
if (v.fullPath === path) {
state.tagsViewList.splice(k, 1);
tagsViews.value.map((v: TagsView, k: number, arr: any) => {
if (!v.isAffix) {
if (v.path === path) {
keepAliveNamesStores.delCachedView(v);
tagsViews.value.splice(k, 1);
setTimeout(() => {
if (state.routePath !== path) {
return;
}
let next;
let next: TagsView;
//
if (state.tagsViewList.length === k) {
next = k !== arr.length ? arr[k] : arr[arr.length - 1]
if (tagsViews.value.length === k) {
next = k !== arr.length ? arr[k] : arr[arr.length - 1];
} else {
next = arr[k];
}
if (next.meta.isDynamic) {
router.push({ name: next.name, params: next.params });
} else {
if (next) {
router.push({ path: next.path, query: next.query });
} else {
router.push({ path: '/' });
}
}, 0);
}
}
});
addBrowserSetSession(state.tagsViewList);
addBrowserSetSession(tagsViews.value);
};
// 4 tagsViewisAffix
const closeOtherTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: TagsView) => {
if (v.isAffix && !v.isHide) {
keepAliveNamesStores.delOthersCachedViews(v);
tagsViews.value.push({ ...v });
}
});
addTagsView(path);
};
// 5 tagsViewisAffix
const closeAllTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
keepAliveNamesStores.delAllCachedViews();
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
state.tagsViewList.push({ ...v });
if (state.tagsViewList.some((v: any) => v.path === path)) router.push({ path, query: route.query });
else router.push({ path: v.path, query: route.query });
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
if (tagsViews.value.some((v: any) => v.path === path)) {
router.push({ path, query: route.query });
}
}
});
addBrowserSetSession(state.tagsViewList);
if (tagsViews.value) {
router.push({ path: '/' });
}
addBrowserSetSession(tagsViews.value);
};
// 6
const openCurrenFullscreen = (path: string) => {
const item = state.tagsViewList.find((v: any) => v.fullPath === path);
const item = tagsViews.value.find((v: any) => v.path === path);
nextTick(() => {
router.push({ path, query: item.query });
router.push({ path, query: item?.query });
const element = document.querySelector('.layout-main');
const screenfulls: any = screenfull;
screenfulls.request(element);
@@ -189,17 +243,17 @@ const openCurrenFullscreen = (path: string) => {
const onCurrentContextmenuClick = (data: any) => {
// pathfullPath
let { id, path } = data;
let currentTag = state.tagsViewList.find((v: any) => v.fullPath === path);
let currentTag = tagsViews.value.find((v: any) => v.path === path);
switch (id) {
case 0:
refreshCurrentTagsView(path);
router.push({ path, query: currentTag.query });
router.push({ path, query: currentTag?.query });
break;
case 1:
closeCurrentTagsView(path);
break;
case 2:
router.push({ path, query: currentTag.query });
router.push({ path, query: currentTag?.query });
closeOtherTagsView(path);
break;
case 3:
@@ -211,8 +265,8 @@ const onCurrentContextmenuClick = (data: any) => {
}
};
//
const isActive = (route: any) => {
return route.fullPath === state.routePath;
const isActive = (tagView: TagsView) => {
return tagView.path === state.routePath;
};
// x,y props
const onContextmenu = (v: any, e: any) => {
@@ -223,7 +277,7 @@ const onContextmenu = (v: any, e: any) => {
};
// tagsView
const onTagsClick = (v: any, k: number) => {
state.routePath = decodeURI(v.fullPath);
state.routePath = decodeURI(v.path);
state.tagsRefsIndex = k;
router.push(v);
};
@@ -288,9 +342,9 @@ const tagsViewmoveToCurrentTag = () => {
});
};
// tagsView tagsView
const getTagsRefsIndex = (path: string) => {
if (state.tagsViewList.length > 0) {
state.tagsRefsIndex = state.tagsViewList.findIndex((item: any) => item.fullPath === path);
const setTagsRefsIndex = (path: string) => {
if (tagsViews.value.length > 0) {
state.tagsRefsIndex = tagsViews.value.findIndex((item: any) => item.path === path);
}
};
// tagsView
@@ -305,7 +359,7 @@ const initSortable = () => {
onEnd: () => {
const sortEndList: any = [];
state.sortable.toArray().map((val: any) => {
state.tagsViewList.map((v: any) => {
tagsViews.value.map((v: any) => {
if (v.name === val) sortEndList.push({ ...v });
});
});
@@ -315,18 +369,6 @@ const initSortable = () => {
}
};
// tagsView
// watch(
// pinia.state,
// (val) => {
// if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
// getTagsViewRoutes();
// },
// {
// deep: true,
// }
// );
//
onBeforeMount(() => {
// 0 1 2 3 4
@@ -357,17 +399,18 @@ onMounted(() => {
});
//
onBeforeRouteUpdate((to) => {
state.routePath = decodeURI(to.fullPath);
addTagsView(to.fullPath, to);
getTagsRefsIndex(to.fullPath);
const path = decodeURI(to.fullPath);
state.routePath = path;
addTagsView(path, to, state.tagsRefsIndex);
setTagsRefsIndex(path);
tagsViewmoveToCurrentTag();
});
</script>
<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

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

View File

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

View File

@@ -2,7 +2,7 @@
<div class="h100">
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="state.keepAliveNameList">
<keep-alive :include="getKeepAliveNames">
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" />
</keep-alive>
</transition>
@@ -11,38 +11,62 @@
</template>
<script lang="ts" setup name="layoutParentView">
import { computed, reactive, onBeforeMount, onUnmounted, nextTick } from 'vue';
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useKeepALiveNames } from '@/store/keepAliveNames';
import mittBus from '@/common/utils/mitt';
import { getTagViews } from '@/common/utils/storage';
const route = useRoute();
const { themeConfig } = storeToRefs(useThemeConfig());
const { keepAliveNames } = storeToRefs(useKeepALiveNames());
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
const state: any = reactive({
refreshRouterViewKey: null,
keepAliveNameList: [],
keepAliveNameNewList: [],
});
// refreshRouterViewKey
// onBeforeRouteUpdate((to: any) => {
// state.refreshRouterViewKey = decodeURI(to.fullPath);
// });
// (name)
const getKeepAliveNames = computed(() => {
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
});
//
onBeforeMount(() => {
state.keepAliveNameList = keepAliveNames.value;
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
if (decodeURI(route.fullPath) !== path) return false;
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
state.refreshRouterViewKey = route.path;
state.refreshRouterViewKey = '';
nextTick(() => {
state.refreshRouterViewKey = null;
state.refreshRouterViewKey = path;
state.keepAliveNameList = keepAliveNames.value;
});
});
});
//
onMounted(() => {
nextTick(() => {
setTimeout(() => {
if (themeConfig.value.isCacheTagsView) {
let tagsViewArr: any = getTagViews() || [];
cachedViews.value = tagsViewArr.filter((item: any) => item?.isKeepAlive).map((item: any) => item.name as string);
}
}, 0);
});
});
// tagsView
watch(
() => route.fullPath,
() => {
state.refreshRouterViewKey = decodeURI(route.fullPath);
},
{
immediate: true,
}
);
//
const setTransitionName = computed(() => {
return themeConfig.value.animation;

View File

@@ -9,6 +9,7 @@ import { registElSvgIcon } from '@/common/utils/svgIcons';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import 'element-plus/theme-chalk/dark/css-vars.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { ElMessage } from 'element-plus';

View File

@@ -1,7 +1,7 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { getSession, clearSession } from '@/common/utils/storage';
import { clearSession, getToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { NextLoading } from '@/common/utils/loading';
import { dynamicRoutes, staticRoutes, pathMatch } from './route';
@@ -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}']);
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 添加静态路由
@@ -29,8 +29,7 @@ const router = createRouter({
// 前端控制路由:初始化方法,防止刷新时丢失
export function initAllFun() {
NextLoading.start(); // 界面 loading 动画开始执行
const token = getSession('token'); // 获取浏览器缓存 token 值
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
@@ -38,18 +37,15 @@ export function initAllFun() {
useUserInfo().setUserInfo({});
router.addRoute(pathMatch); // 添加404界面
resetRoute(); // 删除/重置路由
// 添加动态路由
setFilterRouteEnd().forEach((route: any) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(dynamicRoutes[0]);
// 过滤权限菜单
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus));
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
// 后端控制路由:执行路由数据初始化
export async function initBackEndControlRoutesFun() {
NextLoading.start(); // 界面 loading 动画开始执行
const token = getSession('token'); // 获取浏览器缓存 token 值
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
@@ -57,14 +53,25 @@ export async function initBackEndControlRoutesFun() {
useUserInfo().setUserInfo({});
// 获取路由
let menuRoute = await getBackEndControlRoutes();
dynamicRoutes[0].children = backEndRouterConverter(menuRoute); // 处理路由component
const cacheList: Array<string> = [];
// 处理路由component
dynamicRoutes[0].children = backEndRouterConverter(menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false;
}
if (router.meta.isKeepAlive) {
cacheList.push(router.name);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
// 添加404界面
router.addRoute(pathMatch);
resetRoute(); // 删除/重置路由
// 添加动态路由
formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(dynamicRoutes[0] as unknown as RouteRecordRaw);
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
@@ -81,8 +88,10 @@ export async function getBackEndControlRoutes() {
}
}
type RouterConvCallbackFunc = (router: any) => void;
// 后端控制路由,后端返回路由 转换为vue route
export function backEndRouterConverter(routes: any, parentPath: string = '/') {
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) return;
return routes.map((item: any) => {
if (!item.meta) {
@@ -117,7 +126,9 @@ export function backEndRouterConverter(routes: any, parentPath: string = '/') {
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
}
item.children && backEndRouterConverter(item.children, item.path);
// 存在回调,则执行回调
callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
return item;
});
}
@@ -143,86 +154,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
}
}
// 多级嵌套数组处理成一维数组
export function formatFlatteningRoutes(arr: any) {
if (arr.length <= 0) return false;
for (let i = 0; i < arr.length; i++) {
if (arr[i].children) {
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
}
}
return arr;
}
// 多级嵌套数组处理后的一维数组,再处理成 `定义动态路由` 的格式
// 只保留二级也就是二级以上全部处理成只有二级keep-alive 支持二级缓存
// isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存
export function formatTwoStageRoutes(arr: any) {
if (arr.length <= 0) return false;
const newArr: any = [];
const cacheList: Array<string> = [];
arr.forEach((v: any) => {
if (v.path === '/') {
newArr.push({ component: v.component, name: v.name, path: v.path, redirect: v.redirect, meta: v.meta, children: [] });
} else {
newArr[0].children.push({ ...v });
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
cacheList.push(v.name);
}
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
return newArr;
}
// 判断路由code 是否包含当前登录用户menus字段中menus为字符串code数组
export function hasAnth(menus: any, route: any) {
if (route.meta && route.meta.code) {
return menus.includes(route.meta.code);
}
return true;
}
// 递归过滤有权限的路由
export function setFilterMenuFun(routes: any, menus: any) {
const menu: any = [];
routes.forEach((route: any) => {
const item = { ...route };
if (hasAnth(menus, item)) {
if (item.children) {
item.children = setFilterMenuFun(item.children, menus);
}
menu.push(item);
}
});
return menu;
}
// 获取当前用户的权限去比对路由表,用于动态路由的添加
export function setFilterRoute(chil: any) {
let filterRoute: any = [];
chil.forEach((route: any) => {
// 如果路由需要拥有指定code才可访问则校验该用户菜单是否存在该code
if (route.meta.code) {
useUserInfo().userInfo.menus.forEach((m: any) => {
if (route.meta.code == m) {
filterRoute.push({ ...route });
}
});
} else {
filterRoute.push({ ...route });
}
});
return filterRoute;
}
// 比对后的路由表,进行重新赋值
export function setFilterRouteEnd() {
let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
filterRouteEnd[0].children = setFilterRoute(filterRouteEnd[0].children);
return filterRouteEnd;
}
// 删除/重置路由
export function resetRoute() {
useRoutesList().routesList.forEach((route: any) => {
@@ -232,14 +163,19 @@ export function resetRoute() {
}
export async function initRouter() {
// 初始化方法执行
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
if (!isRequestRoutes) {
// 未开启后端控制路由
initAllFun();
} else if (isRequestRoutes) {
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
await initBackEndControlRoutesFun();
NextLoading.start(); // 界面 loading 动画开始执行
try {
// 初始化方法执行
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
if (!isRequestRoutes) {
// 未开启后端控制路由
initAllFun();
} else if (isRequestRoutes) {
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
await initBackEndControlRoutesFun();
}
} finally {
NextLoading.done();
}
}
@@ -252,12 +188,12 @@ router.beforeEach(async (to, from, next) => {
if (to.meta.title) NProgress.start();
// 如果有标题参数,则再原标题后加上参数来区别
if (to.meta.titleRename) {
if (to.meta.titleRename && to.meta.title) {
to.meta.title = templateResolve(to.meta.title as string, to.query);
}
const token = getSession('token');
if (to.path === '/login' && !token) {
const token = getToken();
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
next();
NProgress.done();
return;
@@ -270,7 +206,7 @@ router.beforeEach(async (to, from, next) => {
if (SysWs) {
SysWs.close();
SysWs = null;
SysWs = undefined;
}
return;
}
@@ -297,7 +233,6 @@ router.beforeEach(async (to, from, next) => {
// 路由加载后
router.afterEach(() => {
NProgress.done();
NextLoading.done();
});
// 导出路由

View File

@@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/views/layout/index.vue';
import Layout from '@/layout/index.vue';
// 定义动态路由
export const dynamicRoutes = [
@@ -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

@@ -18,14 +18,16 @@ export const useKeepALiveNames = defineStore('keepALiveNames', {
this.keepAliveNames = data;
},
async addCachedView(view: any) {
if (view.meta.isKeepAlive) this.cachedViews?.push(view.name);
if (view.isKeepAlive) {
this.cachedViews?.push(view.name);
}
},
async delCachedView(view: any) {
const index = this.cachedViews.indexOf(view.name);
index > -1 && this.cachedViews.splice(index, 1);
},
async delOthersCachedViews(view: any) {
if (view.meta.isKeepAlive) this.cachedViews = [view.name];
if (view.isKeepAlive) this.cachedViews = [view.name];
else this.cachedViews = [];
},
async delAllCachedViews() {

View File

@@ -0,0 +1,25 @@
import { getNowUrl } from '@/common/utils/url';
import { defineStore } from 'pinia';
/**
* tags view
*/
export const useTagsViews = defineStore('tagsViews', {
state: (): TagsViewsState => ({
tagsViews: [],
}),
actions: {
setTagsViews(data: Array<TagsView>) {
this.tagsViews = data;
},
// 设置当前页面的tags view title
setNowTitle(title: string) {
this.tagsViews.forEach((item) => {
// console.log(getNowUrl(), item.path);
if (item.path == getNowUrl()) {
item.title = title;
}
});
},
},
});

View File

@@ -1,4 +1,6 @@
import { defineStore } from 'pinia';
import { dateFormat2 } from '@/common/utils/date';
import { useUserInfo } from '@/store/userInfo';
export const useThemeConfig = defineStore('themeConfig', {
state: (): ThemeConfigState => ({
@@ -24,13 +26,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,14 +83,16 @@ export const useThemeConfig = defineStore('themeConfig', {
isSortableTagsView: true,
// 是否开启 Footer 底部版权信息
isFooter: false,
// 是否暗模式
isDark: false,
// 是否开启灰色模式
isGrayscale: false,
// 是否开启色弱模式
isInvert: false,
// 是否开启水印
isWartermark: false,
// 水印文案
wartermarkText: 'mayfly',
isWatermark: false,
// 水印文案数组0->用户信息 1->当前时间 2->额外信息
watermarkText: ['', '', ''],
/* 其它设置
------------------------------- */
@@ -138,5 +142,40 @@ export const useThemeConfig = defineStore('themeConfig', {
setThemeConfig(data: ThemeConfigState) {
this.themeConfig = data.themeConfig;
},
// 切换暗模式
switchDark(isDark: boolean) {
this.themeConfig.isDark = isDark;
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
this.themeConfig.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
this.themeConfig.editorTheme = 'SolarizedLight';
}
},
// 设置水印配置信息
setWatermarkConfig(useWatermarkConfig: any) {
this.themeConfig.watermarkText = [];
this.themeConfig.isWatermark = useWatermarkConfig.isUse;
if (!useWatermarkConfig.isUse) {
return;
}
// 索引2为用户自定义水印信息
this.themeConfig.watermarkText[2] = useWatermarkConfig.content;
},
// 设置水印用户信息
setWatermarkUser(del: boolean = false) {
const userinfo = useUserInfo().userInfo;
let desc = '';
if (!del && userinfo && userinfo.username) {
desc = `${userinfo.username}(${userinfo.name})`;
}
this.themeConfig.watermarkText[0] = desc;
},
// 设置水印时间为当前时间
setWatermarkNowTime() {
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
},
},
});

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { getSession } from '@/common/utils/storage';
import { getUser } from '@/common/utils/storage';
export const useUserInfo = defineStore('userInfo', {
state: (): UserInfoState => ({
@@ -8,7 +8,7 @@ export const useUserInfo = defineStore('userInfo', {
actions: {
// 设置用户信息
async setUserInfo(data: object) {
const ui = getSession('userInfo');
const ui = getUser();
if (ui) {
this.userInfo = ui;
} else {

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 {
@@ -344,4 +361,16 @@ 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

@@ -83,3 +83,42 @@
opacity: 1;
}
}
/* 登录页动画
------------------------------- */
@keyframes loginLeft {
0% {
left: -100%;
}
50%,
100% {
left: 100%;
}
}
@keyframes loginTop {
0% {
top: -100%;
}
50%,
100% {
top: 100%;
}
}
@keyframes loginRight {
0% {
right: -100%;
}
50%,
100% {
right: 100%;
}
}
@keyframes loginBottom {
0% {
bottom: -100%;
}
50%,
100% {
bottom: 100%;
}
}

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;
@@ -48,4 +48,4 @@
35% {
transform: scale3D(0, 0, 1);
}
}
}

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 {
@@ -53,4 +53,4 @@
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}
}

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() {
@@ -41,4 +53,4 @@
&::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}
}
}

View File

@@ -1,5 +1,5 @@
declare interface UserInfoState<T = any> {
userInfo: any
userInfo: any;
}
declare interface ThemeConfigState {
@@ -37,10 +37,11 @@ declare interface ThemeConfigState {
isCacheTagsView: boolean;
isSortableTagsView: boolean;
isFooter: boolean;
isDark: boolean;
isGrayscale: boolean;
isInvert: boolean;
isWartermark: boolean;
wartermarkText: string;
isWatermark: boolean;
watermarkText: Array<string>;
tagsStyle: string;
animation: string;
columnsAsideStyle: string;
@@ -59,10 +60,40 @@ declare interface ThemeConfigState {
};
}
declare interface TagsView {
/**
* 路径
*/
path: string;
/**
* 标题
*/
title: string;
/**
* router name
*/
name: string;
/**
* router query
*/
query: any;
/**
* 图标
*/
icon: string;
isAffix: boolean;
isKeepAlive: boolean;
isHide?: boolean;
}
// TagsView 路由列表
declare interface TagsViewRoutesState<T = any> {
tagsViewRoutes: T[];
isTagsViewCurrenFull: Boolean;
declare interface TagsViewsState<T = any> {
tagsViews: TagsView[];
}
// 路由列表
@@ -74,4 +105,4 @@ declare interface RoutesListState {
declare interface KeepAliveNamesState {
keepAliveNames: string[];
cachedViews: string[];
}
}

View File

@@ -1,14 +1,5 @@
/* eslint-disable */
import {IDisposable} from 'monaco-editor';
declare global {
interface Window {
completionItemProvider?: IDisposable | undefined;
}
}
// 申明外部 npm 插件模块
declare module 'sql-formatter';
declare module 'jsoneditor';
declare module 'asciinema-player';
declare module 'monaco-editor';
declare module 'vue-grid-layout';
declare module 'vue-grid-layout';

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="right">
<img src="@/assets/image/401.png" />
<img src="@/assets/image/401.svg" />
</div>
</div>
</div>
@@ -20,7 +20,7 @@
<script lang="ts">
import { useRouter } from 'vue-router';
import { clearSession } from '@/common/utils/storage.ts';
import { clearSession } from '@/common/utils/storage';
export default {
name: '401',
setup() {
@@ -39,7 +39,7 @@ export default {
<style scoped lang="scss">
.error {
height: 100%;
background-color: white;
background-color: var(--bg-main-color);
display: flex;
.error-flex {
margin: auto;
@@ -64,7 +64,7 @@ export default {
}
.left-item-title {
font-size: 20px;
color: #333333;
// color: #333333;
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="right">
<img src="@/assets/image/404.png" />
<img src="@/assets/image/404.svg" />
</div>
</div>
</div>
@@ -37,7 +37,7 @@ export default {
<style scoped lang="scss">
.error {
height: 100%;
background-color: white;
background-color: var(--bg-main-color);
display: flex;
.error-flex {
margin: auto;
@@ -62,7 +62,7 @@ export default {
}
.left-item-title {
font-size: 20px;
color: #333333;
// color: #333333;
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}

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

@@ -1,352 +0,0 @@
<template>
<div v-show="state.isShowLockScreen">
<div class="layout-lock-screen-mask"></div>
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
<div class="layout-lock-screen">
<div
class="layout-lock-screen-date"
ref="layoutLockScreenDateRef"
@mousedown="onDownPc"
@mousemove="onMovePc"
@mouseup="onEnd"
@touchstart.stop="onDownApp"
@touchmove.stop="onMoveApp"
@touchend.stop="onEnd"
>
<div class="layout-lock-screen-date-box">
<div class="layout-lock-screen-date-box-time">
{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
</div>
<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
</div>
<div class="layout-lock-screen-date-top">
<SvgIcon name="ele-Top" />
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
</div>
</div>
<transition name="el-zoom-in-center">
<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
<div class="layout-lock-screen-login-box">
<div class="layout-lock-screen-login-box-img">
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
</div>
<div class="layout-lock-screen-login-box-name">Administrator</div>
<div class="layout-lock-screen-login-box-value">
<el-input
placeholder="请输入密码"
ref="layoutLockScreenInputRef"
v-model="state.lockScreenPassword"
@keyup.enter.native.stop="onLockScreenSubmit()"
>
<template #append>
<el-button @click="onLockScreenSubmit">
<el-icon class="el-input__icon">
<ele-Right />
</el-icon>
</el-button>
</template>
</el-input>
</div>
</div>
<div class="layout-lock-screen-login-icon">
<SvgIcon name="ele-Microphone" :size="20" />
<SvgIcon name="ele-AlarmClock" :size="20" />
<SvgIcon name="ele-SwitchButton" :size="20" />
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup lang="ts" name="layoutLockScreen">
import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
import { formatDate } from '@/common/utils/format';
import { setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
// 定义变量内容
const layoutLockScreenDateRef = ref<null>();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
transparency: 1,
downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '' as any,
time: {
hm: '',
s: '',
mdq: '',
},
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下 pc
const onDownPc = (down: MouseEvent) => {
state.isFlags = true;
state.downClientY = down.clientY;
};
// 鼠标按下 app
const onDownApp = (down: TouchEvent) => {
state.isFlags = true;
state.downClientY = down.touches[0].clientY;
};
// 鼠标移动 pc
const onMovePc = (move: MouseEvent) => {
state.moveDifference = move.clientY - state.downClientY;
onMove();
};
// 鼠标移动 app
const onMoveApp = (move: TouchEvent) => {
state.moveDifference = move.touches[0].clientY - state.downClientY;
onMove();
};
// 鼠标移动事件
const onMove = () => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = layoutLockScreenDateRef.value;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
setLocal('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
});
</script>
<style scoped lang="scss">
.layout-lock-screen-fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layout-lock-screen-filter {
filter: blur(1px);
}
.layout-lock-screen-mask {
background: var(--el-color-white);
@extend .layout-lock-screen-fixed;
z-index: 9999990;
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background-image: url('@/assets/image/bg-login.png');
background-size: 100% 100%;
z-index: 9999991;
}
.layout-lock-screen {
@extend .layout-lock-screen-fixed;
z-index: 9999992;
&-date {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
color: var(--el-color-white);
z-index: 9999993;
user-select: none;
&-box {
position: absolute;
left: 30px;
bottom: 50px;
&-time {
font-size: 100px;
color: var(--el-color-white);
}
&-info {
font-size: 40px;
color: var(--el-color-white);
}
&-minutes {
font-size: 16px;
}
}
&-top {
width: 40px;
height: 40px;
line-height: 40px;
border-radius: 100%;
border: 1px solid var(--el-border-color-light, #ebeef5);
background: rgba(255, 255, 255, 0.1);
color: var(--el-color-white);
opacity: 0.8;
position: absolute;
right: 30px;
bottom: 50px;
text-align: center;
overflow: hidden;
transition: all 0.3s ease;
i {
transition: all 0.3s ease;
}
&-text {
opacity: 0;
position: absolute;
top: 150%;
font-size: 12px;
color: var(--el-color-white);
left: 50%;
line-height: 1.2;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
width: 35px;
}
&:hover {
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
color: var(--el-color-white);
opacity: 1;
transition: all 0.3s ease;
i {
transform: translateY(-40px);
transition: all 0.3s ease;
}
.layout-lock-screen-date-top-text {
opacity: 1;
top: 50%;
transition: all 0.3s ease;
}
}
}
}
&-login {
position: relative;
z-index: 9999994;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--el-color-white);
&-box {
text-align: center;
margin: auto;
&-img {
width: 180px;
height: 180px;
margin: auto;
img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
&-name {
font-size: 26px;
margin: 15px 0 30px;
}
}
&-icon {
position: absolute;
right: 30px;
bottom: 30px;
i {
font-size: 20px;
margin-left: 15px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
}
}
:deep(.el-input-group__append) {
background: var(--el-color-white);
padding: 0px 15px;
}
:deep(.el-input__inner) {
border-right-color: var(--el-border-color-extra-light);
&:hover {
border-color: var(--el-border-color-extra-light);
}
}
</style>

View File

@@ -1,138 +0,0 @@
<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"
>
<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)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
// 定义父组件传过来的值
const props = defineProps({
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
{
contextMenuClickId: 4,
txt: '当前页全屏',
affix: false,
icon: 'full-screen',
},
],
item: {} as any,
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.fullPath });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.meta?.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

View File

@@ -37,6 +37,9 @@
</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>
@@ -104,6 +107,23 @@
</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>
@@ -112,14 +132,18 @@ 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 { saveToken, saveUser } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { getAccountLoginSecurity, useWartermark } from '@/common/sysconfig';
import { getAccountLoginSecurity, getLdapEnabled } 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';
import { getToken } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -127,16 +151,20 @@ const rules = {
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
};
// 定义变量内容
const storesThemeConfig = useThemeConfig();
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({
accountLoginSecurity: {
useCaptcha: true,
useCaptcha: false,
useOtp: false,
loginFailCount: 5,
loginFailMin: 10,
@@ -148,7 +176,9 @@ const state = reactive({
password: '',
captcha: '',
cid: '',
ldapLogin: false,
},
loginRes: {} as any,
changePwdDialog: {
visible: false,
form: {
@@ -178,14 +208,34 @@ const state = reactive({
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 { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, loading } = toRefs(state);
const { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, baseInfoDialog, loading, ldapEnabled } = toRefs(state);
onMounted(async () => {
nextTick(async () => {
@@ -194,6 +244,10 @@ onMounted(async () => {
state.accountLoginSecurity = res;
}
getCaptcha();
const ldap = await getLdapEnabled();
state.ldapEnabled = ldap;
state.loginForm.ldapLogin = ldap;
});
// 移除公钥, 方便后续重新获取
sessionStorage.removeItem('RsaPublicKey');
@@ -248,7 +302,11 @@ const onSignIn = async () => {
try {
const loginReq = { ...state.loginForm };
loginReq.password = await RsaEncrypt(originPwd);
loginRes = await openApi.login(loginReq);
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 = '';
@@ -265,20 +323,43 @@ const onSignIn = async () => {
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,
};
// 存储用户信息到浏览器缓存
setUserInfo2Session(userInfos);
saveUser(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
useUserInfo().setUserInfo(userInfos);
@@ -299,10 +380,24 @@ const onSignIn = async () => {
// 登录成功后的跳转
const signInSuccess = async (accessToken: string = '') => {
if (!accessToken) {
accessToken = getToken();
}
// 存储 token 到浏览器缓存
setSession('token', accessToken);
saveToken(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;
// 登录成功,跳到转首页
@@ -314,9 +409,8 @@ const signInSuccess = async (accessToken: string = '') => {
// 关闭 loading
state.loading.signIn = true;
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
if (await useWartermark()) {
setUseWatermark2Session(true);
}
// 水印设置用户信息
storesThemeConfig.setWatermarkUser();
}, 300);
};
@@ -349,6 +443,10 @@ const cancelChangePwd = () => {
state.changePwdDialog.form.username = '';
getCaptcha();
};
defineExpose({
loginResDeal,
});
</script>
<style scoped lang="scss">

View File

@@ -1,128 +1,278 @@
<template>
<div class="login-container">
<div class="login-logo">
<span>{{ themeConfig.globalViceTitle }}</span>
<div class="login-container flex">
<div class="login-left">
<div class="login-left-logo">
<img :src="logoMini" />
<div class="login-left-logo-text">
<span>mayfly-go</span>
<!-- <span class="login-left-logo-text-msg">mayfly-go</span> -->
</div>
</div>
<div class="login-left-img">
<img :src="loginBgImg" />
</div>
<img :src="loginBgSplitImg" class="login-left-waves" />
</div>
<div class="login-content" :class="{ 'login-content-mobile': tabsActiveName === 'mobile' }">
<div class="login-content-main">
<h4 class="login-content-title">mayfly-go</h4>
<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" />
</transition>
</el-tab-pane>
<!-- <el-tab-pane label="手机号登录" name="mobile" :disabled="tabsActiveName === 'mobile'">
<transition name="el-zoom-in-center">
<Mobile v-show="!isTabPaneShow" />
</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="login-right flex">
<div class="login-right-warp flex-margin">
<span class="login-right-warp-one"></span>
<span class="login-right-warp-two"></span>
<div class="login-right-warp-mian">
<div class="login-right-warp-main-title">mayfly-go</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
<el-tab-pane label="账号密码登录" name="account">
<Account ref="loginForm" />
</el-tab-pane>
</el-tabs>
</div>
<div class="mt20" v-show="state.oauth2LoginConfig.enable">
<el-button link size="small">第三方登录: </el-button>
<el-tooltip :content="state.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>
</div>
<!-- <div class="login-copyright">
<div class="mb5 login-copyright-company">mayfly</div>
<div class="login-copyright-msg">mayfly</div>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive } from 'vue';
import Account from '@/views/login/component/AccountLogin.vue';
import { storeToRefs } from 'pinia';
<script setup lang="ts" name="loginIndex">
import { ref, defineAsyncComponent, onMounted, reactive } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
import logoMini from '@/assets/image/logo.svg';
import loginBgImg from '@/assets/image/login-bg-main.svg';
import loginBgSplitImg from '@/assets/image/login-bg-split.svg';
import openApi from '@/common/openApi';
import config from '@/common/config';
// 引入组件
const Account = defineAsyncComponent(() => import('./component/AccountLogin.vue'));
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(useThemeConfig());
const state = reactive({
tabsActiveName: 'account',
isTabPaneShow: true,
isScan: false,
oauth2LoginConfig: {
name: 'OAuth2登录',
enable: false,
},
});
const { isTabPaneShow, tabsActiveName } = toRefs(state);
onMounted(async () => {
storesThemeConfig.setWatermarkUser(true);
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
});
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
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">
.login-container {
width: 100%;
height: 100%;
background: url('@/assets/image/bg-login.png') no-repeat;
background-size: 100% 100%;
.login-logo {
position: absolute;
top: 30px;
left: 50%;
height: 50px;
display: flex;
align-items: center;
font-size: 20px;
color: var(--color-primary);
letter-spacing: 2px;
width: 90%;
transform: translateX(-50%);
}
.login-content {
width: 500px;
padding: 20px;
position: absolute;
top: 50%;
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);
border-radius: 4px;
transition: height 0.2s linear;
height: 480px;
overflow: hidden;
z-index: 1;
.login-content-main {
margin: 0 auto;
width: 80%;
.login-content-title {
color: #333;
font-weight: 500;
font-size: 22px;
text-align: center;
letter-spacing: 4px;
margin: 15px 0 30px;
white-space: nowrap;
background: var(--bg-main-color);
.login-left {
flex: 1;
position: relative;
background-color: rgba(211, 239, 255, 1);
margin-right: 100px;
.login-left-logo {
display: flex;
align-items: center;
position: absolute;
top: 50px;
left: 80px;
z-index: 1;
animation: logoAnimation 0.3s ease;
img {
width: 52px;
height: 52px;
}
.login-left-logo-text {
display: flex;
flex-direction: column;
span {
margin-left: 10px;
font-size: 28px;
color: #26a59a;
}
.login-left-logo-text-msg {
font-size: 12px;
color: #32a99e;
}
}
}
}
.login-content-mobile {
height: 418px;
}
.login-copyright {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 30px;
text-align: center;
color: white;
font-size: 12px;
opacity: 0.8;
.login-copyright-company {
white-space: nowrap;
.login-left-img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 52%;
img {
width: 100%;
height: 100%;
animation: error-num 0.6s ease;
}
}
.login-copyright-msg {
@extend .login-copyright-company;
.login-left-waves {
position: absolute;
top: 0;
right: -100px;
}
}
.login-right {
width: 700px;
.login-right-warp {
border: 1px solid var(--el-color-primary-light-3);
border-radius: 3px;
width: 500px;
height: 500px;
position: relative;
overflow: hidden;
background-color: var(--bg-main-color);
.login-right-warp-one,
.login-right-warp-two {
position: absolute;
display: block;
width: inherit;
height: inherit;
&::before,
&::after {
content: '';
position: absolute;
z-index: 1;
}
}
.login-right-warp-one {
&::before {
filter: hue-rotate(0deg);
top: 0px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--el-color-primary));
animation: loginLeft 3s linear infinite;
}
&::after {
filter: hue-rotate(60deg);
top: -100%;
right: 2px;
width: 3px;
height: 100%;
background: linear-gradient(180deg, transparent, var(--el-color-primary));
animation: loginTop 3s linear infinite;
animation-delay: 0.7s;
}
}
.login-right-warp-two {
&::before {
filter: hue-rotate(120deg);
bottom: 2px;
right: -100%;
width: 100%;
height: 3px;
background: linear-gradient(270deg, transparent, var(--el-color-primary));
animation: loginRight 3s linear infinite;
animation-delay: 1.4s;
}
&::after {
filter: hue-rotate(300deg);
bottom: -100%;
left: 0px;
width: 3px;
height: 100%;
background: linear-gradient(360deg, transparent, var(--el-color-primary));
animation: loginBottom 3s linear infinite;
animation-delay: 2.1s;
}
}
.login-right-warp-mian {
display: flex;
flex-direction: column;
height: 100%;
.login-right-warp-main-title {
height: 110px;
line-height: 110px;
font-size: 27px;
text-align: center;
letter-spacing: 3px;
animation: logoAnimation 0.3s ease;
animation-delay: 0.3s;
color: var(--el-text-color-primary);
}
.login-right-warp-main-form {
flex: 1;
padding: 0 50px 50px;
.login-content-main-sacn {
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
overflow: hidden;
cursor: pointer;
transition: all ease 0.3s;
color: var(--el-color-primary);
&-delta {
position: absolute;
width: 35px;
height: 70px;
z-index: 2;
top: 2px;
right: 21px;
background: var(--el-color-white);
transform: rotate(-45deg);
}
&:hover {
opacity: 1;
transition: all ease 0.3s;
color: var(--el-color-primary) !important;
}
i {
width: 47px;
height: 50px;
display: inline-block;
font-size: 48px;
position: absolute;
right: 1px;
top: 0px;
}
}
}
}
}
}
}

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>

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