mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-28 03:50:25 +08:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2118acf244 | ||
|
|
44a1bd626e | ||
|
|
ea3c70a8a8 | ||
|
|
6343173cf8 | ||
|
|
6837a9c867 | ||
|
|
a726927a28 | ||
|
|
e135e4ce64 | ||
|
|
43edef412c | ||
|
|
2deb3109c2 | ||
|
|
a80221a950 | ||
|
|
10630847df | ||
|
|
f43851698e | ||
|
|
73884bb693 | ||
|
|
1b5bb1de8b | ||
|
|
4814793546 | ||
|
|
d85bbff270 | ||
|
|
bb1522f4dc | ||
|
|
a7632fbf58 | ||
|
|
c4cb4234fd | ||
|
|
89e12678eb | ||
|
|
137ebb8e9e | ||
|
|
05625bd8c1 | ||
|
|
4afeac5fdd | ||
|
|
1d0e91f1af | ||
|
|
cf5111a325 | ||
|
|
78957a8ebd | ||
|
|
4ed892a656 | ||
|
|
3486b07003 | ||
|
|
a5cd7caf19 | ||
|
|
f2c7ef78c0 | ||
|
|
653953ee76 | ||
|
|
a831614d5a | ||
|
|
ebe73e2f19 | ||
|
|
29fd5a25d2 | ||
|
|
44805ce580 | ||
|
|
2a6d620830 | ||
|
|
01d3e1ad28 | ||
|
|
f4162c38db | ||
|
|
1a4626c24d | ||
|
|
d6eb9683d1 | ||
|
|
e2b524dadb | ||
|
|
8998a21626 | ||
|
|
abc015aec0 | ||
|
|
4ef8d27b1e | ||
|
|
40b6e603fc | ||
|
|
21498584b1 | ||
|
|
408bac09a1 | ||
|
|
582d879a77 | ||
|
|
38ff5152e0 | ||
|
|
d1d372e1bf | ||
|
|
5e4793433b | ||
|
|
54ad19f97e | ||
|
|
fc166650b3 | ||
|
|
2acc295259 | ||
|
|
4b3ed1310d | ||
|
|
b2cfd1517c | ||
|
|
b13d27ccd6 | ||
|
|
68e0088016 | ||
|
|
bd1e83989d | ||
|
|
263dfa6be7 | ||
|
|
eb55f93864 | ||
|
|
8589105e44 | ||
|
|
986b187f0a | ||
|
|
008d34c453 | ||
|
|
49d3f988c9 | ||
|
|
76475e807e | ||
|
|
f93231da61 | ||
|
|
bf75483a3c | ||
|
|
b56b0187cf | ||
|
|
7e7f02b502 | ||
|
|
878985f7c5 | ||
|
|
2133d9b737 | ||
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc | ||
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 |
@@ -5,12 +5,12 @@ WORKDIR /mayfly
|
|||||||
|
|
||||||
COPY mayfly_go_web .
|
COPY mayfly_go_web .
|
||||||
|
|
||||||
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
|
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
# 构建后端资源
|
# 构建后端资源
|
||||||
FROM golang:1.21.5 as be-builder
|
FROM golang:1.23 as be-builder
|
||||||
|
|
||||||
ENV GOPROXY https://goproxy.cn
|
ENV GOPROXY https://goproxy.cn
|
||||||
WORKDIR /mayfly
|
WORKDIR /mayfly
|
||||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||||
go build -a \
|
go build -a -ldflags=-w \
|
||||||
-o mayfly-go main.go
|
-o mayfly-go main.go
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -13,7 +13,7 @@
|
|||||||
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/golang/go" target="_blank">
|
<a href="https://github.com/golang/go" target="_blank">
|
||||||
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
|
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://cn.vuejs.org" target="_blank">
|
<a href="https://cn.vuejs.org" target="_blank">
|
||||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
### 介绍
|
### 介绍
|
||||||
|
|
||||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||||
|
|
||||||
### 开发语言与主要框架
|
### 开发语言与主要框架
|
||||||
|
|
||||||
@@ -45,56 +45,61 @@ http://go.mayfly.run
|
|||||||
|
|
||||||
### 系统核心功能截图
|
### 系统核心功能截图
|
||||||
|
|
||||||
##### 记录操作记录
|
#### 首页
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 机器操作
|
#### 机器操作
|
||||||
|
|
||||||
##### 状态查看
|
##### 状态查看
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### ssh 终端
|
##### ssh 终端
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 文件操作
|
##### 文件操作
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||
|

|
||||||
|
|
||||||
#### 数据库操作
|
#### 数据库操作
|
||||||
|
|
||||||
##### sql 编辑器
|
##### sql 编辑器
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 在线增删改查数据
|
##### 在线增删改查数据
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Redis 操作
|
#### Redis 操作
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Mongo 操作
|
#### Mongo 操作
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 系统管理
|
#### 工单流程审批
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 系统管理
|
||||||
|
|
||||||
##### 账号管理
|
##### 账号管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 角色管理
|
##### 角色管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 资源管理
|
##### 菜单资源管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go
|
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: mayfly-go:v1.3.1
|
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ VITE_PORT = 8889
|
|||||||
VITE_OPEN = false
|
VITE_OPEN = false
|
||||||
|
|
||||||
# public path 配置线上环境路径(打包)
|
# public path 配置线上环境路径(打包)
|
||||||
VITE_PUBLIC_PATH = ''
|
VITE_PUBLIC_PATH = ''
|
||||||
|
|
||||||
|
VITE_EDITOR=idea
|
||||||
@@ -1,66 +1,72 @@
|
|||||||
{
|
{
|
||||||
"name": "mayfly",
|
"name": "mayfly",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"type": "module",
|
||||||
"dev": "vite",
|
"scripts": {
|
||||||
"build": "vite build",
|
"dev": "vite",
|
||||||
"preview": "vite preview",
|
"build": "vite build",
|
||||||
"build-preview": "npm run build && npm run preview",
|
"preview": "vite preview",
|
||||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
"build-preview": "npm run build && npm run preview",
|
||||||
},
|
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||||
"dependencies": {
|
},
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.7.2",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"asciinema-player": "^3.6.3",
|
"@vueuse/core": "^11.1.0",
|
||||||
"axios": "^1.6.2",
|
"asciinema-player": "^3.8.1",
|
||||||
"clipboard": "^2.0.11",
|
"axios": "^1.6.2",
|
||||||
"countup.js": "^2.7.0",
|
"clipboard": "^2.0.11",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.6.1",
|
||||||
"echarts": "^5.4.3",
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.5.1",
|
"dayjs": "^1.11.13",
|
||||||
"js-base64": "^3.7.5",
|
"echarts": "^5.5.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"element-plus": "^2.8.6",
|
||||||
"lodash": "^4.17.21",
|
"js-base64": "^3.7.7",
|
||||||
"mitt": "^3.0.1",
|
"jsencrypt": "^3.3.2",
|
||||||
"monaco-editor": "^0.45.0",
|
"lodash": "^4.17.21",
|
||||||
"monaco-sql-languages": "^0.11.0",
|
"mitt": "^3.0.1",
|
||||||
"monaco-themes": "^0.4.4",
|
"monaco-editor": "^0.52.0",
|
||||||
"nprogress": "^0.2.0",
|
"monaco-sql-languages": "^0.12.2",
|
||||||
"pinia": "^2.1.7",
|
"monaco-themes": "^0.4.4",
|
||||||
"qrcode.vue": "^3.4.1",
|
"nprogress": "^0.2.0",
|
||||||
"screenfull": "^6.0.2",
|
"pinia": "^2.2.4",
|
||||||
"sortablejs": "^1.15.0",
|
"qrcode.vue": "^3.5.0",
|
||||||
"splitpanes": "^3.1.5",
|
"screenfull": "^6.0.2",
|
||||||
"sql-formatter": "^15.0.2",
|
"sortablejs": "^1.15.3",
|
||||||
"uuid": "^9.0.1",
|
"splitpanes": "^3.1.5",
|
||||||
"vue": "^3.4.14",
|
"sql-formatter": "^15.4.5",
|
||||||
"vue-router": "^4.2.5",
|
"trzsz": "^1.1.5",
|
||||||
"xterm": "^5.3.0",
|
"uuid": "^9.0.1",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"vue": "^3.5.12",
|
||||||
"xterm-addon-search": "^0.13.0",
|
"vue-router": "^4.4.5",
|
||||||
"xterm-addon-web-links": "^0.9.0"
|
"xterm": "^5.3.0",
|
||||||
},
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"devDependencies": {
|
"xterm-addon-search": "^0.13.0",
|
||||||
"@types/lodash": "^4.14.178",
|
"xterm-addon-web-links": "^0.9.0"
|
||||||
"@types/node": "^18.14.0",
|
},
|
||||||
"@types/nprogress": "^0.2.0",
|
"devDependencies": {
|
||||||
"@types/sortablejs": "^1.15.3",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@types/lodash": "^4.14.178",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@types/node": "^18.14.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@vue/compiler-sfc": "^3.4.14",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"dotenv": "^16.3.1",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"eslint": "^8.35.0",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"prettier": "^3.1.0",
|
"@vue/compiler-sfc": "^3.5.12",
|
||||||
"sass": "^1.69.0",
|
"code-inspector-plugin": "^0.4.5",
|
||||||
"typescript": "^5.3.2",
|
"dotenv": "^16.3.1",
|
||||||
"vite": "^5.0.11",
|
"eslint": "^8.35.0",
|
||||||
"vue-eslint-parser": "^9.4.0"
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
},
|
"prettier": "^3.2.5",
|
||||||
"browserslist": [
|
"sass": "^1.80.3",
|
||||||
"> 1%",
|
"typescript": "^5.6.3",
|
||||||
"last 2 versions",
|
"vite": "^5.4.10",
|
||||||
"not dead"
|
"vue-eslint-parser": "^9.4.3"
|
||||||
]
|
},
|
||||||
}
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
:zIndex="10000000"
|
:zIndex="10000000"
|
||||||
:width="210"
|
:width="210"
|
||||||
v-if="themeConfig.isWatermark"
|
v-if="themeConfig.isWatermark"
|
||||||
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
|
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||||
:content="themeConfig.watermarkText"
|
:content="themeConfig.watermarkText"
|
||||||
class="h100"
|
class="h100"
|
||||||
>
|
>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -55,11 +55,11 @@
|
|||||||
"unicode_decimal": 58905
|
"unicode_decimal": 58905
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "11617944",
|
"icon_id": "25271976",
|
||||||
"name": "oracle",
|
"name": "oracle",
|
||||||
"font_class": "oracle",
|
"font_class": "oracle",
|
||||||
"unicode": "e6ea",
|
"unicode": "e507",
|
||||||
"unicode_decimal": 59114
|
"unicode_decimal": 58631
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "8105644",
|
"icon_id": "8105644",
|
||||||
@@ -67,6 +67,41 @@
|
|||||||
"font_class": "mariadb",
|
"font_class": "mariadb",
|
||||||
"unicode": "e513",
|
"unicode": "e513",
|
||||||
"unicode_decimal": 58643
|
"unicode_decimal": 58643
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "13601813",
|
||||||
|
"name": "sqlite",
|
||||||
|
"font_class": "sqlite",
|
||||||
|
"unicode": "e546",
|
||||||
|
"unicode_decimal": 58694
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "29340317",
|
||||||
|
"name": "temp-mssql",
|
||||||
|
"font_class": "MSSQLNATIVE",
|
||||||
|
"unicode": "e600",
|
||||||
|
"unicode_decimal": 58880
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7699332",
|
||||||
|
"name": "gaussdb",
|
||||||
|
"font_class": "gauss",
|
||||||
|
"unicode": "e683",
|
||||||
|
"unicode_decimal": 59011
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "34836637",
|
||||||
|
"name": "kingbase",
|
||||||
|
"font_class": "kingbase",
|
||||||
|
"unicode": "e882",
|
||||||
|
"unicode_decimal": 59522
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "33047500",
|
||||||
|
"name": "vastbase",
|
||||||
|
"font_class": "vastbase",
|
||||||
|
"unicode": "e62b",
|
||||||
|
"unicode_decimal": 58923
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class Api {
|
|||||||
*/
|
*/
|
||||||
async xhrReq(param: any = null, options: any = {}): Promise<any> {
|
async xhrReq(param: any = null, options: any = {}): Promise<any> {
|
||||||
if (this.beforeHandler) {
|
if (this.beforeHandler) {
|
||||||
this.beforeHandler(param);
|
await this.beforeHandler(param);
|
||||||
}
|
}
|
||||||
return request.xhrReq(this.method, this.url, param, options);
|
return request.xhrReq(this.method, this.url, param, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export class EnumValue {
|
|||||||
*/
|
*/
|
||||||
tag: EnumValueTag;
|
tag: EnumValueTag;
|
||||||
|
|
||||||
|
extra: any;
|
||||||
|
|
||||||
constructor(value: any, label: string) {
|
constructor(value: any, label: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
@@ -53,6 +55,11 @@ export class EnumValue {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExtra(extra: any): EnumValue {
|
||||||
|
this.extra = extra;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public static of(value: any, label: string): EnumValue {
|
public static of(value: any, label: string): EnumValue {
|
||||||
return new EnumValue(value, label);
|
return new EnumValue(value, label);
|
||||||
}
|
}
|
||||||
@@ -60,11 +67,12 @@ export class EnumValue {
|
|||||||
/**
|
/**
|
||||||
* 根据枚举值获取指定枚举值对象
|
* 根据枚举值获取指定枚举值对象
|
||||||
*
|
*
|
||||||
* @param enumValues 所有枚举值
|
* @param enums 枚举对象
|
||||||
* @param value 需要匹配的枚举值
|
* @param value 需要匹配的枚举值
|
||||||
* @returns 枚举值对象
|
* @returns 枚举值对象
|
||||||
*/
|
*/
|
||||||
static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null {
|
static getEnumByValue(enums: any, value: any): EnumValue | null {
|
||||||
|
const enumValues = Object.values(enums) as any;
|
||||||
for (let enumValue of enumValues) {
|
for (let enumValue of enumValues) {
|
||||||
if (enumValue.value == value) {
|
if (enumValue.value == value) {
|
||||||
return enumValue;
|
return enumValue;
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import EnumValue from './Enum';
|
import EnumValue from './Enum';
|
||||||
|
|
||||||
|
// 资源类型
|
||||||
|
export const ResourceTypeEnum = {
|
||||||
|
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||||
|
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
|
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||||
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||||
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
export const TagResourceTypeEnum = {
|
export const TagResourceTypeEnum = {
|
||||||
Machine: EnumValue.of(1, '机器'),
|
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
|
||||||
Db: EnumValue.of(2, '数据库'),
|
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
|
||||||
Redis: EnumValue.of(3, 'redis'),
|
|
||||||
Mongo: EnumValue.of(4, 'mongo'),
|
Machine: ResourceTypeEnum.Machine,
|
||||||
|
Db: ResourceTypeEnum.Db,
|
||||||
|
Redis: ResourceTypeEnum.Redis,
|
||||||
|
Mongo: ResourceTypeEnum.Mongo,
|
||||||
|
|
||||||
|
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
|
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
|
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.7.0',
|
version: 'v1.9.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
38
mayfly_go_web/src/common/crypto.ts
Normal file
38
mayfly_go_web/src/common/crypto.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
import { getToken } from '@/common/utils/storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES 加密数据
|
||||||
|
* @param word
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export function AesEncrypt(word: string, key?: string) {
|
||||||
|
if (!key) {
|
||||||
|
key = getToken().substring(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(word, sKey, {
|
||||||
|
iv: sKey,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7,
|
||||||
|
});
|
||||||
|
|
||||||
|
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AesDecrypt(word: string, key?: string): string {
|
||||||
|
if (!key) {
|
||||||
|
key = getToken().substring(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
// key 和 iv 使用同一个值
|
||||||
|
const decrypted = CryptoJS.AES.decrypt(word, sKey, {
|
||||||
|
iv: sKey,
|
||||||
|
mode: CryptoJS.mode.CBC, // CBC算法
|
||||||
|
padding: CryptoJS.pad.Pkcs7, //使用pkcs7 进行padding 后端需要注意
|
||||||
|
});
|
||||||
|
|
||||||
|
return decrypted.toString(CryptoJS.enc.Base64);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import request from './request';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
login: (param: any) => request.post('/auth/accounts/login', param),
|
login: (param: any) => request.post('/auth/accounts/login', param),
|
||||||
|
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
|
||||||
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
||||||
getPublicKey: () => request.get('/common/public-key'),
|
getPublicKey: () => request.get('/common/public-key'),
|
||||||
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
||||||
@@ -13,4 +14,5 @@ export default {
|
|||||||
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
||||||
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
|
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
|
||||||
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
||||||
|
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const AccountUsernamePattern = {
|
export const AccountUsernamePattern = {
|
||||||
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
|
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
|
||||||
message: '只允许输入5-20位大小写字母、数字、下划线',
|
message: '只允许输入5-20位大小写字母、数字、_-.:',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResourceCodePattern = {
|
||||||
|
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
|
||||||
|
message: '只允许输入1-32位大小写字母、数字、_-.:',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export enum ResultEnum {
|
|||||||
PARAM_ERROR = 405,
|
PARAM_ERROR = 405,
|
||||||
SERVER_ERROR = 500,
|
SERVER_ERROR = 500,
|
||||||
NO_PERMISSION = 501,
|
NO_PERMISSION = 501,
|
||||||
|
ACCESS_TOKEN_INVALID = 502, // accessToken失效
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseUrl: string = config.baseApiUrl;
|
export const baseUrl: string = config.baseApiUrl;
|
||||||
@@ -208,6 +209,36 @@ export function joinClientParams(): string {
|
|||||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件url地址
|
||||||
|
* @param key 文件key
|
||||||
|
* @returns 文件url
|
||||||
|
*/
|
||||||
|
export function getFileUrl(key: string) {
|
||||||
|
return `${baseUrl}/sys/files/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统文件上传url
|
||||||
|
* @param key 文件key
|
||||||
|
* @returns 文件上传url
|
||||||
|
*/
|
||||||
|
export function getUploadFileUrl(key: string = '') {
|
||||||
|
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
* @param key 文件key
|
||||||
|
*/
|
||||||
|
export function downloadFile(key: string) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('href', `${getFileUrl(key)}`);
|
||||||
|
a.setAttribute('target', '_blank');
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function parseResult(result: Result) {
|
function parseResult(result: Result) {
|
||||||
if (result.code === ResultEnum.SUCCESS) {
|
if (result.code === ResultEnum.SUCCESS) {
|
||||||
return result.data;
|
return result.data;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Config from './config';
|
import Config from './config';
|
||||||
import { ElNotification } from 'element-plus';
|
import {ElNotification} from 'element-plus';
|
||||||
import SocketBuilder from './SocketBuilder';
|
import SocketBuilder from './SocketBuilder';
|
||||||
import { getToken } from '@/common/utils/storage';
|
import {getToken} from '@/common/utils/storage';
|
||||||
|
|
||||||
import { joinClientParams } from './request';
|
import {joinClientParams} from './request';
|
||||||
|
|
||||||
class SysSocket {
|
class SysSocket {
|
||||||
/**
|
/**
|
||||||
@@ -19,10 +19,11 @@ class SysSocket {
|
|||||||
/**
|
/**
|
||||||
* 消息类型
|
* 消息类型
|
||||||
*/
|
*/
|
||||||
messageTypes = {
|
messageTypes: any = {
|
||||||
0: 'error',
|
0: 'error',
|
||||||
1: 'success',
|
1: 'success',
|
||||||
2: 'info',
|
2: 'info',
|
||||||
|
22: 'info',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,12 +58,20 @@ class SysSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = this.getMsgType(message.type);
|
const type = this.getMsgType(message.type);
|
||||||
|
let msg = message.msg
|
||||||
|
let duration = 0
|
||||||
|
if (message.type == 22) {
|
||||||
|
let obj = JSON.parse(msg);
|
||||||
|
msg = `文件:${obj['title']} 执行成功: ${obj['executedStatements']} 条`
|
||||||
|
duration = 2000
|
||||||
|
}
|
||||||
ElNotification({
|
ElNotification({
|
||||||
duration: 0,
|
duration: duration,
|
||||||
title: message.title,
|
title: message.title,
|
||||||
message: message.msg,
|
message: msg,
|
||||||
type: type,
|
type: type,
|
||||||
});
|
});
|
||||||
|
console.log(message)
|
||||||
})
|
})
|
||||||
.open((event: any) => console.log(event))
|
.open((event: any) => console.log(event))
|
||||||
.close(() => {
|
.close(() => {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
export function dateFormat2(fmt: string, date: Date) {
|
|
||||||
let ret;
|
|
||||||
const opt = {
|
|
||||||
'y+': date.getFullYear().toString(), // 年
|
|
||||||
'M+': (date.getMonth() + 1).toString(), // 月
|
|
||||||
'd+': date.getDate().toString(), // 日
|
|
||||||
'H+': date.getHours().toString(), // 时
|
|
||||||
'm+': date.getMinutes().toString(), // 分
|
|
||||||
's+': date.getSeconds().toString(), // 秒
|
|
||||||
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
|
|
||||||
// 有其他格式化字符需求可以继续添加,必须转化成字符串
|
|
||||||
};
|
|
||||||
for (const k in opt) {
|
|
||||||
ret = new RegExp('(' + k + ')').exec(fmt);
|
|
||||||
if (ret) {
|
|
||||||
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateStrFormat(fmt: string, dateStr: string) {
|
|
||||||
return dateFormat2(fmt, new Date(dateStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateFormat(dateStr: string) {
|
|
||||||
if (!dateStr) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
|
|
||||||
}
|
|
||||||
@@ -7,24 +7,22 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
|||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
let val: any = data[column];
|
let val: any = data[column];
|
||||||
if (val == null || val == undefined) {
|
if (val == null || val == undefined) {
|
||||||
dataValueArr.push('');
|
val = '';
|
||||||
continue;
|
} else if (val && typeof val == 'string') {
|
||||||
}
|
// 替换换行符
|
||||||
|
val = val.replace(/[\r\n]/g, '\\n');
|
||||||
|
|
||||||
if (typeof val == 'string' && val) {
|
|
||||||
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
||||||
if (val.indexOf(',') != -1) {
|
if (val.indexOf(',') != -1) {
|
||||||
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
||||||
if (val.indexOf('"') != -1) {
|
if (val.indexOf('"') != -1) {
|
||||||
val = val.replace(/\"/g, '""');
|
val = val.replace(/"/g, '""');
|
||||||
}
|
}
|
||||||
// 再将逗号转义
|
// 再将逗号转义
|
||||||
val = `"${val}"`;
|
val = `"${val}"`;
|
||||||
}
|
}
|
||||||
dataValueArr.push(val + '\t');
|
|
||||||
} else {
|
|
||||||
dataValueArr.push(val + '\t');
|
|
||||||
}
|
}
|
||||||
|
dataValueArr.push(String(val));
|
||||||
}
|
}
|
||||||
cvsData.push(dataValueArr);
|
cvsData.push(dataValueArr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
* @param date 日期 字符串 Date 时间戳等
|
||||||
|
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
|
||||||
|
* @returns 格式化后内容
|
||||||
|
*/
|
||||||
|
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
|
if (!date) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return dayjs(date).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化字节单位
|
* 格式化字节单位
|
||||||
* @param size byte size
|
* @param size byte size
|
||||||
@@ -47,161 +62,42 @@ export function convertToBytes(sizeStr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化json字符串
|
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
||||||
* @param txt json字符串
|
*
|
||||||
* @param compress 是否压缩
|
* @param time 时间数
|
||||||
* @returns 格式化后的字符串
|
* @param unit time对应的单位
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function formatJsonString(txt: string, compress: boolean) {
|
export function formatTime(time: number, unit: string = 's') {
|
||||||
var indentChar = ' ';
|
const units = {
|
||||||
if (/^\s*$/.test(txt)) {
|
y: 31536000,
|
||||||
console.log('数据为空,无法格式化! ');
|
M: 2592000,
|
||||||
return txt;
|
d: 86400,
|
||||||
}
|
h: 3600,
|
||||||
try {
|
m: 60,
|
||||||
var data = JSON.parse(txt);
|
s: 1,
|
||||||
} catch (e: any) {
|
};
|
||||||
console.log('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err');
|
|
||||||
return txt;
|
|
||||||
}
|
|
||||||
var draw: any = [],
|
|
||||||
line = compress ? '' : '\n',
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
nodeCount: number = 0,
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
maxDepth: number = 0;
|
|
||||||
|
|
||||||
var notify = function (name: any, value: any, isLast: any, indent: any, formObj: any) {
|
if (!units[unit]) {
|
||||||
nodeCount++; /*节点计数*/
|
return 'Invalid unit';
|
||||||
for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
|
}
|
||||||
tab = compress ? '' : tab; /*压缩模式忽略缩进*/
|
|
||||||
maxDepth = ++indent; /*缩进递增并记录*/
|
let seconds = time * units[unit];
|
||||||
if (value && value.constructor == Array) {
|
let result = '';
|
||||||
/*处理数组*/
|
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
|
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||||
for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
|
const value = Math.floor(seconds / duration);
|
||||||
draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
|
seconds %= duration;
|
||||||
} else if (value && typeof value == 'object') {
|
return { value, unit };
|
||||||
/*处理对象*/
|
});
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
|
|
||||||
var len = 0,
|
timeUnits.forEach(({ value, unit }) => {
|
||||||
i = 0;
|
if (value > 0) {
|
||||||
for (var key in value) len++;
|
result += `${value}${unit} `;
|
||||||
for (var key in value) notify(key, value[key], ++i == len, indent, true);
|
|
||||||
draw.push(tab + '}' + (isLast ? line : ',' + line)); /*缩进'}'换行,若非尾元素则添加逗号*/
|
|
||||||
} else {
|
|
||||||
if (typeof value == 'string') value = '"' + value + '"';
|
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + value + (isLast ? '' : ',') + line);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
var isLast = true,
|
|
||||||
indent = 0;
|
|
||||||
notify('', data, isLast, indent, false);
|
|
||||||
return draw.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
return result;
|
||||||
* 年(Y) 可用1-4个占位符
|
|
||||||
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
|
|
||||||
* 星期(W) 可用1-3个占位符
|
|
||||||
* 季度(q为阿拉伯数字,Q为中文数字)可用1或4个占位符
|
|
||||||
*
|
|
||||||
* let date = new Date()
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS") // 2020-02-09 14:04:23
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS Q") // 2020-02-09 14:09:03 一
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW") // 2020-02-09 14:45:12 星期日
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS QQQQ") // 2020-02-09 14:09:36 第一季度
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-02-09 14:46:12 星期日 第一季度
|
|
||||||
*/
|
|
||||||
export function formatDate(date: Date, format: string) {
|
|
||||||
let we = date.getDay(); // 星期
|
|
||||||
let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
|
|
||||||
const opt: any = {
|
|
||||||
'Y+': date.getFullYear().toString(), // 年
|
|
||||||
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
|
|
||||||
'd+': date.getDate().toString(), // 日
|
|
||||||
'H+': date.getHours().toString(), // 时
|
|
||||||
'M+': date.getMinutes().toString(), // 分
|
|
||||||
'S+': date.getSeconds().toString(), // 秒
|
|
||||||
'q+': qut, // 季度
|
|
||||||
};
|
|
||||||
// 中文数字 (星期)
|
|
||||||
const week: any = {
|
|
||||||
'0': '日',
|
|
||||||
'1': '一',
|
|
||||||
'2': '二',
|
|
||||||
'3': '三',
|
|
||||||
'4': '四',
|
|
||||||
'5': '五',
|
|
||||||
'6': '六',
|
|
||||||
};
|
|
||||||
// 中文数字(季度)
|
|
||||||
const quarter: any = {
|
|
||||||
'1': '一',
|
|
||||||
'2': '二',
|
|
||||||
'3': '三',
|
|
||||||
'4': '四',
|
|
||||||
};
|
|
||||||
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
|
|
||||||
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
|
|
||||||
for (let k in opt) {
|
|
||||||
let r = new RegExp('(' + k + ')').exec(format);
|
|
||||||
// 若输入的长度不为1,则前面补零
|
|
||||||
if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
|
|
||||||
}
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 10秒: 10 * 1000
|
|
||||||
* 1分: 60 * 1000
|
|
||||||
* 1小时: 60 * 60 * 1000
|
|
||||||
* 24小时:60 * 60 * 24 * 1000
|
|
||||||
* 3天: 60 * 60* 24 * 1000 * 3
|
|
||||||
*
|
|
||||||
* let data = new Date()
|
|
||||||
* formatPast(data) // 刚刚
|
|
||||||
* formatPast(data - 11 * 1000) // 11秒前
|
|
||||||
* formatPast(data - 2 * 60 * 1000) // 2分钟前
|
|
||||||
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
|
|
||||||
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
|
|
||||||
* formatPast(data - 60 * 60 * 71 * 1000) // 2天前
|
|
||||||
* formatPast("2020-06-01") // 2020-06-01
|
|
||||||
* formatPast("2020-06-01", "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-06-01 08:00:00 星期一 第二季度
|
|
||||||
*/
|
|
||||||
export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
|
|
||||||
// 传入格式处理、存储转换值
|
|
||||||
let t: any, s: any;
|
|
||||||
// 获取js 时间戳
|
|
||||||
let time: any = new Date().getTime();
|
|
||||||
// 是否是对象
|
|
||||||
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
|
|
||||||
// 当前时间戳 - 传入时间戳
|
|
||||||
time = Number.parseInt(`${time - t}`);
|
|
||||||
if (time < 10000) {
|
|
||||||
// 10秒内
|
|
||||||
return '刚刚';
|
|
||||||
} else if (time < 60000 && time >= 10000) {
|
|
||||||
// 超过10秒少于1分钟内
|
|
||||||
s = Math.floor(time / 1000);
|
|
||||||
return `${s}秒前`;
|
|
||||||
} else if (time < 3600000 && time >= 60000) {
|
|
||||||
// 超过1分钟少于1小时
|
|
||||||
s = Math.floor(time / 60000);
|
|
||||||
return `${s}分钟前`;
|
|
||||||
} else if (time < 86400000 && time >= 3600000) {
|
|
||||||
// 超过1小时少于24小时
|
|
||||||
s = Math.floor(time / 3600000);
|
|
||||||
return `${s}小时前`;
|
|
||||||
} else if (time < 259200000 && time >= 86400000) {
|
|
||||||
// 超过1天少于3天内
|
|
||||||
s = Math.floor(time / 86400000);
|
|
||||||
return `${s}天前`;
|
|
||||||
} else {
|
|
||||||
// 超过3天
|
|
||||||
let date = typeof param === 'string' || 'object' ? new Date(param) : param;
|
|
||||||
return formatDate(date, format);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
56
mayfly_go_web/src/common/utils/object.ts
Normal file
56
mayfly_go_web/src/common/utils/object.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 根据对象访问路径,获取对应的值
|
||||||
|
*
|
||||||
|
* @param obj 对象,如 {user: {name: 'xxx'}, orderNo: 1212211, products: [{id: 12}]}
|
||||||
|
* @param path 访问路径,如 orderNo 或者 user.name 或者product[0].id
|
||||||
|
* @returns 路径对应的值
|
||||||
|
*/
|
||||||
|
export function getValueByPath(obj: any, path: string) {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let result = obj;
|
||||||
|
for (let key of keys) {
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 如果是字符串,则尝试使用json解析
|
||||||
|
if (typeof result == 'string') {
|
||||||
|
try {
|
||||||
|
result = JSON.parse(result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof result !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.includes('[') && key.includes(']')) {
|
||||||
|
// 处理包含数组索引的情况
|
||||||
|
const arrayKey = key.substring(0, key.indexOf('['));
|
||||||
|
const matchIndex = key.match(/\[(.*?)\]/);
|
||||||
|
|
||||||
|
if (!matchIndex) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = parseInt(matchIndex[1]);
|
||||||
|
|
||||||
|
let arrValue = result[arrayKey];
|
||||||
|
if (typeof arrValue == 'string') {
|
||||||
|
try {
|
||||||
|
arrValue = JSON.parse(arrValue);
|
||||||
|
} catch (e) {
|
||||||
|
result = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
|
||||||
|
} else {
|
||||||
|
result = result[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUuid } from './string';
|
import { randomUuid } from './string';
|
||||||
|
|
||||||
const TokenKey = 'm-token';
|
const TokenKey = 'm-token';
|
||||||
|
const RefreshTokenKey = 'm-refresh-token';
|
||||||
const UserKey = 'm-user';
|
const UserKey = 'm-user';
|
||||||
const TagViewsKey = 'm-tagViews';
|
const TagViewsKey = 'm-tagViews';
|
||||||
const ClientIdKey = 'm-clientId';
|
const ClientIdKey = 'm-clientId';
|
||||||
@@ -15,6 +16,14 @@ export function saveToken(token: string) {
|
|||||||
setLocal(TokenKey, token);
|
setLocal(TokenKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRefreshToken(): string {
|
||||||
|
return getLocal(RefreshTokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveRefreshToken(refreshToken: string) {
|
||||||
|
return setLocal(RefreshTokenKey, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取登录用户基础信息
|
// 获取登录用户基础信息
|
||||||
export function getUser() {
|
export function getUser() {
|
||||||
return getLocal(UserKey);
|
return getLocal(UserKey);
|
||||||
@@ -39,6 +48,7 @@ export function getThemeConfig() {
|
|||||||
export function clearUser() {
|
export function clearUser() {
|
||||||
removeLocal(TokenKey);
|
removeLocal(TokenKey);
|
||||||
removeLocal(UserKey);
|
removeLocal(UserKey);
|
||||||
|
removeLocal(RefreshTokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTagViews() {
|
export function getTagViews() {
|
||||||
|
|||||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
|
|||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取内容所需要占用的宽度
|
|
||||||
*/
|
|
||||||
export function getContentWidth(content: any): number {
|
|
||||||
if (!content) {
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
// 以下分配的单位长度可根据实际需求进行调整
|
|
||||||
let flexWidth = 0;
|
|
||||||
for (const char of content) {
|
|
||||||
if (flexWidth > 500) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
|
|
||||||
// 小写字母、数字字符
|
|
||||||
flexWidth += 9.3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char >= 'A' && char <= 'Z') {
|
|
||||||
flexWidth += 9;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char >= '\u4e00' && char <= '\u9fa5') {
|
|
||||||
// 如果是中文字符,为字符分配16个单位宽度
|
|
||||||
flexWidth += 20;
|
|
||||||
} else {
|
|
||||||
// 其他种类字符
|
|
||||||
flexWidth += 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (flexWidth > 450) {
|
|
||||||
// // 设置最大宽度
|
|
||||||
// flexWidth = 450;
|
|
||||||
// }
|
|
||||||
return flexWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @returns uuid
|
* @returns uuid
|
||||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
|
|||||||
clipboard.destroy();
|
clipboard.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||||
|
keyword = keyword?.toLowerCase();
|
||||||
|
return fields.filter((field) => {
|
||||||
|
for (let valueExtractFunc of valueExtractFuncs) {
|
||||||
|
const value = valueExtractFunc(field)?.toLowerCase();
|
||||||
|
if (isPrefixSubsequence(keyword, value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||||
|
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||||
|
* @param targetTemplate 目标模板
|
||||||
|
* @returns 是否匹配
|
||||||
|
*/
|
||||||
|
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||||
|
let i = 0; // 指向prefix的索引
|
||||||
|
let j = 0; // 指向targetTemplate的索引
|
||||||
|
|
||||||
|
while (i < prefix.length && j < targetTemplate.length) {
|
||||||
|
if (prefix[i] === targetTemplate[j]) {
|
||||||
|
// 字符匹配,两个指针都向前移动
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
j++; // 目标字符串指针始终向前移动
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果prefix的所有字符都被找到,返回true
|
||||||
|
return i === prefix.length;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface ViteEnv {
|
|||||||
VITE_PORT: number;
|
VITE_PORT: number;
|
||||||
VITE_OPEN: boolean;
|
VITE_OPEN: boolean;
|
||||||
VITE_PUBLIC_PATH: string;
|
VITE_PUBLIC_PATH: string;
|
||||||
|
VITE_EDITOR: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadEnv(): ViteEnv {
|
export function loadEnv(): ViteEnv {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
|
|||||||
// 判断是否显示 展开/合并 按钮
|
// 判断是否显示 展开/合并 按钮
|
||||||
const showCollapse = computed(() => {
|
const showCollapse = computed(() => {
|
||||||
let show = false;
|
let show = false;
|
||||||
props.items.reduce((prev, current) => {
|
props.items.reduce((prev, current: any) => {
|
||||||
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
|
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
|
||||||
if (typeof props.searchCol !== 'number') {
|
if (typeof props.searchCol !== 'number') {
|
||||||
if (prev >= props.searchCol[breakPoint.value]) show = true;
|
if (prev >= props.searchCol[breakPoint.value]) show = true;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ export function hasPerm(code: string) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断用户是否拥有权限对象里对应的code
|
* 判断用户是否拥有权限对象里对应的code
|
||||||
* @param perms { save: "xxx:save"}
|
|
||||||
* @returns {"xxx:save": true} key->permission code
|
* @returns {"xxx:save": true} key->permission code
|
||||||
|
* @param permCodes
|
||||||
*/
|
*/
|
||||||
export function hasPerms(permCodes: any[]) {
|
export function hasPerms(permCodes: any[]) {
|
||||||
const res = {};
|
const res = {} as { [key: string]: boolean };
|
||||||
for (let permCode of permCodes) {
|
for (let permCode of permCodes) {
|
||||||
if (hasPerm(permCode)) {
|
if (hasPerm(permCode)) {
|
||||||
res[permCode] = true;
|
res[permCode] = true;
|
||||||
|
|||||||
@@ -35,38 +35,42 @@
|
|||||||
<p class="title">时间表达式</p>
|
<p class="title">时间表达式</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
<tr>
|
||||||
<th>crontab完整表达式</th>
|
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
||||||
|
<th>crontab完整表达式</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td>
|
<tr>
|
||||||
<span>{{ crontabValueObj.second }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.second }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.min }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.min }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.hour }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.hour }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.day }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.day }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.mouth }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.mouth }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.week }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.week }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ crontabValueObj.year }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueObj.year }}</span>
|
||||||
<td>
|
</td>
|
||||||
<span>{{ contabValueString }}</span>
|
<td>
|
||||||
</td>
|
<span>{{ crontabValueString }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<CrontabResult :ex="contabValueString"></CrontabResult>
|
<CrontabResult :ex="crontabValueString"></CrontabResult>
|
||||||
|
|
||||||
<div class="pop_btn">
|
<div class="pop_btn">
|
||||||
<el-button size="small" @click="hidePopup">取消</el-button>
|
<el-button size="small" @click="hidePopup">取消</el-button>
|
||||||
@@ -202,7 +206,7 @@ function hidePopup() {
|
|||||||
|
|
||||||
// 填充表达式
|
// 填充表达式
|
||||||
const submitFill = () => {
|
const submitFill = () => {
|
||||||
emit('fill', contabValueString.value);
|
emit('fill', crontabValueString.value);
|
||||||
hidePopup();
|
hidePopup();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,7 +224,7 @@ const clearCron = () => {
|
|||||||
changeTab(state.activeName);
|
changeTab(state.activeName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const contabValueString = computed(() => {
|
const crontabValueString = computed(() => {
|
||||||
let obj = state.crontabValueObj;
|
let obj = state.crontabValueObj;
|
||||||
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
|
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
|
||||||
return str;
|
return str;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
|
<el-input v-model="cron" placeholder="可点击左边按钮配置">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-page-header @back="props.back">
|
||||||
|
<template #content>
|
||||||
|
<span>{{ header }}</span>
|
||||||
|
<span v-if="resource && !hideResource">
|
||||||
|
-
|
||||||
|
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
|
||||||
|
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
|
||||||
|
</span>
|
||||||
|
<el-divider v-if="slots.buttons" direction="vertical" />
|
||||||
|
<slot v-if="slots.buttons" name="buttons"></slot>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<slot v-if="slots.extra" name="extra"></slot>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useSlots } from 'vue';
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
defineOptions({ name: 'DrawerHeader' });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
header: String,
|
||||||
|
back: Function,
|
||||||
|
resource: String,
|
||||||
|
hideResource: Boolean,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
17
mayfly_go_web/src/components/enumselect/EnumSelect.vue
Normal file
17
mayfly_go_web/src/components/enumselect/EnumSelect.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<el-select v-bind="$attrs" v-model="modelValue">
|
||||||
|
<el-option v-for="item in props.enums" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
enums: {
|
||||||
|
type: Object, // 需要为EnumValue类型
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue: any = defineModel('modelValue');
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tag v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
|
<el-tag :disable-transitions="true" v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -40,7 +40,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const convert = (value: any) => {
|
const convert = (value: any) => {
|
||||||
const enumValue = EnumValue.getEnumByValue(Object.values(props.enums as any) as any, value) as any;
|
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
|
||||||
if (!enumValue) {
|
if (!enumValue) {
|
||||||
state.enumLabel = '-';
|
state.enumLabel = '-';
|
||||||
state.type = 'danger';
|
state.type = 'danger';
|
||||||
|
|||||||
60
mayfly_go_web/src/components/file/FileInfo.vue
Normal file
60
mayfly_go_web/src/components/file/FileInfo.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
||||||
|
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
{{ fileDetail?.filename }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import openApi from '@/common/openApi';
|
||||||
|
import { getFileUrl } from '@/common/request';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
const props = defineProps({
|
||||||
|
fileKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
type: [Array],
|
||||||
|
},
|
||||||
|
canDownload: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
setFileInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.fileKey,
|
||||||
|
async (val) => {
|
||||||
|
if (val) {
|
||||||
|
setFileInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileDetail: any = ref({});
|
||||||
|
|
||||||
|
const setFileInfo = async () => {
|
||||||
|
if (!props.fileKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.files && props.files.length > 0) {
|
||||||
|
const file: any = props.files.find((file: any) => {
|
||||||
|
return file.fileKey === props.fileKey;
|
||||||
|
});
|
||||||
|
fileDetail.value = file;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await openApi.getFileDetail([props.fileKey]);
|
||||||
|
fileDetail.value = files?.[0];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -252,7 +252,9 @@ const changeLanguage = (value: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setEditorValue = (value: any) => {
|
const setEditorValue = (value: any) => {
|
||||||
monacoEditorIns.getModel()?.setValue(value);
|
if (value) {
|
||||||
|
monacoEditorIns.getModel()?.setValue(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.value?.format();
|
|
||||||
editorRef.value?.focus();
|
editorRef.value?.focus();
|
||||||
|
editorRef.value?.format();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
>
|
>
|
||||||
<div v-for="(item, index) in tableColumns" :key="index">
|
<div v-for="(item, index) in tableColumns" :key="index">
|
||||||
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
|
<el-checkbox v-model="item.show" :label="item.label" :true-value="true" :false-value="false" />
|
||||||
</div>
|
</div>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||||
@@ -115,18 +115,18 @@
|
|||||||
>
|
>
|
||||||
<!-- 插槽:预留功能 -->
|
<!-- 插槽:预留功能 -->
|
||||||
<template #default="scope" v-if="item.slot">
|
<template #default="scope" v-if="item.slot">
|
||||||
<slot :name="item.prop" :data="scope.row"></slot>
|
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 枚举类型使用tab展示 -->
|
<!-- 枚举类型使用tab展示 -->
|
||||||
<template #default="scope" v-else-if="item.type == 'tag'">
|
<template #default="scope" v-else-if="item.type == 'tag'">
|
||||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
|
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else>
|
<template #default="scope" v-else>
|
||||||
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
||||||
<el-popover
|
<el-popover
|
||||||
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
|
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||||
effect="light"
|
effect="light"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-link
|
<el-link
|
||||||
@click="formatText(scope.row[item.prop])"
|
@click="formatText(item.getValueByData(scope.row))"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
type="success"
|
type="success"
|
||||||
icon="MagicStick"
|
icon="MagicStick"
|
||||||
@@ -156,10 +156,10 @@
|
|||||||
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
:small="props.size == 'small'"
|
:small="props.size == 'small'"
|
||||||
@current-change="handlePageNumChange"
|
@current-change="pageNumChange"
|
||||||
@size-change="handlePageSizeChange"
|
@size-change="pageSizeChange"
|
||||||
style="text-align: right"
|
style="text-align: right"
|
||||||
layout="prev, pager, next, total, sizes, jumper"
|
layout="prev, pager, next, total, sizes"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:current-page="queryForm.pageNum"
|
v-model:current-page="queryForm.pageNum"
|
||||||
v-model:page-size="queryForm.pageSize"
|
v-model:page-size="queryForm.pageSize"
|
||||||
@@ -185,11 +185,11 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
|||||||
import { usePageTable } from '@/hooks/usePageTable';
|
import { usePageTable } from '@/hooks/usePageTable';
|
||||||
import { ElTable } from 'element-plus';
|
import { ElTable } from 'element-plus';
|
||||||
|
|
||||||
const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChange']);
|
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||||
|
|
||||||
export interface PageTableProps {
|
export interface PageTableProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
pageApi: Api; // 请求表格数据的 api
|
pageApi?: Api; // 请求表格数据的 api
|
||||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||||
showSelection?: boolean;
|
showSelection?: boolean;
|
||||||
selectable?: (row: any) => boolean; // 是否可选
|
selectable?: (row: any) => boolean; // 是否可选
|
||||||
@@ -257,7 +257,16 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
|||||||
nowSearchItem.value = searchItem;
|
nowSearchItem.value = searchItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
const pageSizeChange = (val: number) => {
|
||||||
|
emit('pageSizeChange', val);
|
||||||
|
handlePageSizeChange(val);
|
||||||
|
};
|
||||||
|
const pageNumChange = (val: number) => {
|
||||||
|
emit('pageNumChange', val);
|
||||||
|
handlePageNumChange(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||||
props.pageable,
|
props.pageable,
|
||||||
props.pageApi,
|
props.pageApi,
|
||||||
queryForm,
|
queryForm,
|
||||||
@@ -288,6 +297,13 @@ watch(isShowSearch, () => {
|
|||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newValue: any) => {
|
||||||
|
tableData = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
useEventListener(window, 'resize', calcuTableHeight);
|
useEventListener(window, 'resize', calcuTableHeight);
|
||||||
@@ -346,6 +362,7 @@ defineExpose({
|
|||||||
tableRef: tableRef,
|
tableRef: tableRef,
|
||||||
search: getTableData,
|
search: getTableData,
|
||||||
getData,
|
getData,
|
||||||
|
total,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import EnumValue from '@/common/Enum';
|
import EnumValue from '@/common/Enum';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import { getValueByPath } from '@/common/utils/object';
|
||||||
import { getTextWidth } from '@/common/utils/string';
|
import { getTextWidth } from '@/common/utils/string';
|
||||||
|
|
||||||
export class TableColumn {
|
export class TableColumn {
|
||||||
@@ -29,10 +30,15 @@ export class TableColumn {
|
|||||||
minWidth: number | string;
|
minWidth: number | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否插槽,是的话插槽名则为prop属性名
|
* 是否为插槽,若slotName为空则插槽名为prop属性名
|
||||||
*/
|
*/
|
||||||
slot: boolean = false;
|
slot: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插槽名,
|
||||||
|
*/
|
||||||
|
slotName: string = '';
|
||||||
|
|
||||||
showOverflowTooltip: boolean = true;
|
showOverflowTooltip: boolean = true;
|
||||||
|
|
||||||
sortable: boolean = false;
|
sortable: boolean = false;
|
||||||
@@ -87,7 +93,7 @@ export class TableColumn {
|
|||||||
if (this.formatFunc) {
|
if (this.formatFunc) {
|
||||||
return this.formatFunc(rowData, this.prop);
|
return this.formatFunc(rowData, this.prop);
|
||||||
}
|
}
|
||||||
return rowData[this.prop];
|
return getValueByPath(rowData, this.prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
static new(prop: string, label: string): TableColumn {
|
static new(prop: string, label: string): TableColumn {
|
||||||
@@ -144,8 +150,9 @@ export class TableColumn {
|
|||||||
* 标识该列为插槽
|
* 标识该列为插槽
|
||||||
* @returns this
|
* @returns this
|
||||||
*/
|
*/
|
||||||
isSlot(): TableColumn {
|
isSlot(slotName: string = ''): TableColumn {
|
||||||
this.slot = true;
|
this.slot = true;
|
||||||
|
this.slotName = slotName;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +172,7 @@ export class TableColumn {
|
|||||||
*/
|
*/
|
||||||
isTime(): TableColumn {
|
isTime(): TableColumn {
|
||||||
this.setFormatFunc((data: any, prop: string) => {
|
this.setFormatFunc((data: any, prop: string) => {
|
||||||
return dateFormat(data[prop]);
|
return formatDate(getValueByPath(data, prop));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -176,7 +183,7 @@ export class TableColumn {
|
|||||||
*/
|
*/
|
||||||
isEnum(enums: any): TableColumn {
|
isEnum(enums: any): TableColumn {
|
||||||
this.setFormatFunc((data: any, prop: string) => {
|
this.setFormatFunc((data: any, prop: string) => {
|
||||||
return EnumValue.getLabelByValue(enums, data[prop]);
|
return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -218,7 +225,7 @@ export class TableColumn {
|
|||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
let nowData = tableData[i];
|
let nowData = tableData[i];
|
||||||
let nowValue = nowData[prop];
|
let nowValue = getValueByPath(nowData, prop);
|
||||||
if (!nowValue) {
|
if (!nowValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
505
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
505
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div ref="viewportRef" class="viewport" :style="{ width: state.size.width + 'px', height: state.size.height + 'px' }">
|
||||||
|
<div ref="displayRef" class="display" tabindex="0" />
|
||||||
|
<div class="btn-box">
|
||||||
|
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr10" title="剪贴板" />
|
||||||
|
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
|
||||||
|
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
|
||||||
|
|
||||||
|
<el-dropdown>
|
||||||
|
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65288'])"> Ctrl + Alt + Backspace </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '100'])"> Windows + D </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '101'])"> Windows + E </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '114'])"> Windows + R </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515'])"> Windows </el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||||||
|
</div>
|
||||||
|
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="!state.fullscreen"
|
||||||
|
destroy-on-close
|
||||||
|
:title="state.filesystemDialog.title"
|
||||||
|
v-model="state.filesystemDialog.visible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
width="70%"
|
||||||
|
>
|
||||||
|
<machine-file
|
||||||
|
:machine-id="state.filesystemDialog.machineId"
|
||||||
|
:auth-cert-name="state.filesystemDialog.authCertName"
|
||||||
|
:protocol="state.filesystemDialog.protocol"
|
||||||
|
:file-id="state.filesystemDialog.fileId"
|
||||||
|
:path="state.filesystemDialog.path"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="!state.fullscreen"
|
||||||
|
destroy-on-close
|
||||||
|
:title="state.filesystemDialog.title"
|
||||||
|
v-model="state.filesystemDialog.visible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
width="70%"
|
||||||
|
>
|
||||||
|
<machine-file
|
||||||
|
:machine-id="state.filesystemDialog.machineId"
|
||||||
|
:auth-cert-name="state.filesystemDialog.authCertName"
|
||||||
|
:protocol="state.filesystemDialog.protocol"
|
||||||
|
:file-id="state.filesystemDialog.fileId"
|
||||||
|
:path="state.filesystemDialog.path"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Guacamole from './guac/guacamole-common';
|
||||||
|
import { getMachineRdpSocketUrl } from '@/views/ops/machine/api';
|
||||||
|
import clipboard from './guac/clipboard';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
import ClipboardDialog from '@/components/terminal-rdp/guac/ClipboardDialog.vue';
|
||||||
|
import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||||
|
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||||||
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { joinClientParams } from '@/common/request';
|
||||||
|
|
||||||
|
const viewportRef = ref({} as any);
|
||||||
|
const displayRef = ref({} as any);
|
||||||
|
const clipboardRef = ref({} as any);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
machineId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
authCert: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
clipboardList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['statusChange']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
client: null as any,
|
||||||
|
display: null as any,
|
||||||
|
displayElm: {} as any,
|
||||||
|
clipboard: {} as any,
|
||||||
|
keyboard: {} as any,
|
||||||
|
mouse: null as any,
|
||||||
|
touchpad: null as any,
|
||||||
|
errorMessage: '',
|
||||||
|
arguments: {} as any,
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
size: {
|
||||||
|
height: 710,
|
||||||
|
width: 1024,
|
||||||
|
force: false,
|
||||||
|
},
|
||||||
|
enableClipboard: true,
|
||||||
|
clipboardDialog: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
filesystemDialog: {
|
||||||
|
visible: false,
|
||||||
|
authCertName: '',
|
||||||
|
machineId: 0,
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
fileId: 0,
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
fullscreen: false,
|
||||||
|
beforeFullSize: {
|
||||||
|
height: 710,
|
||||||
|
width: 1024,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const installKeyboard = () => {
|
||||||
|
state.keyboard = new Guacamole.Keyboard(state.displayElm);
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.keyboard.onkeydown = (keysym: any) => {
|
||||||
|
state.client.sendKeyEvent(1, keysym);
|
||||||
|
};
|
||||||
|
state.keyboard.onkeyup = (keysym: any) => {
|
||||||
|
state.client.sendKeyEvent(0, keysym);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const uninstallKeyboard = () => {
|
||||||
|
state.keyboard!.onkeydown = state.keyboard!.onkeyup = () => {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const installMouse = () => {
|
||||||
|
state.mouse = new Guacamole.Mouse(state.displayElm);
|
||||||
|
// Hide software cursor when mouse leaves display
|
||||||
|
state.mouse.onmouseout = () => {
|
||||||
|
if (!state.display) return;
|
||||||
|
state.display.showCursor(false);
|
||||||
|
};
|
||||||
|
state.mouse.onmousedown = state.mouse.onmouseup = state.mouse.onmousemove = handleMouseState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installTouchpad = () => {
|
||||||
|
state.touchpad = new Guacamole.Mouse.Touchpad(state.displayElm);
|
||||||
|
|
||||||
|
state.touchpad.onmousedown =
|
||||||
|
state.touchpad.onmouseup =
|
||||||
|
state.touchpad.onmousemove =
|
||||||
|
(st: any) => {
|
||||||
|
// 记录按下时,光标所在位置
|
||||||
|
console.log(st);
|
||||||
|
handleMouseState(st, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录单指按压时候手在屏幕的位置
|
||||||
|
state.displayElm.ontouchend = (event: TouchEvent) => {
|
||||||
|
console.log('end', event);
|
||||||
|
state.displayElm.ontouchend = () => {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setClipboard = (data: string) => {
|
||||||
|
clipboardRef.value.setValue(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const installClipboard = () => {
|
||||||
|
state.enableClipboard = clipboard.install(state.client) as any;
|
||||||
|
clipboard.installWatcher(props.clipboardList, setClipboard);
|
||||||
|
state.client.onclipboard = clipboard.onClipboard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installResize = () => {
|
||||||
|
// 在resize事件结束后300毫秒执行
|
||||||
|
useEventListener('resize', debounce(resize, 300));
|
||||||
|
};
|
||||||
|
|
||||||
|
const installDisplay = () => {
|
||||||
|
let { width, height, force } = state.size;
|
||||||
|
state.display = state.client.getDisplay();
|
||||||
|
const displayElm = displayRef.value;
|
||||||
|
displayElm.appendChild(state.display.getElement());
|
||||||
|
displayElm.addEventListener('contextmenu', (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.returnValue = false;
|
||||||
|
});
|
||||||
|
state.client.connect('width=' + width + '&height=' + height + '&force=' + force + '&' + joinClientParams());
|
||||||
|
window.onunload = () => state.client.disconnect();
|
||||||
|
|
||||||
|
// allows focusing on the display div so that keyboard doesn't always go to session
|
||||||
|
displayElm.onclick = () => {
|
||||||
|
displayElm.focus();
|
||||||
|
};
|
||||||
|
displayElm.onfocus = () => {
|
||||||
|
displayElm.className = 'focus';
|
||||||
|
};
|
||||||
|
displayElm.onblur = () => {
|
||||||
|
displayElm.className = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
state.displayElm = displayElm;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installClient = () => {
|
||||||
|
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.authCert)) as any;
|
||||||
|
if (state.client) {
|
||||||
|
state.display?.scale(0);
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.client = new Guacamole.Client(tunnel);
|
||||||
|
|
||||||
|
tunnel.onerror = (status: any) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Tunnel failed ${JSON.stringify(status)}`);
|
||||||
|
// state.connectionState = states.TUNNEL_ERROR;
|
||||||
|
};
|
||||||
|
|
||||||
|
tunnel.onstatechange = (st: any) => {
|
||||||
|
console.log('statechange', st);
|
||||||
|
state.status = st;
|
||||||
|
switch (st) {
|
||||||
|
case TunnelState.CONNECTING: // 'CONNECTING'
|
||||||
|
break;
|
||||||
|
case TunnelState.OPEN: // 'OPEN'
|
||||||
|
state.status = TerminalStatus.Connected;
|
||||||
|
emit('statusChange', TerminalStatus.Connected);
|
||||||
|
break;
|
||||||
|
case TunnelState.CLOSED: // 'CLOSED'
|
||||||
|
state.status = TerminalStatus.Disconnected;
|
||||||
|
emit('statusChange', TerminalStatus.Disconnected);
|
||||||
|
break;
|
||||||
|
case TunnelState.UNSTABLE: // 'UNSTABLE'
|
||||||
|
state.status = TerminalStatus.Error;
|
||||||
|
emit('statusChange', TerminalStatus.Error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onstatechange = (clientState: any) => {
|
||||||
|
console.log('clientState', clientState);
|
||||||
|
switch (clientState) {
|
||||||
|
case ClientState.IDLE:
|
||||||
|
console.log('连接空闲');
|
||||||
|
break;
|
||||||
|
case ClientState.CONNECTING:
|
||||||
|
console.log('连接中...');
|
||||||
|
break;
|
||||||
|
case ClientState.WAITING:
|
||||||
|
console.log('等待服务器响应...');
|
||||||
|
break;
|
||||||
|
case ClientState.CONNECTED:
|
||||||
|
console.log('连接成功...');
|
||||||
|
break;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case ClientState.DISCONNECTING:
|
||||||
|
console.log('断开连接中...');
|
||||||
|
break;
|
||||||
|
case ClientState.DISCONNECTED:
|
||||||
|
console.log('已断开连接...');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onerror = (error: any) => {
|
||||||
|
state.client.disconnect();
|
||||||
|
console.error(`Client error ${JSON.stringify(error)}`);
|
||||||
|
state.errorMessage = error.message;
|
||||||
|
// state.connectionState = states.CLIENT_ERROR;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onsync = () => {};
|
||||||
|
|
||||||
|
state.client.onargv = (stream: any, mimetype: any, name: any) => {
|
||||||
|
if (mimetype !== 'text/plain') return;
|
||||||
|
|
||||||
|
const reader = new Guacamole.StringReader(stream);
|
||||||
|
|
||||||
|
// Assemble received data into a single string
|
||||||
|
let value = '';
|
||||||
|
reader.ontext = (text: any) => {
|
||||||
|
value += text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test mutability once stream is finished, storing the current value for the argument only if it is mutable
|
||||||
|
reader.onend = () => {
|
||||||
|
const stream = state.client.createArgumentValueStream('text/plain', name);
|
||||||
|
stream.onack = (status: any) => {
|
||||||
|
if (status.isError()) {
|
||||||
|
// ignore reject
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.arguments[name] = value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const elm = viewportRef.value;
|
||||||
|
if (!elm || !elm.offsetWidth) {
|
||||||
|
// resize is being called on the hidden window
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let box = elm.parentElement;
|
||||||
|
|
||||||
|
state.size.width = box.clientWidth;
|
||||||
|
state.size.height = box.clientHeight;
|
||||||
|
|
||||||
|
const width = parseInt(String(box.clientWidth));
|
||||||
|
const height = parseInt(String(box.clientHeight));
|
||||||
|
|
||||||
|
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
|
||||||
|
if (state.status !== TerminalStatus.Connected) {
|
||||||
|
connect(width, height);
|
||||||
|
} else {
|
||||||
|
state.client.sendSize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// setting timeout so display has time to get the correct size
|
||||||
|
// setTimeout(() => {
|
||||||
|
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
|
||||||
|
// state.display.scale(scale);
|
||||||
|
// console.log(state.size, scale);
|
||||||
|
// }, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseState = (mouseState: any, showCursor = false) => {
|
||||||
|
state.client.getDisplay().showCursor(showCursor);
|
||||||
|
|
||||||
|
const scaledMouseState = Object.assign({}, mouseState, {
|
||||||
|
x: mouseState.x / state.display.getScale(),
|
||||||
|
y: mouseState.y / state.display.getScale(),
|
||||||
|
});
|
||||||
|
state.client.sendMouseState(scaledMouseState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = (width: number, height: number, force = false) => {
|
||||||
|
if (!width && !height) {
|
||||||
|
if (state.size && state.size.width && state.size.height) {
|
||||||
|
width = state.size.width;
|
||||||
|
height = state.size.height;
|
||||||
|
} else {
|
||||||
|
// 获取当前viewportRef宽高
|
||||||
|
width = viewportRef.value.clientWidth;
|
||||||
|
height = viewportRef.value.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.size = { width, height, force };
|
||||||
|
|
||||||
|
installClient();
|
||||||
|
installDisplay();
|
||||||
|
installKeyboard();
|
||||||
|
installMouse();
|
||||||
|
installTouchpad();
|
||||||
|
installClipboard();
|
||||||
|
installResize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.client?.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const blur = () => {
|
||||||
|
uninstallKeyboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {};
|
||||||
|
|
||||||
|
const openPaste = async () => {
|
||||||
|
state.clipboardDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePaste = async () => {
|
||||||
|
installKeyboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onsubmitClipboard = (val: string) => {
|
||||||
|
state.clipboardDialog.visible = false;
|
||||||
|
installKeyboard();
|
||||||
|
clipboard.sendRemoteClipboard(state.client, val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFilesystem = async () => {
|
||||||
|
state.filesystemDialog.protocol = 2;
|
||||||
|
state.filesystemDialog.machineId = props.machineId;
|
||||||
|
state.filesystemDialog.authCertName = props.authCert;
|
||||||
|
state.filesystemDialog.fileId = props.machineId;
|
||||||
|
state.filesystemDialog.path = '/';
|
||||||
|
state.filesystemDialog.title = `远程桌面文件管理`;
|
||||||
|
state.filesystemDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFullScreen = function () {
|
||||||
|
launchIntoFullscreen(viewportRef.value);
|
||||||
|
state.fullscreen = true;
|
||||||
|
|
||||||
|
// 记录原始尺寸
|
||||||
|
state.beforeFullSize = {
|
||||||
|
width: state.size.width,
|
||||||
|
height: state.size.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用新的宽高重新连接
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(viewportRef.value.clientWidth, viewportRef.value.clientHeight, false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
watchFullscreenChange(watchFullscreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
function watchFullscreen(event: Event, isFull: boolean) {
|
||||||
|
if (!isFull) {
|
||||||
|
closeFullScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeFullScreen = function () {
|
||||||
|
exitFullscreen();
|
||||||
|
|
||||||
|
state.fullscreen = false;
|
||||||
|
|
||||||
|
// 使用新的宽高重新连接
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(state.beforeFullSize.width, state.beforeFullSize.height, false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 取消注册esc事件,退出全屏
|
||||||
|
unWatchFullscreenChange(watchFullscreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSendKeyboard = (keys: string[]) => {
|
||||||
|
if (!state.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
state.client.sendKeyEvent(1, keys[i]);
|
||||||
|
}
|
||||||
|
for (let j = 0; j < keys.length; j++) {
|
||||||
|
state.client.sendKeyEvent(0, keys[j]);
|
||||||
|
}
|
||||||
|
ElMessage.success('发送组合键成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exposes = {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
init: connect,
|
||||||
|
close: disconnect,
|
||||||
|
fitTerminal: resize,
|
||||||
|
focus,
|
||||||
|
blur,
|
||||||
|
setRemoteClipboard: onsubmitClipboard,
|
||||||
|
} as TerminalExpose;
|
||||||
|
|
||||||
|
defineExpose(exposes);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.viewport {
|
||||||
|
position: relative;
|
||||||
|
width: 1024px;
|
||||||
|
min-height: 710px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.display {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.btn-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
background: #dddddd4a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
137
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
137
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rdpDialog" ref="dialogRef">
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:before-close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
width="1024"
|
||||||
|
@open="connect()"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="terminal-title-wrapper">
|
||||||
|
<!-- 左侧 -->
|
||||||
|
<div class="title-left-fixed">
|
||||||
|
<!-- title信息 -->
|
||||||
|
<div>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="title-right-fixed">
|
||||||
|
<el-popconfirm @confirm="connect(true)" title="确认重新连接?">
|
||||||
|
<template #reference>
|
||||||
|
<div class="mr10 pointer">
|
||||||
|
<el-tag v-if="state.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
|
||||||
|
<el-tag v-else type="danger" effect="light" round> 未连接,点击重连 </el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
|
||||||
|
<el-popconfirm @confirm="handleClose" title="确认关闭?">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon name="Close" class="pointer-icon" title="关闭" :size="20" />
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<machine-rdp ref="rdpRef" :machine-id="machineId" :auth-cert="authCert" @status-change="handleStatusChange" />
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref, toRefs, watch } from 'vue';
|
||||||
|
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
|
const rdpRef = ref({} as any);
|
||||||
|
const dialogRef = ref({} as any);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean },
|
||||||
|
machineId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
authCert: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: { type: String },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
title: '',
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialogVisible } = toRefs(state);
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
const visible = newValue.visible;
|
||||||
|
state.dialogVisible = visible;
|
||||||
|
if (visible) {
|
||||||
|
state.title = newValue.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const connect = (force = false) => {
|
||||||
|
rdpRef.value?.disconnect();
|
||||||
|
|
||||||
|
let width = 1024;
|
||||||
|
let height = 710;
|
||||||
|
rdpRef.value?.connect(width, height, force);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (status: TerminalStatus) => {
|
||||||
|
state.status = status;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭取消按钮触发的事件
|
||||||
|
*/
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('update:machineId', null);
|
||||||
|
emit('cancel');
|
||||||
|
rdpRef.value?.disconnect();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.rdpDialog {
|
||||||
|
.el-dialog {
|
||||||
|
padding: 0;
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||||
|
max-height: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.title-right-fixed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="clipboard-dialog">
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="请输入需要粘贴的文本"
|
||||||
|
:before-close="onclose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.modelValue" type="textarea" :rows="20" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="onsubmit">确 定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, toRefs, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['submit', 'close', 'update:visible']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
modelValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialogVisible } = toRefs(state);
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
state.dialogVisible = newValue.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onclose = () => {
|
||||||
|
emits('update:visible', false);
|
||||||
|
emits('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onsubmit = () => {
|
||||||
|
state.dialogVisible = false;
|
||||||
|
if (state.modelValue) {
|
||||||
|
ElMessage.success('发送剪贴板数据成功');
|
||||||
|
emits('submit', state.modelValue);
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('请输入需要粘贴的文本');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (val: string) => {
|
||||||
|
state.modelValue = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ setValue });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.clipboard-dialog {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
mayfly_go_web/src/components/terminal-rdp/guac/clipboard.js
Normal file
147
mayfly_go_web/src/components/terminal-rdp/guac/clipboard.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import Guacamole from './guacamole-common';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const clipboard = {};
|
||||||
|
|
||||||
|
clipboard.install = (client) => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboard.getLocalClipboard().then((data) => (clipboard.cache = data));
|
||||||
|
|
||||||
|
window.addEventListener('load', clipboard.update(client), true);
|
||||||
|
window.addEventListener('copy', clipboard.update(client));
|
||||||
|
window.addEventListener('cut', clipboard.update(client));
|
||||||
|
window.addEventListener(
|
||||||
|
'focus',
|
||||||
|
(e) => {
|
||||||
|
if (e.target === window) {
|
||||||
|
clipboard.update(client)();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.update = (client) => {
|
||||||
|
return () => {
|
||||||
|
clipboard.getLocalClipboard().then((data) => {
|
||||||
|
clipboard.cache = data;
|
||||||
|
clipboard.setRemoteClipboard(client);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.sendRemoteClipboard = (client, text) => {
|
||||||
|
clipboard.cache = {
|
||||||
|
type: 'text/plain',
|
||||||
|
data: text,
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setRemoteClipboard(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setRemoteClipboard = (client) => {
|
||||||
|
if (!clipboard.cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let writer;
|
||||||
|
|
||||||
|
const stream = client.createClipboardStream(clipboard.cache.type);
|
||||||
|
|
||||||
|
if (typeof clipboard.cache.data === 'string') {
|
||||||
|
writer = new Guacamole.StringWriter(stream);
|
||||||
|
writer.sendText(clipboard.cache.data);
|
||||||
|
writer.sendEnd();
|
||||||
|
|
||||||
|
clipboard.appendClipboardList('up', clipboard.cache.data);
|
||||||
|
} else {
|
||||||
|
writer = new Guacamole.BlobWriter(stream);
|
||||||
|
writer.oncomplete = function clipboardSent() {
|
||||||
|
writer.sendEnd();
|
||||||
|
};
|
||||||
|
writer.sendBlob(clipboard.cache.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.getLocalClipboard = async () => {
|
||||||
|
// 获取本地剪贴板数据
|
||||||
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
return {
|
||||||
|
type: 'text/plain',
|
||||||
|
data: text,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('只有https才可以访问剪贴板');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setLocalClipboard = async (data) => {
|
||||||
|
if (data.type === 'text/plain') {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取到远程服务器剪贴板变动
|
||||||
|
clipboard.onClipboard = (stream, mimetype) => {
|
||||||
|
let reader;
|
||||||
|
|
||||||
|
if (/^text\//.exec(mimetype)) {
|
||||||
|
reader = new Guacamole.StringReader(stream);
|
||||||
|
|
||||||
|
// Assemble received data into a single string
|
||||||
|
let data = '';
|
||||||
|
reader.ontext = (text) => {
|
||||||
|
data += text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set clipboard contents once stream is finished
|
||||||
|
reader.onend = () => {
|
||||||
|
clipboard.setLocalClipboard({
|
||||||
|
type: mimetype,
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboard.setClipboardFn && typeof clipboard.setClipboardFn === 'function' && clipboard.setClipboardFn(data);
|
||||||
|
|
||||||
|
clipboard.appendClipboardList('down', data);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
reader = new Guacamole.BlobReader(stream, mimetype);
|
||||||
|
reader.onend = () => {
|
||||||
|
clipboard.setLocalClipboard({
|
||||||
|
type: mimetype,
|
||||||
|
data: reader.getBlob(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 注册剪贴板监听器,如果有本地或远程剪贴板变动,则会更新剪贴板列表
|
||||||
|
*/
|
||||||
|
clipboard.installWatcher = (clipboardList, setClipboardFn) => {
|
||||||
|
clipboard.clipboardList = clipboardList;
|
||||||
|
clipboard.setClipboardFn = setClipboardFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.appendClipboardList = (src, data) => {
|
||||||
|
clipboard.clipboardList = clipboard.clipboardList || [];
|
||||||
|
// 循环判断是否重复
|
||||||
|
for (let i = 0; i < clipboard.clipboardList.length; i++) {
|
||||||
|
if (clipboard.clipboardList[i].data === data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboard.clipboardList.push({ type: 'text/plain', data, src });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default clipboard;
|
||||||
14441
mayfly_go_web/src/components/terminal-rdp/guac/guacamole-common.js
Normal file
14441
mayfly_go_web/src/components/terminal-rdp/guac/guacamole-common.js
Normal file
File diff suppressed because it is too large
Load Diff
38
mayfly_go_web/src/components/terminal-rdp/guac/screen.js
Normal file
38
mayfly_go_web/src/components/terminal-rdp/guac/screen.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export function launchIntoFullscreen(element) {
|
||||||
|
if (element.requestFullscreen) {
|
||||||
|
element.requestFullscreen();
|
||||||
|
} else if (element.mozRequestFullScreen) {
|
||||||
|
element.mozRequestFullScreen();
|
||||||
|
} else if (element.webkitRequestFullscreen) {
|
||||||
|
element.webkitRequestFullscreen();
|
||||||
|
} else if (element.msRequestFullscreen) {
|
||||||
|
element.msRequestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function exitFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.mozCancelFullScreen) {
|
||||||
|
document.mozCancelFullScreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function watchFullscreenChange(callback) {
|
||||||
|
function onFullscreenChange(e) {
|
||||||
|
let isFull = (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) != null;
|
||||||
|
callback(e, isFull);
|
||||||
|
}
|
||||||
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('mozfullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('msfullscreenchange', onFullscreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unWatchFullscreenChange(callback) {
|
||||||
|
document.removeEventListener('fullscreenchange', callback);
|
||||||
|
document.removeEventListener('mozfullscreenchange', callback);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', callback);
|
||||||
|
document.removeEventListener('msfullscreenchange', callback);
|
||||||
|
}
|
||||||
78
mayfly_go_web/src/components/terminal-rdp/guac/states.js
Normal file
78
mayfly_go_web/src/components/terminal-rdp/guac/states.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export const ClientState = {
|
||||||
|
/**
|
||||||
|
* The client is idle, with no active connection.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
IDLE: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is in the process of establishing a connection.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTING: 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is waiting on further information or a remote server to
|
||||||
|
* establish the connection.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
WAITING: 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is actively connected to a remote server.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTED: 3,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is in the process of disconnecting from the remote server.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
DISCONNECTING: 4,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client has completed the connection and is no longer connected.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
DISCONNECTED: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TunnelState = {
|
||||||
|
/**
|
||||||
|
* A connection is in pending. It is not yet known whether connection was
|
||||||
|
* successful.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTING: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection was successful, and data is being received.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
OPEN: 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection is closed. Connection may not have been successful, the
|
||||||
|
* tunnel may have been explicitly closed by either side, or an error may
|
||||||
|
* have occurred.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CLOSED: 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection is open, but communication through the tunnel appears to
|
||||||
|
* be disrupted, and the connection may close as a result.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
UNSTABLE: 3,
|
||||||
|
};
|
||||||
11
mayfly_go_web/src/components/terminal-rdp/index.ts
Normal file
11
mayfly_go_web/src/components/terminal-rdp/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface TerminalExpose {
|
||||||
|
/** 连接 */
|
||||||
|
init(width: number, height: number, force: boolean): void;
|
||||||
|
|
||||||
|
/** 短开连接 */
|
||||||
|
close(): void;
|
||||||
|
|
||||||
|
blur(): void;
|
||||||
|
|
||||||
|
focus(): void;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
|
<div id="terminal-body" :style="{ height }">
|
||||||
<div ref="terminalRef" class="terminal" />
|
<div ref="terminalRef" class="terminal" />
|
||||||
|
|
||||||
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal, ITheme } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
@@ -20,8 +20,15 @@ import TerminalSearch from './TerminalSearch.vue';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { TerminalStatus } from './common';
|
import { TerminalStatus } from './common';
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import themes from './themes';
|
||||||
|
import { TrzszFilter } from 'trzsz';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
// mounted时,是否执行init方法
|
||||||
|
mountInit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 初始化执行命令
|
* 初始化执行命令
|
||||||
*/
|
*/
|
||||||
@@ -60,13 +67,13 @@ const state = reactive({
|
|||||||
search: null as any,
|
search: null as any,
|
||||||
weblinks: null as any,
|
weblinks: null as any,
|
||||||
},
|
},
|
||||||
status: TerminalStatus.NoConnected,
|
status: -11,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
if (props.mountInit) {
|
||||||
init();
|
init();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -76,15 +83,30 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 监听 themeConfig terminalTheme配置的变化
|
||||||
|
watch(
|
||||||
|
() => themeConfig.value.terminalTheme,
|
||||||
|
() => {
|
||||||
|
term.options.theme = getTerminalTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
state.status = TerminalStatus.NoConnected;
|
||||||
if (term) {
|
if (term) {
|
||||||
console.log('重新连接...');
|
console.log('重新连接...');
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
initTerm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initTerm() {
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||||
@@ -92,13 +114,10 @@ function init() {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
theme: {
|
fastScrollModifier: 'ctrl',
|
||||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
theme: getTerminalTheme(),
|
||||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
|
||||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
|
||||||
// cursorAccent: "red", // 光标停止颜色
|
|
||||||
} as any,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
term.open(terminalRef.value);
|
term.open(terminalRef.value);
|
||||||
|
|
||||||
// 注册自适应组件
|
// 注册自适应组件
|
||||||
@@ -106,31 +125,12 @@ function init() {
|
|||||||
state.addon.fit = fitAddon;
|
state.addon.fit = fitAddon;
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
|
// 注册窗口大小监听器
|
||||||
|
useEventListener('resize', debounce(fitTerminal, 400));
|
||||||
|
|
||||||
// 注册搜索组件
|
|
||||||
const searchAddon = new SearchAddon();
|
|
||||||
state.addon.search = searchAddon;
|
|
||||||
term.loadAddon(searchAddon);
|
|
||||||
|
|
||||||
// 注册 url link组件
|
|
||||||
const weblinks = new WebLinksAddon();
|
|
||||||
state.addon.weblinks = weblinks;
|
|
||||||
term.loadAddon(weblinks);
|
|
||||||
|
|
||||||
// 初始化websocket
|
|
||||||
initSocket();
|
initSocket();
|
||||||
}
|
// 注册其他插件
|
||||||
|
loadAddon();
|
||||||
/**
|
|
||||||
* 连接成功
|
|
||||||
*/
|
|
||||||
const onConnected = () => {
|
|
||||||
// 注册心跳
|
|
||||||
pingInterval = setInterval(sendPing, 15000);
|
|
||||||
|
|
||||||
// 注册 terminal 事件
|
|
||||||
term.onResize((event) => sendResize(event.cols, event.rows));
|
|
||||||
term.onData((event) => sendCmd(event));
|
|
||||||
|
|
||||||
// 注册自定义快捷键
|
// 注册自定义快捷键
|
||||||
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||||
@@ -142,50 +142,26 @@ const onConnected = () => {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
state.status = TerminalStatus.Connected;
|
|
||||||
|
|
||||||
// 注册窗口大小监听器
|
|
||||||
useEventListener('resize', debounce(fitTerminal, 400));
|
|
||||||
|
|
||||||
focus();
|
|
||||||
|
|
||||||
// 如果有初始要执行的命令,则发送执行命令
|
|
||||||
if (props.cmd) {
|
|
||||||
sendCmd(props.cmd + ' \r');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自适应终端
|
|
||||||
const fitTerminal = () => {
|
|
||||||
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
|
||||||
if (!dimensions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dimensions?.cols && dimensions?.rows) {
|
|
||||||
term.resize(dimensions.cols, dimensions.rows);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const focus = () => {
|
|
||||||
setTimeout(() => term.focus(), 400);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
term.clear();
|
|
||||||
term.clearSelection();
|
|
||||||
term.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
function initSocket() {
|
function initSocket() {
|
||||||
if (props.socketUrl) {
|
if (!props.socketUrl) {
|
||||||
let socketUrl = `${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`;
|
return;
|
||||||
socket = new WebSocket(socketUrl);
|
|
||||||
}
|
}
|
||||||
|
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
||||||
// 监听socket连接
|
// 监听socket连接
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
onConnected();
|
// 注册心跳
|
||||||
|
pingInterval = setInterval(sendPing, 15000);
|
||||||
|
state.status = TerminalStatus.Connected;
|
||||||
|
|
||||||
|
focus();
|
||||||
|
fitTerminal();
|
||||||
|
|
||||||
|
// 如果有初始要执行的命令,则发送执行命令
|
||||||
|
if (props.cmd) {
|
||||||
|
sendCmd(props.cmd + ' \r');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听socket错误信息
|
// 监听socket错误信息
|
||||||
@@ -197,20 +173,95 @@ function initSocket() {
|
|||||||
|
|
||||||
socket.onclose = (e: CloseEvent) => {
|
socket.onclose = (e: CloseEvent) => {
|
||||||
console.log('terminal socket close...', e.reason);
|
console.log('terminal socket close...', e.reason);
|
||||||
// 清除 ping
|
|
||||||
pingInterval && clearInterval(pingInterval);
|
|
||||||
state.status = TerminalStatus.Disconnected;
|
state.status = TerminalStatus.Disconnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听socket消息
|
|
||||||
socket.onmessage = getMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessage(msg: any) {
|
function loadAddon() {
|
||||||
// msg.data是真正后端返回的数据
|
// 注册搜索组件
|
||||||
term.write(msg.data);
|
const searchAddon = new SearchAddon();
|
||||||
|
state.addon.search = searchAddon;
|
||||||
|
term.loadAddon(searchAddon);
|
||||||
|
|
||||||
|
// 注册 url link组件
|
||||||
|
const weblinks = new WebLinksAddon();
|
||||||
|
state.addon.weblinks = weblinks;
|
||||||
|
term.loadAddon(weblinks);
|
||||||
|
|
||||||
|
// 注册 trzsz
|
||||||
|
// initialize trzsz filter
|
||||||
|
const trzsz = new TrzszFilter({
|
||||||
|
// write the server output to the terminal
|
||||||
|
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||||
|
// send the user input to the server
|
||||||
|
sendToServer: sendCmd,
|
||||||
|
// the terminal columns
|
||||||
|
terminalColumns: term.cols,
|
||||||
|
// there is a windows shell
|
||||||
|
isWindowsShell: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// let trzsz process the server output
|
||||||
|
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
|
||||||
|
// let trzsz process the user input
|
||||||
|
term.onData((data) => trzsz.processTerminalInput(data));
|
||||||
|
term.onBinary((data) => trzsz.processBinaryInput(data));
|
||||||
|
term.onResize((size) => {
|
||||||
|
sendResize(size.cols, size.rows);
|
||||||
|
// tell trzsz the terminal columns has been changed
|
||||||
|
trzsz.setTerminalColumns(size.cols);
|
||||||
|
});
|
||||||
|
// enable drag files or directories to upload
|
||||||
|
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||||
|
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
trzsz
|
||||||
|
.uploadFiles(event.dataTransfer.items)
|
||||||
|
.then(() => console.log('upload success'))
|
||||||
|
.catch((err: any) => console.log(err));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入内容至终端
|
||||||
|
const write2Term = (data: any) => {
|
||||||
|
term.write(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeln2Term = (data: any) => {
|
||||||
|
term.writeln(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTerminalTheme = () => {
|
||||||
|
const terminalTheme = themeConfig.value.terminalTheme;
|
||||||
|
// 如果不是自定义主题,则返回内置主题
|
||||||
|
if (terminalTheme != 'custom') {
|
||||||
|
return themes[terminalTheme];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义主题
|
||||||
|
return {
|
||||||
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||||
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||||
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||||
|
// cursorAccent: "red", // 光标停止颜色
|
||||||
|
} as ITheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自适应终端
|
||||||
|
const fitTerminal = () => {
|
||||||
|
state.addon.fit.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
setTimeout(() => term.focus(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
term.clear();
|
||||||
|
term.clearSelection();
|
||||||
|
term.focus();
|
||||||
|
};
|
||||||
|
|
||||||
enum MsgType {
|
enum MsgType {
|
||||||
Resize = 1,
|
Resize = 1,
|
||||||
Data = 2,
|
Data = 2,
|
||||||
@@ -218,29 +269,19 @@ enum MsgType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const send = (msg: any) => {
|
const send = (msg: any) => {
|
||||||
state.status == TerminalStatus.Connected && socket.send(JSON.stringify(msg));
|
state.status == TerminalStatus.Connected && socket?.send(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendResize = (cols: number, rows: number) => {
|
const sendResize = (cols: number, rows: number) => {
|
||||||
send({
|
send(`${MsgType.Resize}|${rows}|${cols}`);
|
||||||
type: MsgType.Resize,
|
|
||||||
Cols: cols,
|
|
||||||
Rows: rows,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPing = () => {
|
const sendPing = () => {
|
||||||
send({
|
send(`${MsgType.Ping}|ping`);
|
||||||
type: MsgType.Ping,
|
|
||||||
msg: 'ping',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendCmd(key: any) {
|
function sendCmd(key: any) {
|
||||||
send({
|
send(`${MsgType.Data}|${key}`);
|
||||||
type: MsgType.Data,
|
|
||||||
msg: key,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSocket() {
|
function closeSocket() {
|
||||||
@@ -265,20 +306,19 @@ const getStatus = (): TerminalStatus => {
|
|||||||
return state.status;
|
return state.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminal-body {
|
#terminal-body {
|
||||||
background: #212529;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.terminal {
|
.terminal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.xterm .xterm-viewport {
|
// .xterm .xterm-viewport {
|
||||||
overflow-y: hidden;
|
// overflow-y: hidden;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
|
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="终端"
|
title="SSH终端"
|
||||||
v-model="openTerminal.visible"
|
v-model="openTerminal.visible"
|
||||||
top="32px"
|
top="32px"
|
||||||
class="terminal-dialog"
|
class="terminal-dialog"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
<div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
||||||
<TerminalBody
|
<TerminalBody
|
||||||
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
||||||
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive } from 'vue';
|
import { reactive, toRefs } from 'vue';
|
||||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { TerminalStatus } from './common';
|
import { TerminalStatus } from './common';
|
||||||
@@ -259,6 +259,10 @@ defineExpose({
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
padding: 1px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// 取消body最大高度,否则全屏有问题
|
// 取消body最大高度,否则全屏有问题
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
|
|||||||
113
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal file
113
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer v-model="visible" :before-close="cancel" size="50%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="props.title" :back="cancel">
|
||||||
|
<template #extra>
|
||||||
|
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr20" />
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions class="mb10" :column="1" border v-if="extra">
|
||||||
|
<el-descriptions-item v-for="(value, key) in extra" :key="key" :span="1" :label="key">{{ value }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<TerminalBody class="mb10" ref="terminalRef" height="calc(100vh - 220px)" />
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import TerminalBody from './TerminalBody.vue';
|
||||||
|
import { logApi } from '../../views/system/api';
|
||||||
|
import { LogTypeEnum } from '@/views/system/enums';
|
||||||
|
import { useIntervalFn } from '@vueuse/core';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '日志',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
const logId = defineModel<number>('logId', { default: 0 });
|
||||||
|
|
||||||
|
const terminalRef: any = ref(null);
|
||||||
|
const nowLine = ref(0);
|
||||||
|
const log = ref({}) as any;
|
||||||
|
|
||||||
|
const extra = computed(() => {
|
||||||
|
if (log.value?.extra) {
|
||||||
|
return JSON.parse(log.value.extra);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定时获取最新日志
|
||||||
|
const { pause, resume } = useIntervalFn(() => {
|
||||||
|
writeLog();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => logId.value,
|
||||||
|
(logId: number) => {
|
||||||
|
terminalRef.value?.clear();
|
||||||
|
if (!logId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeLog();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
logId.value = 0;
|
||||||
|
nowLine.value = 0;
|
||||||
|
pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeLog = async () => {
|
||||||
|
const log = await getLog();
|
||||||
|
if (!log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeLog2Term(log);
|
||||||
|
|
||||||
|
// 如果不是还在执行中的日志,则暂停轮询
|
||||||
|
if (log.type != LogTypeEnum.Running.value) {
|
||||||
|
pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resume();
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeLog2Term = (log: any) => {
|
||||||
|
if (!log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = log.resp.split('\n');
|
||||||
|
for (let line of lines.slice(nowLine.value)) {
|
||||||
|
nowLine.value += 1;
|
||||||
|
terminalRef.value?.writeln2Term(line);
|
||||||
|
}
|
||||||
|
terminalRef.value?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLog = async () => {
|
||||||
|
if (!logId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const logRes = await logApi.detail.request({
|
||||||
|
id: logId.value,
|
||||||
|
});
|
||||||
|
log.value = logRes;
|
||||||
|
return logRes;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
|
||||||
export enum TerminalStatus {
|
export enum TerminalStatus {
|
||||||
Error = -1,
|
Error = -1,
|
||||||
NoConnected = 0,
|
NoConnected = 0,
|
||||||
Connected = 1,
|
Connected = 1,
|
||||||
Disconnected = 2,
|
Disconnected = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TerminalStatusEnum = {
|
||||||
|
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||||
|
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
|
||||||
|
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
|
||||||
|
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||||
|
};
|
||||||
|
|||||||
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
export default {
|
||||||
|
dark: {
|
||||||
|
foreground: '#c7c7c7',
|
||||||
|
background: '#000000',
|
||||||
|
cursor: '#c7c7c7',
|
||||||
|
selectionBackground: '#686868',
|
||||||
|
|
||||||
|
black: '#000000',
|
||||||
|
brightBlack: '#676767',
|
||||||
|
|
||||||
|
red: '#c91b00',
|
||||||
|
brightRed: '#ff6d67',
|
||||||
|
|
||||||
|
green: '#00c200',
|
||||||
|
brightGreen: '#5ff967',
|
||||||
|
|
||||||
|
yellow: '#c7c400',
|
||||||
|
brightYellow: '#fefb67',
|
||||||
|
|
||||||
|
blue: '#0225c7',
|
||||||
|
brightBlue: '#6871ff',
|
||||||
|
|
||||||
|
magenta: '#c930c7',
|
||||||
|
brightMagenta: '#ff76ff',
|
||||||
|
|
||||||
|
cyan: '#00c5c7',
|
||||||
|
brightCyan: '#5ffdff',
|
||||||
|
|
||||||
|
white: '#c7c7c7',
|
||||||
|
brightWhite: '#fffefe',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
foreground: '#000000',
|
||||||
|
background: '#fffefe',
|
||||||
|
cursor: '#000000',
|
||||||
|
selectionBackground: '#c7c7c7',
|
||||||
|
|
||||||
|
black: '#000000',
|
||||||
|
brightBlack: '#676767',
|
||||||
|
|
||||||
|
red: '#c91b00',
|
||||||
|
brightRed: '#ff6d67',
|
||||||
|
|
||||||
|
green: '#00c200',
|
||||||
|
brightGreen: '#5ff967',
|
||||||
|
|
||||||
|
yellow: '#c7c400',
|
||||||
|
brightYellow: '#fefb67',
|
||||||
|
|
||||||
|
blue: '#0225c7',
|
||||||
|
brightBlue: '#6871ff',
|
||||||
|
|
||||||
|
magenta: '#c930c7',
|
||||||
|
brightMagenta: '#ff76ff',
|
||||||
|
|
||||||
|
cyan: '#00c5c7',
|
||||||
|
brightCyan: '#5ffdff',
|
||||||
|
|
||||||
|
white: '#c7c7c7',
|
||||||
|
brightWhite: '#fffefe',
|
||||||
|
},
|
||||||
|
solarizedLight: {
|
||||||
|
foreground: '#657b83',
|
||||||
|
background: '#fdf6e3',
|
||||||
|
cursor: '#657b83',
|
||||||
|
selectionBackground: '#c7c7c7',
|
||||||
|
|
||||||
|
black: '#073642',
|
||||||
|
brightBlack: '#002b36',
|
||||||
|
|
||||||
|
red: '#dc322f',
|
||||||
|
brightRed: '#cb4b16',
|
||||||
|
|
||||||
|
green: '#859900',
|
||||||
|
brightGreen: '#586e75',
|
||||||
|
|
||||||
|
yellow: '#b58900',
|
||||||
|
brightYellow: '#657b83',
|
||||||
|
|
||||||
|
blue: '#268bd2',
|
||||||
|
brightBlue: '#839496',
|
||||||
|
|
||||||
|
magenta: '#d33682',
|
||||||
|
brightMagenta: '#6c71c4',
|
||||||
|
|
||||||
|
cyan: '#2aa198',
|
||||||
|
brightCyan: '#93a1a1',
|
||||||
|
|
||||||
|
white: '#eee8d5',
|
||||||
|
brightWhite: '#fdf6e3',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Api from '@/common/Api';
|
import Api from '@/common/Api';
|
||||||
import { isReactive, reactive, toRefs, toValue } from 'vue';
|
import { reactive, toRefs, toValue } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description table 页面操作方法封装
|
* @description table 页面操作方法封装
|
||||||
@@ -41,15 +41,10 @@ export const usePageTable = (
|
|||||||
let sp = toValue(state.searchParams);
|
let sp = toValue(state.searchParams);
|
||||||
if (beforeQueryFn) {
|
if (beforeQueryFn) {
|
||||||
sp = beforeQueryFn(sp);
|
sp = beforeQueryFn(sp);
|
||||||
|
|
||||||
if (isReactive(state.searchParams)) {
|
|
||||||
state.searchParams.value = sp;
|
|
||||||
} else {
|
|
||||||
state.searchParams = sp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await api.request(sp);
|
let res = await api.request(sp);
|
||||||
|
res.list = res.list || [];
|
||||||
dataCallBack && (res = await dataCallBack(res));
|
dataCallBack && (res = await dataCallBack(res));
|
||||||
|
|
||||||
if (pageable) {
|
if (pageable) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { getClientId, getToken } from '@/common/utils/storage';
|
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
|
||||||
import { templateResolve } from '@/common/utils/string';
|
import { templateResolve } from '@/common/utils/string';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { createFetch } from '@vueuse/core';
|
import { createFetch } from '@vueuse/core';
|
||||||
@@ -8,6 +8,7 @@ import { Result, ResultEnum } from '@/common/request';
|
|||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { unref } from 'vue';
|
import { unref } from 'vue';
|
||||||
import { URL_401 } from '@/router/staticRouter';
|
import { URL_401 } from '@/router/staticRouter';
|
||||||
|
import openApi from '@/common/openApi';
|
||||||
|
|
||||||
const baseUrl: string = config.baseApiUrl;
|
const baseUrl: string = config.baseApiUrl;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ const useCustomFetch = createFetch({
|
|||||||
combination: 'chain',
|
combination: 'chain',
|
||||||
options: {
|
options: {
|
||||||
immediate: false,
|
immediate: false,
|
||||||
timeout: 60000,
|
timeout: 600000,
|
||||||
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
|
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
|
||||||
async beforeFetch({ options }) {
|
async beforeFetch({ options }) {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
@@ -41,16 +42,13 @@ const useCustomFetch = createFetch({
|
|||||||
|
|
||||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
|
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
|
||||||
const uaf = useCustomFetch<T>(api.url, {
|
const uaf = useCustomFetch<T>(api.url, {
|
||||||
beforeFetch({ url, options }) {
|
async beforeFetch({ url, options }) {
|
||||||
options.method = api.method;
|
options.method = api.method;
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let paramsValue = unref(params);
|
let paramsValue = unref(params);
|
||||||
if (api.beforeHandler) {
|
|
||||||
paramsValue = api.beforeHandler(paramsValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiUrl = url;
|
let apiUrl = url;
|
||||||
// 简单判断该url是否是restful风格
|
// 简单判断该url是否是restful风格
|
||||||
@@ -58,6 +56,10 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
|||||||
apiUrl = templateResolve(apiUrl, paramsValue);
|
apiUrl = templateResolve(apiUrl, paramsValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (api.beforeHandler) {
|
||||||
|
paramsValue = await api.beforeHandler(paramsValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (paramsValue) {
|
if (paramsValue) {
|
||||||
const method = options.method?.toLowerCase();
|
const method = options.method?.toLowerCase();
|
||||||
// post和put使用json格式传参
|
// post和put使用json格式传参
|
||||||
@@ -87,61 +89,104 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
execute: async function () {
|
execute: async function () {
|
||||||
try {
|
return execUaf(uaf);
|
||||||
await uaf.execute(true);
|
|
||||||
} catch (e: any) {
|
|
||||||
const rejectPromise = Promise.reject(e);
|
|
||||||
|
|
||||||
if (e?.name == 'AbortError') {
|
|
||||||
console.log('请求已取消');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const respStatus = uaf.response.value?.status;
|
|
||||||
if (respStatus == 404) {
|
|
||||||
ElMessage.error('请求接口不存在');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
if (respStatus == 500) {
|
|
||||||
ElMessage.error('服务器响应异常');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(e);
|
|
||||||
ElMessage.error('网络请求错误');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: Result = uaf.data.value as any;
|
|
||||||
if (!result) {
|
|
||||||
ElMessage.error('网络请求失败');
|
|
||||||
return Promise.reject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果返回为成功结果,则将结果的data赋值给响应式data
|
|
||||||
if (result.code === ResultEnum.SUCCESS) {
|
|
||||||
uaf.data.value = result.data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果提示没有权限,则跳转至无权限页面
|
|
||||||
if (result.code === ResultEnum.NO_PERMISSION) {
|
|
||||||
router.push({
|
|
||||||
path: URL_401,
|
|
||||||
});
|
|
||||||
return Promise.reject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
|
||||||
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
|
|
||||||
ElMessage.error(result.msg);
|
|
||||||
uaf.error.value = new Error(result.msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(result);
|
|
||||||
},
|
},
|
||||||
isFetching: uaf.isFetching,
|
isFetching: uaf.isFetching,
|
||||||
data: uaf.data,
|
data: uaf.data,
|
||||||
abort: uaf.abort,
|
abort: uaf.abort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let refreshingToken = false;
|
||||||
|
let queue: any[] = [];
|
||||||
|
|
||||||
|
async function execUaf(uaf: any) {
|
||||||
|
try {
|
||||||
|
await uaf.execute(true);
|
||||||
|
} catch (e: any) {
|
||||||
|
const rejectPromise = Promise.reject(e);
|
||||||
|
|
||||||
|
if (e?.name == 'AbortError') {
|
||||||
|
console.log('请求已取消');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const respStatus = uaf.response.value?.status;
|
||||||
|
if (respStatus == 404) {
|
||||||
|
ElMessage.error('请求接口不存在');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
if (respStatus == 500) {
|
||||||
|
ElMessage.error('服务器响应异常');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(e);
|
||||||
|
ElMessage.error('网络请求错误');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Result = uaf.data.value as any;
|
||||||
|
if (!result) {
|
||||||
|
ElMessage.error('网络请求失败');
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultCode = result.code;
|
||||||
|
|
||||||
|
// 如果返回为成功结果,则将结果的data赋值给响应式data
|
||||||
|
if (resultCode === ResultEnum.SUCCESS) {
|
||||||
|
uaf.data.value = result.data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是accessToken失效,则使用refreshToken刷新token
|
||||||
|
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
|
||||||
|
if (refreshingToken) {
|
||||||
|
// 请求加入队列等待, 防止并发多次请求refreshToken
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
queue.push(() => {
|
||||||
|
resolve(execUaf(uaf));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshingToken = true;
|
||||||
|
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
|
||||||
|
saveToken(res.token);
|
||||||
|
saveRefreshToken(res.refresh_token);
|
||||||
|
// 重新缓存后端用户权限code
|
||||||
|
await openApi.getPermissions();
|
||||||
|
|
||||||
|
// 执行accessToken失效的请求
|
||||||
|
queue.forEach((resolve: any) => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
clearUser();
|
||||||
|
} finally {
|
||||||
|
refreshingToken = false;
|
||||||
|
queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await execUaf(uaf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提示没有权限,则跳转至无权限页面
|
||||||
|
if (resultCode === ResultEnum.NO_PERMISSION) {
|
||||||
|
router.push({
|
||||||
|
path: URL_401,
|
||||||
|
});
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
||||||
|
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
|
||||||
|
ElMessage.error(result.msg);
|
||||||
|
uaf.error.value = new Error(result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
|
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
|
||||||
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'"
|
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
|
||||||
@click="onThemeConfigChange" />
|
|
||||||
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
|
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
|
||||||
<transition-group name="breadcrumb" mode="out-in">
|
<transition-group name="breadcrumb" mode="out-in">
|
||||||
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
|
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
|
||||||
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
|
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
|
||||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||||
v-if="themeConfig.isBreadcrumbIcon" />
|
|
||||||
{{ v.meta.title }}
|
{{ v.meta.title }}
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="onBreadcrumbClick(v)">
|
<a v-else @click.prevent="onBreadcrumbClick(v)">
|
||||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||||
v-if="themeConfig.isBreadcrumbIcon" />
|
|
||||||
{{ v.meta.title }}
|
{{ v.meta.title }}
|
||||||
</a>
|
</a>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-search-dialog">
|
<div class="layout-search-dialog">
|
||||||
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
||||||
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
|
<el-autocomplete
|
||||||
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
|
v-model="state.menuQuery"
|
||||||
|
:fetch-suggestions="menuSearch"
|
||||||
|
placeholder="菜单搜索"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
ref="layoutMenuAutocompleteRef"
|
||||||
|
@select="onHandleSelect"
|
||||||
|
@blur="onSearchBlur"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon class="el-input__icon">
|
<el-icon class="el-input__icon">
|
||||||
<search />
|
<search />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div>
|
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
|
||||||
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useRoutesList } from '@/store/routesList';
|
import { useRoutesList } from '@/store/routesList';
|
||||||
|
|
||||||
const layoutMenuAutocompleteRef: any = ref(null);;
|
const layoutMenuAutocompleteRef: any = ref(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state: any = reactive({
|
const state: any = reactive({
|
||||||
isShowSearch: false,
|
isShowSearch: false,
|
||||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
|
|||||||
const createFilter = (queryString: any) => {
|
const createFilter = (queryString: any) => {
|
||||||
return (restaurant: any) => {
|
return (restaurant: any) => {
|
||||||
return (
|
return (
|
||||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
|
|||||||
closeSearch();
|
closeSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({openSearch})
|
defineExpose({ openSearch });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -5,26 +5,39 @@
|
|||||||
<!-- ssh终端主题 -->
|
<!-- ssh终端主题 -->
|
||||||
<el-divider content-position="left">终端主题</el-divider>
|
<el-divider content-position="left">终端主题</el-divider>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
|
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalTheme" size="small" style="width: 140px">
|
||||||
</el-color-picker>
|
<el-option v-for="(_, k) in themes" :key="k" :label="k" :value="k"> </el-option>
|
||||||
|
<el-option label="自定义" value="custom"> </el-option>
|
||||||
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<template v-if="themeConfig.terminalTheme == 'custom'">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
|
||||||
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
</el-color-picker>
|
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
|
||||||
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
|
||||||
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
|
||||||
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
|
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
|
||||||
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -39,7 +52,7 @@
|
|||||||
</el-input-number>
|
</el-input-number>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
|
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
|
||||||
@@ -418,6 +431,7 @@ import { useThemeConfig } from '@/store/themeConfig';
|
|||||||
import { getLightColor } from '@/common/utils/theme';
|
import { getLightColor } from '@/common/utils/theme';
|
||||||
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
|
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
|
||||||
import mittBus from '@/common/utils/mitt';
|
import mittBus from '@/common/utils/mitt';
|
||||||
|
import themes from '@/components/terminal/themes';
|
||||||
|
|
||||||
const copyConfigBtnRef = ref();
|
const copyConfigBtnRef = ref();
|
||||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||||
@@ -615,6 +629,9 @@ const setLocalThemeConfigStyle = () => {
|
|||||||
};
|
};
|
||||||
// 一键复制配置
|
// 一键复制配置
|
||||||
const onCopyConfigClick = (target: any) => {
|
const onCopyConfigClick = (target: any) => {
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let copyThemeConfig = getLocal('themeConfig');
|
let copyThemeConfig = getLocal('themeConfig');
|
||||||
copyThemeConfig.isDrawer = false;
|
copyThemeConfig.isDrawer = false;
|
||||||
const clipboard = new ClipboardJS(target, {
|
const clipboard = new ClipboardJS(target, {
|
||||||
@@ -690,6 +707,25 @@ defineExpose({ openDrawer });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
::v-deep(.el-drawer) {
|
||||||
|
--el-drawer-padding-primary: unset !important;
|
||||||
|
|
||||||
|
.el-drawer__header {
|
||||||
|
padding: 0 15px !important;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.layout-breadcrumb-seting-bar {
|
.layout-breadcrumb-seting-bar {
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
|||||||
@@ -174,12 +174,7 @@ watch(preDark, (newValue) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const switchDark = () => {
|
const switchDark = () => {
|
||||||
themeConfig.value.isDark = isDark.value;
|
themeConfigStore.switchDark(isDark.value);
|
||||||
if (isDark.value) {
|
|
||||||
themeConfig.value.editorTheme = 'vs-dark';
|
|
||||||
} else {
|
|
||||||
themeConfig.value.editorTheme = 'vs';
|
|
||||||
}
|
|
||||||
saveThemeConfig(themeConfig.value);
|
saveThemeConfig(themeConfig.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
|
|||||||
state.tagsRefsIndex = k;
|
state.tagsRefsIndex = k;
|
||||||
try {
|
try {
|
||||||
router.push(v);
|
router.push(v);
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 更新滚动条显示
|
// 更新滚动条显示
|
||||||
const updateScrollbar = () => {
|
const updateScrollbar = () => {
|
||||||
|
|||||||
@@ -89,13 +89,18 @@ type RouterConvCallbackFunc = (router: any) => void;
|
|||||||
* @param meta.link ==> 外链地址
|
* @param meta.link ==> 外链地址
|
||||||
* */
|
* */
|
||||||
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
||||||
if (!routes) return [];
|
if (!routes) {
|
||||||
return routes.map((item: any) => {
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeItems = [];
|
||||||
|
for (let item of routes) {
|
||||||
if (!item.meta) {
|
if (!item.meta) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
// 将json字符串的meta转为对象
|
// 将json字符串的meta转为对象
|
||||||
item.meta = JSON.parse(item.meta);
|
item.meta = JSON.parse(item.meta);
|
||||||
|
|
||||||
// 将meta.comoponet 解析为route.component
|
// 将meta.comoponet 解析为route.component
|
||||||
if (item.meta.component) {
|
if (item.meta.component) {
|
||||||
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
|
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
|
||||||
@@ -126,8 +131,10 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
|
|||||||
// 存在回调,则执行回调
|
// 存在回调,则执行回调
|
||||||
callbackFunc && callbackFunc(item);
|
callbackFunc && callbackFunc(item);
|
||||||
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
|
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
|
||||||
return item;
|
routeItems.push(item);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return routeItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,6 +159,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`未匹配到[${component}]组件名对应的组件文件`);
|
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 终端不需要连接系统websocket消息
|
// 终端不需要连接系统websocket消息
|
||||||
if (to.path != '/machine/terminal') {
|
if (to.path != '/machine/terminal' && to.path != '/machine/terminal-rdp') {
|
||||||
syssocket.init();
|
syssocket.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
|||||||
titleRename: true,
|
titleRename: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/machine/terminal-rdp',
|
||||||
|
name: 'machineTerminalRdp',
|
||||||
|
component: () => import('@/views/ops/machine/RdpTerminalPage.vue'),
|
||||||
|
meta: {
|
||||||
|
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
|
||||||
|
title: '终端 | {name}',
|
||||||
|
// 是否根据query对标题名进行参数替换,即最终显示为‘终端_机器名’
|
||||||
|
titleRename: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 错误页面路由
|
// 错误页面路由
|
||||||
|
|||||||
29
mayfly_go_web/src/store/autoOpenResource.ts
Normal file
29
mayfly_go_web/src/store/autoOpenResource.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动打开资源
|
||||||
|
*/
|
||||||
|
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
||||||
|
state: () => ({
|
||||||
|
autoOpenResource: {
|
||||||
|
machineCodePath: '',
|
||||||
|
dbCodePath: '',
|
||||||
|
redisCodePath: '',
|
||||||
|
mongoCodePath: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setMachineCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.machineCodePath = codePath;
|
||||||
|
},
|
||||||
|
setDbCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.dbCodePath = codePath;
|
||||||
|
},
|
||||||
|
setRedisCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.redisCodePath = codePath;
|
||||||
|
},
|
||||||
|
setMongoCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.mongoCodePath = codePath;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { dateFormat2 } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
import { getSysStyleConfig } from '@/common/sysconfig';
|
import { getSysStyleConfig } from '@/common/sysconfig';
|
||||||
import { getLocal, getThemeConfig } from '@/common/utils/storage';
|
import { getLocal, getThemeConfig } from '@/common/utils/storage';
|
||||||
@@ -114,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
||||||
layout: 'classic',
|
layout: 'classic',
|
||||||
|
|
||||||
|
terminalTheme: 'light',
|
||||||
// ssh终端字体颜色
|
// ssh终端字体颜色
|
||||||
terminalForeground: '#C5C8C6',
|
terminalForeground: '#C5C8C6',
|
||||||
// ssh终端背景色
|
// ssh终端背景色
|
||||||
@@ -190,7 +191,25 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
},
|
},
|
||||||
// 设置水印时间为当前时间
|
// 设置水印时间为当前时间
|
||||||
setWatermarkNowTime() {
|
setWatermarkNowTime() {
|
||||||
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
this.themeConfig.watermarkText[1] = formatDate(new Date());
|
||||||
|
},
|
||||||
|
// 切换暗黑模式
|
||||||
|
switchDark(isDark: boolean) {
|
||||||
|
this.themeConfig.isDark = isDark;
|
||||||
|
// 切换编辑器主题
|
||||||
|
if (isDark) {
|
||||||
|
this.themeConfig.editorTheme = 'vs-dark';
|
||||||
|
} else {
|
||||||
|
this.themeConfig.editorTheme = 'vs';
|
||||||
|
}
|
||||||
|
// 如果终端主题不是自定义主题,则切换主题
|
||||||
|
if (this.themeConfig.terminalTheme != 'custom') {
|
||||||
|
if (isDark) {
|
||||||
|
this.themeConfig.terminalTheme = 'dark';
|
||||||
|
} else {
|
||||||
|
this.themeConfig.terminalTheme = 'light';
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
font-weight: 450;
|
font-weight: 500;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
background-color: var(--bg-main-color);
|
background-color: var(--bg-main-color);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
@import 'common/transition.scss';
|
@use 'common/transition.scss';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import 'mixins/index.scss';
|
@use 'mixins/index' as mixins;
|
||||||
|
|
||||||
/* Button 按钮
|
/* Button 按钮
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@@ -7,335 +7,353 @@
|
|||||||
------------------------------- */
|
------------------------------- */
|
||||||
// 菜单搜索
|
// 菜单搜索
|
||||||
.el-autocomplete-suggestion__wrap {
|
.el-autocomplete-suggestion__wrap {
|
||||||
max-height: 280px !important;
|
max-height: 280px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form 表单
|
/* Form 表单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-form {
|
.el-form {
|
||||||
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
|
||||||
.el-form-item:last-of-type {
|
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
||||||
margin-bottom: 0 !important;
|
.el-form-item:last-of-type {
|
||||||
}
|
margin-bottom: 0 !important;
|
||||||
// 修复行内表单最后一个 el-form-item 位置下移问题
|
}
|
||||||
&.el-form--inline {
|
|
||||||
.el-form-item--large.el-form-item:last-of-type {
|
// 修复行内表单最后一个 el-form-item 位置下移问题
|
||||||
margin-bottom: 22px !important;
|
&.el-form--inline {
|
||||||
}
|
.el-form-item--large.el-form-item:last-of-type {
|
||||||
.el-form-item--default.el-form-item:last-of-type,
|
margin-bottom: 22px !important;
|
||||||
.el-form-item--small.el-form-item:last-of-type {
|
}
|
||||||
margin-bottom: 18px !important;
|
|
||||||
}
|
.el-form-item--default.el-form-item:last-of-type,
|
||||||
}
|
.el-form-item--small.el-form-item:last-of-type {
|
||||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
margin-bottom: 18px !important;
|
||||||
.el-form-item .el-form-item__label .el-icon {
|
}
|
||||||
margin-right: 0px;
|
}
|
||||||
}
|
|
||||||
|
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
||||||
|
.el-form-item .el-form-item__label .el-icon {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert 警告
|
/* Alert 警告
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-alert {
|
.el-alert {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-alert__title {
|
.el-alert__title {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message 消息提示
|
/* Message 消息提示
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-message {
|
.el-message {
|
||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
padding: 15px !important;
|
padding: 15px !important;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NavMenu 导航菜单
|
/* NavMenu 导航菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
// 鼠标 hover 时颜色
|
// 鼠标 hover 时颜色
|
||||||
.el-menu-hover-bg-color {
|
.el-menu-hover-bg-color {
|
||||||
background-color: var(--bg-menuBarActiveColor) !important;
|
background-color: var(--bg-menuBarActiveColor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认样式修改
|
// 默认样式修改
|
||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
height: 56px !important;
|
height: 56px !important;
|
||||||
line-height: 56px !important;
|
line-height: 56px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
color: var(--bg-menuBarColor);
|
color: var(--bg-menuBarColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
width: 64px !important;
|
width: 64px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 外部链接时
|
// 外部链接时
|
||||||
.el-menu-item a,
|
.el-menu-item a,
|
||||||
.el-menu-item a:hover,
|
.el-menu-item a:hover,
|
||||||
.el-menu-item i,
|
.el-menu-item i,
|
||||||
.el-sub-menu__title i {
|
.el-sub-menu__title i {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三方图标字体间距/大小设置
|
// 第三方图标字体间距/大小设置
|
||||||
.el-menu-item .iconfont,
|
.el-menu-item .iconfont,
|
||||||
.el-sub-menu .iconfont,
|
.el-sub-menu .iconfont,
|
||||||
.el-menu-item .fa,
|
.el-menu-item .fa,
|
||||||
.el-sub-menu .fa {
|
.el-sub-menu .fa {
|
||||||
@include generalIcon;
|
@include mixins.generalIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
||||||
.el-menu-item.is-active,
|
.el-menu-item.is-active,
|
||||||
.el-sub-menu.is-active .el-sub-menu__title,
|
.el-sub-menu.is-active .el-sub-menu__title,
|
||||||
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
||||||
@extend .el-menu-hover-bg-color;
|
@extend .el-menu-hover-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover {
|
.el-menu-item:hover {
|
||||||
@extend .el-menu-hover-bg-color;
|
@extend .el-menu-hover-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 子级菜单背景颜色
|
// 子级菜单背景颜色
|
||||||
// .el-menu--inline {
|
// .el-menu--inline {
|
||||||
// background: var(--next-bg-menuBar-light-1);
|
// background: var(--next-bg-menuBar-light-1);
|
||||||
// }
|
// }
|
||||||
// 水平菜单、横向菜单折叠 a 标签
|
// 水平菜单、横向菜单折叠 a 标签
|
||||||
.el-popper.is-dark a {
|
.el-popper.is-dark a {
|
||||||
color: var(--el-color-white) !important;
|
color: var(--el-color-white) !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水平菜单、横向菜单折叠背景色
|
// 水平菜单、横向菜单折叠背景色
|
||||||
.el-popper.is-pure.is-light {
|
.el-popper.is-pure.is-light {
|
||||||
// 水平菜单
|
|
||||||
.el-menu--vertical {
|
// 水平菜单
|
||||||
background: var(--bg-menuBar);
|
.el-menu--vertical {
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
background: var(--bg-menuBar);
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
.el-popper.is-pure.is-light {
|
color: var(--el-menu-active-color);
|
||||||
.el-menu--vertical {
|
}
|
||||||
.el-sub-menu .el-sub-menu__title {
|
|
||||||
background-color: unset !important;
|
.el-popper.is-pure.is-light {
|
||||||
color: var(--bg-menuBarColor);
|
.el-menu--vertical {
|
||||||
}
|
.el-sub-menu .el-sub-menu__title {
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
background-color: unset !important;
|
||||||
color: var(--el-menu-active-color);
|
color: var(--bg-menuBarColor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
}
|
color: var(--el-menu-active-color);
|
||||||
// 横向菜单
|
}
|
||||||
.el-menu--horizontal {
|
}
|
||||||
background: var(--bg-topBar);
|
}
|
||||||
.el-menu-item,
|
}
|
||||||
.el-sub-menu {
|
|
||||||
height: 48px !important;
|
// 横向菜单
|
||||||
line-height: 48px !important;
|
.el-menu--horizontal {
|
||||||
color: var(--bg-topBarColor);
|
background: var(--bg-topBar);
|
||||||
.el-sub-menu__title {
|
|
||||||
height: 48px !important;
|
.el-menu-item,
|
||||||
line-height: 48px !important;
|
.el-sub-menu {
|
||||||
color: var(--bg-topBarColor);
|
height: 48px !important;
|
||||||
}
|
line-height: 48px !important;
|
||||||
.el-popper.is-pure.is-light {
|
color: var(--bg-topBarColor);
|
||||||
.el-menu--horizontal {
|
|
||||||
.el-sub-menu .el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
background-color: unset !important;
|
height: 48px !important;
|
||||||
color: var(--bg-topBarColor);
|
line-height: 48px !important;
|
||||||
}
|
color: var(--bg-topBarColor);
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
}
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-popper.is-pure.is-light {
|
||||||
}
|
.el-menu--horizontal {
|
||||||
}
|
.el-sub-menu .el-sub-menu__title {
|
||||||
}
|
background-color: unset !important;
|
||||||
.el-menu-item.is-active,
|
color: var(--bg-topBarColor);
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
}
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
}
|
color: var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item.is-active,
|
||||||
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
|
color: var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 横向菜单(经典、横向)布局
|
// 横向菜单(经典、横向)布局
|
||||||
.el-menu.el-menu--horizontal {
|
.el-menu.el-menu--horizontal {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
.el-menu-item,
|
|
||||||
.el-sub-menu__title {
|
.el-menu-item,
|
||||||
height: 48px !important;
|
.el-sub-menu__title {
|
||||||
color: var(--bg-topBarColor);
|
height: 48px !important;
|
||||||
}
|
color: var(--bg-topBarColor);
|
||||||
.el-menu-item:not(.is-active):hover,
|
}
|
||||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
|
||||||
color: var(--bg-topBarColor);
|
.el-menu-item:not(.is-active):hover,
|
||||||
}
|
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||||
|
color: var(--bg-topBarColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单收起时,图标不居中问题
|
// 菜单收起时,图标不居中问题
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
.el-menu-item .iconfont,
|
|
||||||
.el-sub-menu .iconfont,
|
.el-menu-item .iconfont,
|
||||||
.el-menu-item .fa,
|
.el-sub-menu .iconfont,
|
||||||
.el-sub-menu .fa {
|
.el-menu-item .fa,
|
||||||
margin-right: 0 !important;
|
.el-sub-menu .fa {
|
||||||
}
|
margin-right: 0 !important;
|
||||||
.el-sub-menu__title {
|
}
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
.el-sub-menu__title {
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs 标签页
|
/* Tabs 标签页
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-tabs__nav-wrap::after {
|
.el-tabs__nav-wrap::after {
|
||||||
height: 1px !important;
|
height: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown 下拉菜单
|
/* Dropdown 下拉菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-dropdown-menu {
|
.el-dropdown-menu {
|
||||||
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
list-style: none !important;
|
||||||
}
|
/*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
||||||
.el-dropdown-menu .el-dropdown-menu__item {
|
|
||||||
white-space: nowrap;
|
|
||||||
&:not(.is-disabled):hover {
|
|
||||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
|
||||||
color: var(--el-dropdown-menuItem-hover-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Steps 步骤条
|
.el-dropdown-menu .el-dropdown-menu__item {
|
||||||
------------------------------- */
|
white-space: nowrap;
|
||||||
.el-step__icon-inner {
|
|
||||||
font-size: 30px !important;
|
&:not(.is-disabled):hover {
|
||||||
font-weight: 400 !important;
|
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||||
}
|
color: var(--el-dropdown-menuItem-hover-color);
|
||||||
.el-step__title {
|
}
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialog 对话框
|
/* Dialog 对话框
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-overlay {
|
.el-overlay {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.el-overlay-dialog {
|
|
||||||
display: flex;
|
.el-overlay-dialog {
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
position: unset !important;
|
justify-content: center;
|
||||||
width: 100%;
|
position: unset !important;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
.el-dialog {
|
height: 100%;
|
||||||
margin: 0 auto !important;
|
|
||||||
position: absolute;
|
.el-dialog {
|
||||||
.el-dialog__body {
|
margin: 0 auto !important;
|
||||||
padding: 20px !important;
|
position: absolute;
|
||||||
}
|
|
||||||
}
|
.el-dialog__body {
|
||||||
}
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: calc(90vh - 111px) !important;
|
max-height: calc(90vh - 111px) !important;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card 卡片
|
/* Card 卡片
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-card__header {
|
.el-card__header {
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table 表格 element plus 2.2.0 版本
|
/* Table 表格 element plus 2.2.0 版本
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-table {
|
.el-table {
|
||||||
.el-button.is-text {
|
.el-button.is-text {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* scrollbar
|
/* scrollbar
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-scrollbar__bar {
|
.el-scrollbar__bar {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
|
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
|
||||||
.el-scrollbar__wrap {
|
.el-scrollbar__wrap {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-select-dropdown .el-scrollbar__wrap {
|
.el-select-dropdown .el-scrollbar__wrap {
|
||||||
overflow-x: scroll !important;
|
overflow-x: scroll !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*修复Select 选择器高度问题*/
|
/*修复Select 选择器高度问题*/
|
||||||
.el-select-dropdown__wrap {
|
.el-select-dropdown__wrap {
|
||||||
max-height: 274px !important;
|
max-height: 274px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*修复Cascader 级联选择器高度问题*/
|
/*修复Cascader 级联选择器高度问题*/
|
||||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
|
.el-cascader-menu__wrap.el-scrollbar__wrap {
|
||||||
height: 204px !important;
|
height: 204px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
|
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
|
||||||
.layout-container-view .el-scrollbar__view {
|
.layout-container-view .el-scrollbar__view {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
|
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
|
||||||
.layout-columns-warp .layout-aside .el-scrollbar__view {
|
.layout-columns-warp .layout-aside .el-scrollbar__view {
|
||||||
height: unset !important;
|
height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination 分页
|
/* Pagination 分页
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-pagination__editor {
|
.el-pagination__editor {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*深色模式时分页高亮问题*/
|
/*深色模式时分页高亮问题*/
|
||||||
.el-pagination.is-background .btn-next.is-active,
|
.el-pagination.is-background .btn-next.is-active,
|
||||||
.el-pagination.is-background .btn-prev.is-active,
|
.el-pagination.is-background .btn-prev.is-active,
|
||||||
.el-pagination.is-background .el-pager li.is-active {
|
.el-pagination.is-background .el-pager li.is-active {
|
||||||
background-color: var(--el-color-primary) !important;
|
background-color: var(--el-color-primary) !important;
|
||||||
color: var(--el-color-white) !important;
|
color: var(--el-color-white) !important;
|
||||||
}
|
|
||||||
|
|
||||||
/* Drawer 抽屉
|
|
||||||
------------------------------- */
|
|
||||||
.el-drawer {
|
|
||||||
--el-drawer-padding-primary: unset !important;
|
|
||||||
.el-drawer__header {
|
|
||||||
padding: 0 15px !important;
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
border-bottom: 1px solid var(--el-border-color);
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Breadcrumb 面包屑
|
/* Breadcrumb 面包屑
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-breadcrumb__inner a:hover,
|
.el-breadcrumb__inner a:hover,
|
||||||
.el-breadcrumb__inner.is-link:hover {
|
.el-breadcrumb__inner.is-link:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-breadcrumb__inner a,
|
.el-breadcrumb__inner a,
|
||||||
.el-breadcrumb__inner.is-link {
|
.el-breadcrumb__inner.is-link {
|
||||||
color: var(--bg-topBarColor);
|
color: var(--bg-topBarColor);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// el-tooltip使用自定义主题时的样式
|
// el-tooltip使用自定义主题时的样式
|
||||||
.el-popper.is-customized {
|
.el-popper.is-customized {
|
||||||
/* Set padding to ensure the height is 32px */
|
/* Set padding to ensure the height is 32px */
|
||||||
// padding: 6px 12px;
|
// padding: 6px 12px;
|
||||||
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
|
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-customized .el-popper__arrow::before {
|
.el-popper.is-customized .el-popper__arrow::before {
|
||||||
background: linear-gradient(45deg, #b2e68d, #bce689);
|
background: linear-gradient(45deg, #b2e68d, #bce689);
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -343,7 +361,9 @@
|
|||||||
|
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 6px; /* 设置圆角 */
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
|
/* 设置圆角 */
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
/* 添加轻微阴影效果 */
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
@import './app.scss';
|
@use './app.scss';
|
||||||
@import './base.scss';
|
@use './base.scss';
|
||||||
@import './other.scss';
|
@use './other.scss';
|
||||||
@import './element.scss';
|
@use './element.scss';
|
||||||
@import './media/media.scss';
|
@use './media/media.scss';
|
||||||
@import './waves.scss';
|
@use './waves.scss';
|
||||||
@import './dark.scss';
|
@use './dark.scss';
|
||||||
@import './iconSelector.scss';
|
@use './iconSelector.scss';
|
||||||
@@ -1,94 +1,109 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
.big-data-down-left {
|
.big-data-down-left {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
flex-direction: unset !important;
|
flex-direction: unset !important;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.flex-warp-item {
|
|
||||||
min-height: 196.24px;
|
.flex-warp-item {
|
||||||
padding: 0 7.5px 15px 15px !important;
|
min-height: 196.24px;
|
||||||
.flex-warp-item-box {
|
padding: 0 7.5px 15px 15px !important;
|
||||||
border: none !important;
|
|
||||||
border-bottom: 1px solid #ebeef5 !important;
|
.flex-warp-item-box {
|
||||||
}
|
border: none !important;
|
||||||
}
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
}
|
}
|
||||||
.big-data-down-center {
|
}
|
||||||
width: 100% !important;
|
}
|
||||||
.big-data-down-center-one,
|
|
||||||
.big-data-down-center-two {
|
.big-data-down-center {
|
||||||
min-height: 196.24px;
|
width: 100% !important;
|
||||||
padding-left: 15px !important;
|
|
||||||
.big-data-down-center-one-content {
|
.big-data-down-center-one,
|
||||||
border: none !important;
|
.big-data-down-center-two {
|
||||||
border-bottom: 1px solid #ebeef5 !important;
|
min-height: 196.24px;
|
||||||
}
|
padding-left: 15px !important;
|
||||||
.flex-warp-item-box {
|
|
||||||
@extend .big-data-down-center-one-content;
|
.big-data-down-center-one-content {
|
||||||
}
|
border: none !important;
|
||||||
}
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
}
|
}
|
||||||
.big-data-down-right {
|
|
||||||
.flex-warp-item {
|
.flex-warp-item-box {
|
||||||
.flex-warp-item-box {
|
@extend .big-data-down-center-one-content;
|
||||||
border: none !important;
|
}
|
||||||
border-bottom: 1px solid #ebeef5 !important;
|
}
|
||||||
}
|
}
|
||||||
&:nth-of-type(2) {
|
|
||||||
padding-left: 15px !important;
|
.big-data-down-right {
|
||||||
}
|
.flex-warp-item {
|
||||||
&:last-of-type {
|
.flex-warp-item-box {
|
||||||
.flex-warp-item-box {
|
border: none !important;
|
||||||
border: none !important;
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
&:nth-of-type(2) {
|
||||||
}
|
padding-left: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
.flex-warp-item-box {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度大于768px小于1200px
|
/* 页面宽度大于768px小于1200px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (min-width: $sm) and (max-width: $lg) {
|
@media screen and (min-width: index.$sm) and (max-width: index.$lg) {
|
||||||
.chart-warp-bottom {
|
.chart-warp-bottom {
|
||||||
.big-data-down-left {
|
.big-data-down-left {
|
||||||
width: 50% !important;
|
width: 50% !important;
|
||||||
}
|
}
|
||||||
.big-data-down-center {
|
|
||||||
width: 50% !important;
|
.big-data-down-center {
|
||||||
}
|
width: 50% !important;
|
||||||
.big-data-down-right {
|
}
|
||||||
.flex-warp-item {
|
|
||||||
width: 50% !important;
|
.big-data-down-right {
|
||||||
&:nth-of-type(2) {
|
.flex-warp-item {
|
||||||
padding-left: 7.5px !important;
|
width: 50% !important;
|
||||||
}
|
|
||||||
}
|
&:nth-of-type(2) {
|
||||||
}
|
padding-left: 7.5px !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度小于1200px
|
/* 页面宽度小于1200px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $lg) {
|
@media screen and (max-width: index.$lg) {
|
||||||
.chart-warp-top {
|
.chart-warp-top {
|
||||||
.up-left {
|
.up-left {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chart-warp-bottom {
|
|
||||||
overflow-y: auto !important;
|
.chart-warp-bottom {
|
||||||
flex-wrap: wrap;
|
overflow-y: auto !important;
|
||||||
.big-data-down-right {
|
flex-wrap: wrap;
|
||||||
width: 100% !important;
|
|
||||||
flex-direction: unset !important;
|
.big-data-down-right {
|
||||||
flex-wrap: wrap;
|
width: 100% !important;
|
||||||
.flex-warp-item {
|
flex-direction: unset !important;
|
||||||
min-height: 196.24px;
|
flex-wrap: wrap;
|
||||||
padding: 0 7.5px 15px 15px !important;
|
|
||||||
}
|
.flex-warp-item {
|
||||||
}
|
min-height: 196.24px;
|
||||||
}
|
padding: 0 7.5px 15px 15px !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于576px
|
/* 页面宽度小于576px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $xs) {
|
@media screen and (max-width: index.$xs) {
|
||||||
.el-cascader__dropdown.el-popper {
|
.el-cascader__dropdown.el-popper {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
@import './index.scss';
|
@use './index.scss';
|
||||||
|
|
||||||
/* 页面宽度小于800px
|
/* 页面宽度小于800px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
width: 90% !important;
|
width: 90% !important;
|
||||||
}
|
}
|
||||||
.el-dialog.is-fullscreen {
|
|
||||||
width: 100% !important;
|
.el-dialog.is-fullscreen {
|
||||||
}
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
.error {
|
.error {
|
||||||
.error-flex {
|
.error-flex {
|
||||||
flex-direction: column-reverse !important;
|
flex-direction: column-reverse !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
.right,
|
|
||||||
.left {
|
.right,
|
||||||
flex: unset !important;
|
.left {
|
||||||
display: flex !important;
|
flex: unset !important;
|
||||||
}
|
display: flex !important;
|
||||||
.left-item {
|
}
|
||||||
margin: auto !important;
|
|
||||||
}
|
.left-item {
|
||||||
.right img {
|
margin: auto !important;
|
||||||
max-width: 450px !important;
|
}
|
||||||
@extend .left-item;
|
|
||||||
}
|
.right img {
|
||||||
}
|
max-width: 450px !important;
|
||||||
|
@extend .left-item;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度大于768px小于992px
|
/* 页面宽度大于768px小于992px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (min-width: $sm) and (max-width: $md) {
|
@media screen and (min-width: index.$sm) and (max-width: index.$md) {
|
||||||
.error {
|
.error {
|
||||||
.error-flex {
|
.error-flex {
|
||||||
padding-left: 30px !important;
|
padding-left: 30px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于576px
|
/* 页面宽度小于576px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $xs) {
|
@media screen and (max-width: index.$xs) {
|
||||||
.el-form-item__label {
|
.el-form-item__label {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
.el-form-item__content {
|
|
||||||
margin-left: 0 !important;
|
.el-form-item__content {
|
||||||
}
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
.home-warning-media,
|
|
||||||
.home-dynamic-media {
|
.home-warning-media,
|
||||||
margin-top: 15px;
|
.home-dynamic-media {
|
||||||
}
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +1,61 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于576px
|
/* 页面宽度小于576px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $xs) {
|
@media screen and (max-width: index.$xs) {
|
||||||
// MessageBox 弹框
|
|
||||||
.el-message-box {
|
// MessageBox 弹框
|
||||||
width: 80% !important;
|
.el-message-box {
|
||||||
}
|
width: 80% !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
// Breadcrumb 面包屑
|
|
||||||
.layout-navbars-breadcrumb-hide {
|
// Breadcrumb 面包屑
|
||||||
display: none;
|
.layout-navbars-breadcrumb-hide {
|
||||||
}
|
display: none;
|
||||||
// 外链视图
|
}
|
||||||
.layout-view-link {
|
|
||||||
a {
|
// 外链视图
|
||||||
max-width: 80%;
|
.layout-view-link {
|
||||||
text-align: center;
|
a {
|
||||||
}
|
max-width: 80%;
|
||||||
}
|
text-align: center;
|
||||||
// 菜单搜索
|
}
|
||||||
.layout-search-dialog {
|
}
|
||||||
.el-autocomplete {
|
|
||||||
width: 80% !important;
|
// 菜单搜索
|
||||||
}
|
.layout-search-dialog {
|
||||||
}
|
.el-autocomplete {
|
||||||
|
width: 80% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度小于1000px
|
/* 页面宽度小于1000px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: 1000px) {
|
@media screen and (max-width: 1000px) {
|
||||||
// 布局配置
|
|
||||||
.layout-drawer-content-flex {
|
// 布局配置
|
||||||
position: relative;
|
.layout-drawer-content-flex {
|
||||||
&::after {
|
position: relative;
|
||||||
content: '手机版不支持切换布局';
|
|
||||||
position: absolute;
|
&::after {
|
||||||
top: 0;
|
content: '手机版不支持切换布局';
|
||||||
right: 0;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
left: 0;
|
right: 0;
|
||||||
z-index: 1;
|
bottom: 0;
|
||||||
text-align: center;
|
left: 0;
|
||||||
height: 140px;
|
z-index: 1;
|
||||||
line-height: 140px;
|
text-align: center;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
height: 140px;
|
||||||
color: #666666;
|
line-height: 140px;
|
||||||
}
|
background: rgba(255, 255, 255, 0.9);
|
||||||
}
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于576px
|
/* 页面宽度小于576px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $xs) {
|
@media screen and (max-width: index.$xs) {
|
||||||
.login-container {
|
.login-container {
|
||||||
.login-content {
|
.login-content {
|
||||||
width: 90% !important;
|
width: 90% !important;
|
||||||
padding: 20px 0 !important;
|
padding: 20px 0 !important;
|
||||||
}
|
}
|
||||||
.login-content-form-btn {
|
|
||||||
width: 100% !important;
|
.login-content-form-btn {
|
||||||
padding: 12px 0 !important;
|
width: 100% !important;
|
||||||
}
|
padding: 12px 0 !important;
|
||||||
.login-copyright {
|
}
|
||||||
.login-copyright-msg {
|
|
||||||
white-space: unset !important;
|
.login-copyright {
|
||||||
}
|
.login-copyright-msg {
|
||||||
}
|
white-space: unset !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
@import './login.scss';
|
@use './login.scss';
|
||||||
@import './error.scss';
|
@use './error.scss';
|
||||||
@import './layout.scss';
|
@use './layout.scss';
|
||||||
@import './personal.scss';
|
@use './personal.scss';
|
||||||
@import './tagsView.scss';
|
@use './tagsView.scss';
|
||||||
@import './home.scss';
|
@use './home.scss';
|
||||||
@import './chart.scss';
|
@use './chart.scss';
|
||||||
@import './form.scss';
|
@use './form.scss';
|
||||||
@import './scrollbar.scss';
|
@use './scrollbar.scss';
|
||||||
@import './pagination.scss';
|
@use './pagination.scss';
|
||||||
@import './dialog.scss';
|
@use './dialog.scss';
|
||||||
@import './cityLinkage.scss';
|
@use './cityLinkage.scss';
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于576px
|
/* 页面宽度小于576px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $xs) {
|
@media screen and (max-width: index.$xs) {
|
||||||
.el-pager,
|
|
||||||
.el-pagination__jump {
|
.el-pager,
|
||||||
display: none !important;
|
.el-pagination__jump {
|
||||||
}
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认居中对齐
|
// 默认居中对齐
|
||||||
.el-pagination {
|
.el-pagination {
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
.personal-info {
|
.personal-info {
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
.personal-recommend-col {
|
|
||||||
margin-bottom: 15px;
|
.personal-recommend-col {
|
||||||
&:last-of-type {
|
margin-bottom: 15px;
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
&:last-of-type {
|
||||||
}
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +1,66 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
// 滚动条的宽度
|
|
||||||
::-webkit-scrollbar {
|
// 滚动条的宽度
|
||||||
width: 3px !important;
|
::-webkit-scrollbar {
|
||||||
height: 3px !important;
|
width: 3px !important;
|
||||||
}
|
height: 3px !important;
|
||||||
::-webkit-scrollbar-track-piece {
|
}
|
||||||
background-color: var(--bg-main-color);
|
|
||||||
}
|
::-webkit-scrollbar-track-piece {
|
||||||
// 滚动条的设置
|
background-color: var(--bg-main-color);
|
||||||
::-webkit-scrollbar-thumb {
|
}
|
||||||
background-color: rgba(144, 147, 153, 0.3);
|
|
||||||
background-clip: padding-box;
|
// 滚动条的设置
|
||||||
min-height: 28px;
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 5px;
|
background-color: rgba(144, 147, 153, 0.3);
|
||||||
transition: 0.3s background-color;
|
background-clip: padding-box;
|
||||||
}
|
min-height: 28px;
|
||||||
::-webkit-scrollbar-thumb:hover {
|
border-radius: 5px;
|
||||||
background-color: rgba(144, 147, 153, 0.5);
|
transition: 0.3s background-color;
|
||||||
}
|
}
|
||||||
// element plus scrollbar
|
|
||||||
.el-scrollbar__bar.is-vertical {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
width: 2px !important;
|
background-color: rgba(144, 147, 153, 0.5);
|
||||||
}
|
}
|
||||||
.el-scrollbar__bar.is-horizontal {
|
|
||||||
height: 2px !important;
|
// element plus scrollbar
|
||||||
}
|
.el-scrollbar__bar.is-vertical {
|
||||||
|
width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-scrollbar__bar.is-horizontal {
|
||||||
|
height: 2px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面宽度大于768px
|
/* 页面宽度大于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (min-width: 769px) {
|
@media screen and (min-width: 769px) {
|
||||||
// 滚动条的宽度
|
|
||||||
::-webkit-scrollbar {
|
// 滚动条的宽度
|
||||||
width: 7px;
|
::-webkit-scrollbar {
|
||||||
height: 7px;
|
width: 7px;
|
||||||
}
|
height: 7px;
|
||||||
::-webkit-scrollbar-track-piece {
|
}
|
||||||
background-color: var(--bg-main-color);
|
|
||||||
}
|
::-webkit-scrollbar-track-piece {
|
||||||
// 滚动条的设置
|
background-color: var(--bg-main-color);
|
||||||
::-webkit-scrollbar-thumb {
|
}
|
||||||
background-color: rgba(144, 147, 153, 0.3);
|
|
||||||
background-clip: padding-box;
|
// 滚动条的设置
|
||||||
min-height: 28px;
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 5px;
|
background-color: rgba(144, 147, 153, 0.3);
|
||||||
transition: 0.3s background-color;
|
background-clip: padding-box;
|
||||||
}
|
min-height: 28px;
|
||||||
::-webkit-scrollbar-thumb:hover {
|
border-radius: 5px;
|
||||||
background-color: rgba(144, 147, 153, 0.5);
|
transition: 0.3s background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(144, 147, 153, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
@import './index.scss';
|
@use './index.scss' as index;
|
||||||
|
|
||||||
/* 页面宽度小于768px
|
/* 页面宽度小于768px
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@media screen and (max-width: $sm) {
|
@media screen and (max-width: index.$sm) {
|
||||||
.tags-view-form {
|
.tags-view-form {
|
||||||
.tags-view-form-col {
|
.tags-view-form-col {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
/* 第三方图标字体间距/大小设置
|
/* 第三方图标字体间距/大小设置
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@mixin generalIcon {
|
@mixin generalIcon {
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文本不换行
|
/* 文本不换行
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@mixin text-no-wrap() {
|
@mixin text-no-wrap() {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 多行文本溢出
|
/* 多行文本溢出
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@mixin text-ellipsis($line: 2) {
|
@mixin text-ellipsis($line: 2) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: $line;
|
-webkit-line-clamp: $line;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条(页面未使用) div 中使用:
|
/* 滚动条(页面未使用) div 中使用:
|
||||||
@@ -35,22 +35,26 @@
|
|||||||
// @include scrollBar;
|
// @include scrollBar;
|
||||||
// }
|
// }
|
||||||
@mixin scrollBar {
|
@mixin scrollBar {
|
||||||
// 滚动条凹槽的颜色,还可以设置边框属性
|
|
||||||
&::-webkit-scrollbar-track-piece {
|
// 滚动条凹槽的颜色,还可以设置边框属性
|
||||||
background-color: #f8f8f8;
|
&::-webkit-scrollbar-track-piece {
|
||||||
}
|
background-color: #f8f8f8;
|
||||||
// 滚动条的宽度
|
}
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 9px;
|
// 滚动条的宽度
|
||||||
height: 9px;
|
&::-webkit-scrollbar {
|
||||||
}
|
width: 9px;
|
||||||
// 滚动条的设置
|
height: 9px;
|
||||||
&::-webkit-scrollbar-thumb {
|
}
|
||||||
background-color: #dddddd;
|
|
||||||
background-clip: padding-box;
|
// 滚动条的设置
|
||||||
min-height: 28px;
|
&::-webkit-scrollbar-thumb {
|
||||||
}
|
background-color: #dddddd;
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
background-clip: padding-box;
|
||||||
background-color: #bbb;
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #bbb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,31 @@
|
|||||||
/* wangeditor富文本编辑器
|
/* wangeditor富文本编辑器
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.w-e-toolbar {
|
.w-e-toolbar {
|
||||||
border: 1px solid #ebeef5 !important;
|
border: 1px solid #ebeef5 !important;
|
||||||
border-bottom: 1px solid #ebeef5 !important;
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 3px;
|
||||||
z-index: 2 !important;
|
z-index: 2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-e-text-container {
|
.w-e-text-container {
|
||||||
border: 1px solid #ebeef5 !important;
|
border: 1px solid #ebeef5 !important;
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* web端自定义截屏
|
/* web端自定义截屏
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
#screenShotContainer {
|
#screenShotContainer {
|
||||||
z-index: 9998 !important;
|
z-index: 9998 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolPanel {
|
#toolPanel {
|
||||||
height: 42px !important;
|
height: 42px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#optionPanel {
|
#optionPanel {
|
||||||
height: 37px !important;
|
height: 37px !important;
|
||||||
}
|
}
|
||||||
1
mayfly_go_web/src/types/pinia.d.ts
vendored
1
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -52,6 +52,7 @@ declare interface ThemeConfigState {
|
|||||||
logoIcon: string;
|
logoIcon: string;
|
||||||
globalI18n: string;
|
globalI18n: string;
|
||||||
globalComponentSize: string;
|
globalComponentSize: string;
|
||||||
|
terminalTheme: string;
|
||||||
terminalForeground: string;
|
terminalForeground: string;
|
||||||
terminalBackground: string;
|
terminalBackground: string;
|
||||||
terminalCursor: string;
|
terminalCursor: string;
|
||||||
|
|||||||
2
mayfly_go_web/src/types/shim.d.ts
vendored
2
mayfly_go_web/src/types/shim.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
// 申明外部 npm 插件模块
|
// 申明外部 npm 插件模块
|
||||||
declare module 'sql-formatter';
|
|
||||||
declare module 'jsoneditor';
|
declare module 'jsoneditor';
|
||||||
declare module 'asciinema-player';
|
declare module 'asciinema-player';
|
||||||
declare module 'vue-grid-layout';
|
declare module 'vue-grid-layout';
|
||||||
declare module 'splitpanes';
|
declare module 'splitpanes';
|
||||||
|
declare module 'uuid';
|
||||||
|
|||||||
@@ -98,4 +98,3 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/router/staticRouter
|
|
||||||
|
|||||||
149
mayfly_go_web/src/views/flow/ProcInstEdit.vue
Executable file
149
mayfly_go_web/src/views/flow/ProcInstEdit.vue
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="bizType" label="业务类型">
|
||||||
|
<EnumSelect v-model="form.bizType" :enums="FlowBizType" placeholder="请选择业务类型" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model.trim="form.remark" type="textarea" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">业务信息</el-divider>
|
||||||
|
<component
|
||||||
|
ref="bizFormRef"
|
||||||
|
v-if="form.bizType"
|
||||||
|
:is="bizComponents[form.bizType]"
|
||||||
|
v-model:bizForm="form.bizForm"
|
||||||
|
@changeResourceCode="changeResourceCode"
|
||||||
|
>
|
||||||
|
</component>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<span v-if="flowProcdef || !state.form.procdefId">
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
|
||||||
|
<ProcdefTasks v-if="flowProcdef" :procdef="flowProcdef" />
|
||||||
|
|
||||||
|
<el-result v-if="!state.form.procdefId" icon="error" title="不存在审批节点" sub-title="该资源无需审批操作"> </el-result>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
|
||||||
|
import { procdefApi, procinstApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { FlowBizType } from './enums';
|
||||||
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
|
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
|
||||||
|
|
||||||
|
const DbSqlExecFlowBizForm = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecFlowBizForm.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
const formRef: any = useTemplateRef('formRef');
|
||||||
|
const bizFormRef: any = useTemplateRef('bizFormRef');
|
||||||
|
|
||||||
|
// 业务组件
|
||||||
|
const bizComponents: any = shallowReactive({
|
||||||
|
db_sql_exec_flow: DbSqlExecFlowBizForm,
|
||||||
|
redis_run_cmd_flow: RedisRunCmdFlowBizForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
bizType: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择流程业务类型',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入申请备注',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
bizType: FlowBizType.DbSqlExec.value,
|
||||||
|
procdefId: -1,
|
||||||
|
status: null,
|
||||||
|
remark: '',
|
||||||
|
bizForm: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tasks: [] as any,
|
||||||
|
form: { ...defaultForm },
|
||||||
|
flowProcdef: null as any,
|
||||||
|
sortable: '' as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form, flowProcdef } = toRefs(state);
|
||||||
|
|
||||||
|
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
|
||||||
|
|
||||||
|
const changeResourceCode = async (resourceType: any, code: string) => {
|
||||||
|
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
||||||
|
if (!state.flowProcdef) {
|
||||||
|
state.form.procdefId = 0;
|
||||||
|
} else {
|
||||||
|
state.form.procdefId = state.flowProcdef.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
await bizFormRef.value.validateBizForm();
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('请正确填写信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await procinstStart();
|
||||||
|
ElMessage.success('流程发起成功');
|
||||||
|
emit('val-change', state.form);
|
||||||
|
//重置表单域
|
||||||
|
cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
state.flowProcdef = null;
|
||||||
|
formRef.value.resetFields();
|
||||||
|
bizFormRef.value.resetBizForm();
|
||||||
|
|
||||||
|
state.form = { ...defaultForm };
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
257
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
257
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
@@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="name" label="名称">
|
||||||
|
<el-input v-model.trim="form.name" placeholder="请输入流程名称" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="defKey" label="key">
|
||||||
|
<el-input :disabled="form.id" v-model.trim="form.defKey" placeholder="请输入流程key" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="status" label="状态">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择状态">
|
||||||
|
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="condition" label="触发条件">
|
||||||
|
<template #label>
|
||||||
|
触发条件
|
||||||
|
<el-tooltip content="go template语法。若输出结果为1,则表示触发该审批流程" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="form.condition"
|
||||||
|
:rows="10"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="触发条件, 返回值=1, 则表示触发该审批流程"
|
||||||
|
auto-complete="off"
|
||||||
|
clearable
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联资源">
|
||||||
|
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
|
||||||
|
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="名称" min-width="100px">
|
||||||
|
<template #header>
|
||||||
|
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addTask()"> </el-button>
|
||||||
|
<span class="ml10">节点名称</span>
|
||||||
|
<el-tooltip content="点击指定节点可进行拖拽排序" placement="top">
|
||||||
|
<el-icon class="ml5">
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input v-model="scope.row.name"> </el-input>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="userId" label="审核人员" min-width="150px" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<AccountSelectFormItem v-model="scope.row.userId" label="" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="60px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="deleteTask(scope.$index)" class="ml5" type="danger" icon="delete" plain></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, ref, nextTick } from 'vue';
|
||||||
|
import { procdefApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
import { randomUuid } from '../../common/utils/string';
|
||||||
|
import { ProcdefStatus } from './enums';
|
||||||
|
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: [Boolean, Object],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
const formRef: any = ref(null);
|
||||||
|
const taskTableRef: any = ref(null);
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入流程名称',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defKey: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入流程key',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tasks: [] as any,
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
defKey: null,
|
||||||
|
status: null,
|
||||||
|
condition: '',
|
||||||
|
remark: null,
|
||||||
|
// 流程的审批节点任务
|
||||||
|
tasks: '',
|
||||||
|
codePaths: [],
|
||||||
|
},
|
||||||
|
sortable: '' as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form, tasks } = toRefs(state);
|
||||||
|
|
||||||
|
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||||
|
|
||||||
|
watch(props, (newValue: any) => {
|
||||||
|
if (newValue.data) {
|
||||||
|
state.form = { ...newValue.data };
|
||||||
|
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||||
|
const tasks = JSON.parse(state.form.tasks);
|
||||||
|
tasks.forEach((t: any) => {
|
||||||
|
t.userId = Number.parseInt(t.userId);
|
||||||
|
});
|
||||||
|
state.tasks = tasks;
|
||||||
|
} else {
|
||||||
|
state.form = { status: ProcdefStatus.Enable.value } as any;
|
||||||
|
state.form.condition = `{{/* DBMS-执行sql规则; param参数描述如下 */}}
|
||||||
|
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||||
|
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||||
|
{{/* 不是select和read语句时,开启流程审批 */}}
|
||||||
|
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||||
|
1
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{/* Redis-执行命令规则; param参数描述如下 */}}
|
||||||
|
{{/* cmdType: read(读命令) / write(写命令); */}}
|
||||||
|
{{/* cmd: get/set/hset...等 */}}
|
||||||
|
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||||
|
{{ if eq .param.cmdType "write" }}
|
||||||
|
1
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}`;
|
||||||
|
state.tasks = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initSort = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const table = taskTableRef.value.$el.querySelector('table > tbody') as any;
|
||||||
|
state.sortable = Sortable.create(table, {
|
||||||
|
animation: 200,
|
||||||
|
//拖拽结束事件
|
||||||
|
onEnd: (evt) => {
|
||||||
|
const curRow = state.tasks.splice(evt.oldIndex, 1)[0];
|
||||||
|
state.tasks.splice(evt.newIndex, 0, curRow);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTask = () => {
|
||||||
|
state.tasks.push({ taskKey: randomUuid() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTask = (idx: any) => {
|
||||||
|
state.tasks.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('请正确填写信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkRes = checkTasks();
|
||||||
|
if (checkRes.err) {
|
||||||
|
ElMessage.error(checkRes.err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.form.tasks = JSON.stringify(checkRes.tasks);
|
||||||
|
await saveFlowDefExec();
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
emit('val-change', state.form);
|
||||||
|
//重置表单域
|
||||||
|
formRef.value.resetFields();
|
||||||
|
state.form = {} as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTasks = () => {
|
||||||
|
if (state.tasks?.length == 0) {
|
||||||
|
return { err: '请完善审批节点任务' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
for (let i = 0; i < state.tasks.length; i++) {
|
||||||
|
const task = { ...state.tasks[i] };
|
||||||
|
if (!task.name || !task.userId) {
|
||||||
|
return { err: `请完善第${i + 1}个审批节点任务信息` };
|
||||||
|
}
|
||||||
|
// 转为字符串(方便后续万一需要调整啥的)
|
||||||
|
task.userId = `${task.userId}`;
|
||||||
|
if (!task.taskKey) {
|
||||||
|
task.taskKey = randomUuid();
|
||||||
|
}
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tasks: tasks };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
147
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
147
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procdefApi.list"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button v-auth="perms.save" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button>
|
||||||
|
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="deleteProcdef()" type="danger" icon="delete">删除</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tasks="{ data }">
|
||||||
|
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #codePaths="{ data }">
|
||||||
|
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="flowTasksDialog.visible" :title="flowTasksDialog.title">
|
||||||
|
<procdef-tasks :tasks="flowTasksDialog.tasks" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<procdef-edit v-model:visible="flowDefEditor.visible" :title="flowDefEditor.title" v-model:data="flowDefEditor.data" @val-change="valChange()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||||
|
import { procdefApi } from './api';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcdefEdit from './ProcdefEdit.vue';
|
||||||
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
|
import { ProcdefStatus } from './enums';
|
||||||
|
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
save: 'flow:procdef:save',
|
||||||
|
del: 'flow:procdef:del',
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.input('name', '名称'), SearchItem.input('defKey', 'key')];
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('name', '名称'),
|
||||||
|
TableColumn.new('defKey', 'key'),
|
||||||
|
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
|
||||||
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
|
||||||
|
TableColumn.new('codePaths', '关联资源').isSlot().setMinWidth('250px'),
|
||||||
|
TableColumn.new('creator', '创建账号'),
|
||||||
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 该用户拥有的的操作列按钮权限
|
||||||
|
const actionBtns: any = hasPerms([perms.save, perms.del]);
|
||||||
|
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
name: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
flowDefEditor: {
|
||||||
|
title: '新建流程定义',
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
},
|
||||||
|
flowTasksDialog: {
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
tasks: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, flowDefEditor, flowTasksDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
|
columns.push(actionColumn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcdefTasks = (procdef: any) => {
|
||||||
|
state.flowTasksDialog.tasks = procdef.tasks;
|
||||||
|
state.flowTasksDialog.title = procdef.name + '-审批节点';
|
||||||
|
state.flowTasksDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editFlowDef = (data: any) => {
|
||||||
|
if (!data) {
|
||||||
|
state.flowDefEditor.data = null;
|
||||||
|
state.flowDefEditor.title = '新建流程定义';
|
||||||
|
} else {
|
||||||
|
state.flowDefEditor.data = data;
|
||||||
|
state.flowDefEditor.title = '编辑流程定义';
|
||||||
|
}
|
||||||
|
state.flowDefEditor.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.flowDefEditor.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProcdef = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】的流程定义?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
search();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
163
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
163
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="50%" :close-on-click-modal="!props.instTaskId">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">流程信息</el-divider>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="业务">
|
||||||
|
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发起人">
|
||||||
|
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="流程状态">
|
||||||
|
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="业务状态">
|
||||||
|
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<div v-if="procinst.duration">
|
||||||
|
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-descriptions-item label="备注">
|
||||||
|
{{ procinst.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
<procdef-tasks :tasks="procinst?.procdef?.tasks" :procinst-tasks="procinst.procinstTasks" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">业务信息</el-divider>
|
||||||
|
<component v-if="procinst.bizType" ref="keyValueRef" :is="bizComponents[procinst.bizType]" :procinst="procinst"> </component>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.instTaskId">
|
||||||
|
<el-divider content-position="left">审批表单</el-divider>
|
||||||
|
<el-form :model="form" label-width="auto">
|
||||||
|
<el-form-item prop="status" label="结果" required>
|
||||||
|
<el-select v-model="form.status" placeholder="请选择审批结果">
|
||||||
|
<el-option :label="ProcinstTaskStatus.Pass.label" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
||||||
|
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
|
||||||
|
<el-option :label="ProcinstTaskStatus.Reject.label" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model.trim="form.remark" placeholder="备注" type="textarea" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer v-if="props.instTaskId">
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
||||||
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecBiz.vue'));
|
||||||
|
const RedisRunCmdBiz = defineAsyncComponent(() => import('./flowbiz/redis/RedisRunCmdBiz.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
procinstId: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
// 流程实例任务id(存在则展示审批相关信息)
|
||||||
|
instTaskId: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
// 业务组件
|
||||||
|
const bizComponents: any = shallowReactive({
|
||||||
|
db_sql_exec_flow: DbSqlExecBiz,
|
||||||
|
redis_run_cmd_flow: RedisRunCmdBiz,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
procinst: {} as any,
|
||||||
|
tasks: [] as any,
|
||||||
|
form: {
|
||||||
|
status: ProcinstTaskStatus.Pass.value,
|
||||||
|
remark: '',
|
||||||
|
},
|
||||||
|
saveBtnLoading: false,
|
||||||
|
sortable: '' as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { procinst, form, saveBtnLoading } = toRefs(state);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinstId,
|
||||||
|
async (newValue: any) => {
|
||||||
|
if (newValue) {
|
||||||
|
state.procinst = await procinstApi.detail.request({ id: newValue });
|
||||||
|
} else {
|
||||||
|
state.procinst = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
const status = state.form.status;
|
||||||
|
let api = procinstApi.completeTask;
|
||||||
|
if (status === ProcinstTaskStatus.Back.value) {
|
||||||
|
api = procinstApi.backTask;
|
||||||
|
} else if (status === ProcinstTaskStatus.Reject.value) {
|
||||||
|
api = procinstApi.rejectTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.saveBtnLoading = true;
|
||||||
|
await api.request({ id: props.instTaskId, remark: state.form.remark });
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
cancel();
|
||||||
|
emit('val-change');
|
||||||
|
} finally {
|
||||||
|
state.saveBtnLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
137
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
137
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procinstApi.list"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" icon="plus" @click="startProcInst()">发起流程</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link @click="showProcinst(data)" type="primary">查看</el-button>
|
||||||
|
|
||||||
|
<el-popconfirm
|
||||||
|
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
|
||||||
|
title="确认取消该流程?"
|
||||||
|
width="160"
|
||||||
|
@confirm="procinstCancel(data)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="warning">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<ProcinstDetail
|
||||||
|
v-model:visible="procinstDetail.visible"
|
||||||
|
:title="procinstDetail.title"
|
||||||
|
:procinst-id="procinstDetail.procinstId"
|
||||||
|
:inst-task-id="procinstDetail.instTaskId"
|
||||||
|
@val-change="valChange()"
|
||||||
|
@cancel="procinstDetail.procinstId = 0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
|
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
import ProcInstEdit from './ProcInstEdit.vue';
|
||||||
|
|
||||||
|
const searchItems = [
|
||||||
|
SearchItem.select('status', '流程状态').withEnum(ProcinstStatus),
|
||||||
|
SearchItem.select('bizType', '业务类型').withEnum(FlowBizType),
|
||||||
|
SearchItem.input('bizKey', '业务key'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
|
||||||
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('creator', '发起人'),
|
||||||
|
TableColumn.new('bizKey', '业务key'),
|
||||||
|
TableColumn.new('procdefName', '流程名'),
|
||||||
|
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
|
||||||
|
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
|
||||||
|
TableColumn.new('createTime', '发起时间').isTime(),
|
||||||
|
TableColumn.new('endTime', '结束时间').isTime(),
|
||||||
|
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||||
|
const duration = data[prop];
|
||||||
|
if (!duration) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatTime(duration);
|
||||||
|
}),
|
||||||
|
// TableColumn.new('bizHandleRes', '业务处理结果'),
|
||||||
|
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
status: null,
|
||||||
|
bizType: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
procinstDetail: {
|
||||||
|
title: '查看流程',
|
||||||
|
visible: false,
|
||||||
|
procinstId: 0,
|
||||||
|
instTaskId: 0,
|
||||||
|
},
|
||||||
|
procinstEdit: {
|
||||||
|
title: '发起流程',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, procinstDetail, procinstEdit } = toRefs(state);
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const procinstCancel = async (data: any) => {
|
||||||
|
await procinstApi.cancel.request({ id: data.id });
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcinst = (data: any) => {
|
||||||
|
state.procinstDetail.procinstId = data.id;
|
||||||
|
state.procinstDetail.title = '流程查看';
|
||||||
|
state.procinstDetail.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startProcInst = () => {
|
||||||
|
state.procinstEdit.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.procinstDetail.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
111
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
111
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procinstApi.tasks"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link @click="showProcinst(data, false)" type="primary">查看</el-button>
|
||||||
|
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="showProcinst(data, true)" type="primary">审核</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<ProcinstDetail
|
||||||
|
v-model:visible="procinstDetail.visible"
|
||||||
|
:title="procinstDetail.title"
|
||||||
|
:procinst-id="procinstDetail.procinstId"
|
||||||
|
:inst-task-id="procinstDetail.instTaskId"
|
||||||
|
@val-change="valChange()"
|
||||||
|
@cancel="procinstDetail.procinstId = 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
|
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.select('status', '任务状态').withEnum(ProcinstTaskStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('procinst.bizType', '业务').typeTag(FlowBizType),
|
||||||
|
TableColumn.new('procinst.remark', '备注'),
|
||||||
|
TableColumn.new('procinst.creator', '发起人'),
|
||||||
|
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
|
||||||
|
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
|
||||||
|
TableColumn.new('procinst.bizKey', '业务key'),
|
||||||
|
TableColumn.new('procinst.procdefName', '流程名'),
|
||||||
|
TableColumn.new('taskName', '当前节点'),
|
||||||
|
TableColumn.new('procinst.createTime', '发起时间').isTime(),
|
||||||
|
TableColumn.new('createTime', '开始时间').isTime(),
|
||||||
|
TableColumn.new('endTime', '结束时间').isTime(),
|
||||||
|
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||||
|
const duration = data[prop];
|
||||||
|
if (!duration) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatTime(duration);
|
||||||
|
}),
|
||||||
|
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
status: ProcinstTaskStatus.Process.value,
|
||||||
|
bizType: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
procinstDetail: {
|
||||||
|
title: '查看流程',
|
||||||
|
visible: false,
|
||||||
|
procinstId: 0,
|
||||||
|
instTaskId: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcinst = (data: any, audit: boolean) => {
|
||||||
|
state.procinstDetail.procinstId = data.procinstId;
|
||||||
|
if (!audit) {
|
||||||
|
state.procinstDetail.instTaskId = 0;
|
||||||
|
state.procinstDetail.title = '流程查看';
|
||||||
|
} else {
|
||||||
|
state.procinstDetail.instTaskId = data.id;
|
||||||
|
state.procinstDetail.title = '流程审批';
|
||||||
|
}
|
||||||
|
state.procinstDetail.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.procinstDetail.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
21
mayfly_go_web/src/views/flow/api.ts
Normal file
21
mayfly_go_web/src/views/flow/api.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
|
export const procdefApi = {
|
||||||
|
list: Api.newGet('/flow/procdefs'),
|
||||||
|
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
|
||||||
|
save: Api.newPost('/flow/procdefs'),
|
||||||
|
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const procinstApi = {
|
||||||
|
list: Api.newGet('/flow/procinsts'),
|
||||||
|
start: Api.newPost('/flow/procinsts/start'),
|
||||||
|
detail: Api.newGet('/flow/procinsts/{id}'),
|
||||||
|
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
|
||||||
|
tasks: Api.newGet('/flow/procinsts/tasks'),
|
||||||
|
completeTask: Api.newPost('/flow/procinsts/tasks/complete'),
|
||||||
|
backTask: Api.newPost('/flow/procinsts/tasks/back'),
|
||||||
|
rejectTask: Api.newPost('/flow/procinsts/tasks/reject'),
|
||||||
|
save: Api.newPost('/flow/procdefs'),
|
||||||
|
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-form-item :label="props.label">
|
||||||
|
<el-select style="width: 100%" v-model="procdefKey" filterable placeholder="绑定流程则开启对应审批流程" v-bind="$attrs" clearable>
|
||||||
|
<el-option v-for="item in procdefs" :key="item.defKey" :label="`${item.defKey} [${item.name}]`" :value="item.defKey"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { procdefApi } from '../api';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '工单流程',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getProcdefs();
|
||||||
|
});
|
||||||
|
|
||||||
|
const procdefKey = defineModel('modelValue');
|
||||||
|
|
||||||
|
const procdefs: any = ref([]);
|
||||||
|
|
||||||
|
const getProcdefs = () => {
|
||||||
|
procdefApi.list.request({ pageSize: 200 }).then((res) => {
|
||||||
|
procdefs.value = res.list;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
134
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
134
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<el-steps align-center :active="stepActive">
|
||||||
|
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
||||||
|
<template #description>
|
||||||
|
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
||||||
|
<div v-if="task.completeTime">{{ `${formatDate(task.completeTime)}` }}</div>
|
||||||
|
<div v-if="task.remark">{{ task.remark }}</div>
|
||||||
|
</template>
|
||||||
|
</el-step>
|
||||||
|
</el-steps>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import { accountApi } from '../../system/api';
|
||||||
|
import { ProcinstTaskStatus } from '../enums';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import { ElSteps, ElStep } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 流程定义任务
|
||||||
|
tasks: {
|
||||||
|
type: [String, Object],
|
||||||
|
},
|
||||||
|
procdef: {
|
||||||
|
type: [Object],
|
||||||
|
},
|
||||||
|
// 流程实例任务列表
|
||||||
|
procinstTasks: {
|
||||||
|
type: [Array],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tasksArr: [] as any,
|
||||||
|
stepActive: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tasksArr, stepActive } = toRefs(state);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.tasks,
|
||||||
|
(newValue: any) => {
|
||||||
|
parseTasks(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinstTasks,
|
||||||
|
() => {
|
||||||
|
parseTasks(props.tasks);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procdef,
|
||||||
|
async (newValue: any) => {
|
||||||
|
if (newValue) {
|
||||||
|
parseTasksByKey(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.procdef) {
|
||||||
|
parseTasksByKey(props.procdef);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseTasks(props.tasks);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseTasksByKey = async (procdef: any) => {
|
||||||
|
parseTasks(procdef.tasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTasks = async (tasksStr: any) => {
|
||||||
|
if (!tasksStr) return;
|
||||||
|
const tasks = JSON.parse(tasksStr);
|
||||||
|
const userIds = tasks.map((x: any) => x.userId);
|
||||||
|
const usersRes = await accountApi.querySimple.request({ ids: [...new Set(userIds)].join(','), pageSize: 50 });
|
||||||
|
const users = usersRes.list;
|
||||||
|
// 将数组转换为 Map 结构,以 id 为 key
|
||||||
|
const userMap = users.reduce((acc: any, obj: any) => {
|
||||||
|
acc.set(obj.id, obj);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
// 流程实例任务(用于显示完成时间,完成到哪一步等)
|
||||||
|
let instTasksMap: any;
|
||||||
|
if (props.procinstTasks) {
|
||||||
|
state.stepActive = props.procinstTasks.length - 1;
|
||||||
|
instTasksMap = props.procinstTasks.reduce((acc: any, obj: any) => {
|
||||||
|
acc.set(obj.taskKey, obj);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let task of tasks) {
|
||||||
|
const user = userMap.get(Number.parseInt(task.userId));
|
||||||
|
task.accountUsername = user.username;
|
||||||
|
task.accountName = user.name;
|
||||||
|
|
||||||
|
// 存在实例任务,则赋值实例任务对应的完成时间和备注
|
||||||
|
const instTask = instTasksMap?.get(task.taskKey);
|
||||||
|
if (instTask) {
|
||||||
|
task.status = instTask.status;
|
||||||
|
task.completeTime = instTask.endTime;
|
||||||
|
task.remark = instTask.remark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tasksArr = tasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStepStatus = (task: any): any => {
|
||||||
|
const taskStatus = task.status;
|
||||||
|
if (!taskStatus) {
|
||||||
|
return 'wait';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Pass.value) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Process.value) {
|
||||||
|
return 'proccess';
|
||||||
|
}
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Back.value || taskStatus == ProcinstTaskStatus.Reject.value) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'wait';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const ProcdefStatus = {
|
||||||
|
Enable: EnumValue.of(1, '启用').setTagType('success'),
|
||||||
|
Disable: EnumValue.of(-1, '禁用').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstStatus = {
|
||||||
|
Active: EnumValue.of(1, '执行中').setTagType('primary'),
|
||||||
|
Completed: EnumValue.of(2, '完成').setTagType('success'),
|
||||||
|
Suspended: EnumValue.of(-1, '挂起').setTagType('warning'),
|
||||||
|
Terminated: EnumValue.of(-2, '终止').setTagType('danger'),
|
||||||
|
Cancelled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstBizStatus = {
|
||||||
|
Wait: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||||
|
Success: EnumValue.of(2, '处理成功').setTagType('success'),
|
||||||
|
Fail: EnumValue.of(-2, '处理失败').setTagType('danger'),
|
||||||
|
No: EnumValue.of(-1, '不处理').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstTaskStatus = {
|
||||||
|
Process: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||||
|
Pass: EnumValue.of(2, '通过').setTagType('success'),
|
||||||
|
Reject: EnumValue.of(-1, '拒绝').setTagType('danger'),
|
||||||
|
Back: EnumValue.of(-2, '驳回').setTagType('warning'),
|
||||||
|
Canceled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FlowBizType = {
|
||||||
|
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL').setTagType('warning'),
|
||||||
|
RedisRunWriteCmd: EnumValue.of('redis_run_cmd_flow', 'Redis-执行命令').setTagType('danger'),
|
||||||
|
};
|
||||||
103
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecBiz.vue
Executable file
103
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecBiz.vue
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="主机">
|
||||||
|
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />
|
||||||
|
{{ `${db?.host}:${db?.port}` }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="数据库">{{ dbName }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="执行SQL">
|
||||||
|
<monaco-editor height="300px" language="sql" v-model="sql" :options="{ readOnly: true }" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="runRes && runRes.length > 0">
|
||||||
|
<el-divider content-position="left">处理结果</el-divider>
|
||||||
|
<el-table :data="runRes" :max-height="400">
|
||||||
|
<el-table-column prop="sql" label="SQL" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="res" label="执行结果" :min-width="30" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-popover placement="top" :width="400" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<el-link icon="view" :type="scope.row.errorMsg ? 'danger' : 'success'" :underline="false"> </el-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-text v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</el-text>
|
||||||
|
<el-table v-else :data="scope.row.res" size="small">
|
||||||
|
<el-table-column v-for="col in scope.row.columns" :key="col.name" :label="col.name" :prop="col.name" />
|
||||||
|
</el-table>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- <el-table-column prop="errorMsg" label="错误信息" :min-width="60" show-overflow-tooltip /> -->
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
procinst: {
|
||||||
|
type: [Object],
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
// sqlExec: {
|
||||||
|
// sql: '',
|
||||||
|
// } as any,
|
||||||
|
db: {} as any,
|
||||||
|
dbName: '',
|
||||||
|
sql: '',
|
||||||
|
runRes: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { db, dbName, sql, runRes } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
parseBizForm(props.procinst.bizForm);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinst.bizForm,
|
||||||
|
(newValue: any) => {
|
||||||
|
parseBizForm(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseBizForm = async (bizFormStr: string) => {
|
||||||
|
if (props.procinst.bizHandleRes) {
|
||||||
|
state.runRes = JSON.parse(props.procinst.bizHandleRes);
|
||||||
|
} else {
|
||||||
|
state.runRes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bizForm = JSON.parse(bizFormStr);
|
||||||
|
state.sql = bizForm.sql;
|
||||||
|
state.dbName = bizForm.dbName;
|
||||||
|
|
||||||
|
const dbRes = await dbApi.dbs.request({ id: bizForm.dbId });
|
||||||
|
state.db = dbRes.list?.[0];
|
||||||
|
|
||||||
|
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
|
||||||
|
state.db.codePaths = res.map((item: any) => item.codePath);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
83
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecFlowBizForm.vue
Executable file
83
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecFlowBizForm.vue
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="dbId" label="数据库" required>
|
||||||
|
<db-select-tree
|
||||||
|
placeholder="请选择数据库"
|
||||||
|
v-model:db-id="bizForm.dbId"
|
||||||
|
v-model:db-name="bizForm.dbName"
|
||||||
|
v-model:db-type="dbType"
|
||||||
|
@select-db="changeResourceCode"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="sql" label="SQL" required>
|
||||||
|
<div class="w100">
|
||||||
|
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
dbId: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择数据库',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sql: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入执行SQL',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['changeResourceCode']);
|
||||||
|
|
||||||
|
const formRef: any = ref(null);
|
||||||
|
|
||||||
|
const bizForm = defineModel<any>('bizForm', {
|
||||||
|
default: {
|
||||||
|
dbId: 0,
|
||||||
|
dbName: '',
|
||||||
|
sql: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbType = ref('');
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => bizForm.value.dbId,
|
||||||
|
() => {
|
||||||
|
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeResourceCode = async (db: any) => {
|
||||||
|
emit('changeResourceCode', TagResourceTypeEnum.Db.value, db.code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBizForm = async () => {
|
||||||
|
return formRef.value.validate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBizForm = () => {
|
||||||
|
//重置表单域
|
||||||
|
formRef.value.resetFields();
|
||||||
|
bizForm.value.dbId = 0;
|
||||||
|
bizForm.value.dbName = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ validateBizForm, resetBizForm });
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
89
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdBiz.vue
Executable file
89
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdBiz.vue
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="mode">
|
||||||
|
{{ redis.mode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="执行Cmd">
|
||||||
|
<el-input type="textarea" disabled v-model="cmd" rows="5" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="runRes && runRes.length > 0">
|
||||||
|
<el-divider content-position="left">处理结果</el-divider>
|
||||||
|
<el-table :data="runRes" :max-height="400">
|
||||||
|
<el-table-column prop="cmd" label="命令" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="res" label="执行结果" :min-width="50" show-overflow-tooltip> </el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
|
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||||
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
procinst: {
|
||||||
|
type: [Object],
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
cmd: '',
|
||||||
|
runRes: [],
|
||||||
|
db: 0,
|
||||||
|
redis: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cmd, redis, runRes } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
parseRunCmdForm(props.procinst.bizForm);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinst.bizForm,
|
||||||
|
(newValue: any) => {
|
||||||
|
parseRunCmdForm(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseRunCmdForm = async (bizFormStr: string) => {
|
||||||
|
if (props.procinst.bizHandleRes) {
|
||||||
|
state.runRes = JSON.parse(props.procinst.bizHandleRes);
|
||||||
|
} else {
|
||||||
|
state.runRes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bizFormStr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bizForm = JSON.parse(bizFormStr);
|
||||||
|
state.cmd = bizForm.cmd;
|
||||||
|
state.db = bizForm.db;
|
||||||
|
|
||||||
|
const res = await redisApi.redisList.request({ id: bizForm.id });
|
||||||
|
if (!res.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.redis = res.list?.[0];
|
||||||
|
|
||||||
|
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
|
||||||
|
state.redis.codePaths = res.map((item: any) => item.codePath);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
148
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdFlowBizForm.vue
Executable file
148
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdFlowBizForm.vue
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="id" label="库" required>
|
||||||
|
<TagTreeResourceSelect
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-model="selectRedis"
|
||||||
|
@change="changeRedis"
|
||||||
|
:resource-type="TagResourceTypeEnum.Redis.value"
|
||||||
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
|
placeholder="请选择Redis实例与库"
|
||||||
|
>
|
||||||
|
</TagTreeResourceSelect>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="cmd" label="CMD" required>
|
||||||
|
<el-input type="textarea" v-model="bizForm.cmd" placeholder="如: SET 'key' 'value'; 多条命令;分割" :rows="5" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
||||||
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
id: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择Redis实例',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cmd: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入执行CMD',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// tagpath 节点类型
|
||||||
|
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const res = await redisApi.redisList.request({ tagPath: parentNode.key });
|
||||||
|
if (!res.total) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisInfos = res.list;
|
||||||
|
await sleep(100);
|
||||||
|
return redisInfos.map((x: any) => {
|
||||||
|
x.tagPath = parentNode.key;
|
||||||
|
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// redis实例节点类型
|
||||||
|
const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
|
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
||||||
|
id: redisInfo.id,
|
||||||
|
db: x,
|
||||||
|
name: `db${x}`,
|
||||||
|
keys: 0,
|
||||||
|
tagPath: redisInfo.tagPath,
|
||||||
|
redisName: redisInfo.name,
|
||||||
|
code: redisInfo.code,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redisInfo.mode == 'cluster') {
|
||||||
|
return dbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
|
||||||
|
for (let db in res.Keyspace) {
|
||||||
|
for (let d of dbs) {
|
||||||
|
if (db == d.params.name) {
|
||||||
|
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 替换label
|
||||||
|
dbs.forEach((e: any) => {
|
||||||
|
e.label = `${e.params.name}`;
|
||||||
|
});
|
||||||
|
return dbs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['changeResourceCode']);
|
||||||
|
|
||||||
|
const formRef: any = ref(null);
|
||||||
|
|
||||||
|
const bizForm = defineModel<any>('bizForm', {
|
||||||
|
default: {
|
||||||
|
id: 0,
|
||||||
|
db: 0,
|
||||||
|
cmd: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const redisName = ref('');
|
||||||
|
const tagPath = ref('');
|
||||||
|
|
||||||
|
const selectRedis = computed({
|
||||||
|
get: () => {
|
||||||
|
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
|
||||||
|
},
|
||||||
|
set: () => {
|
||||||
|
//
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeRedis = (nodeData: TagTreeNode) => {
|
||||||
|
const params = nodeData.params;
|
||||||
|
tagPath.value = params.tagPath;
|
||||||
|
redisName.value = params.redisName;
|
||||||
|
bizForm.value.id = params.id;
|
||||||
|
bizForm.value.db = parseInt(params.db);
|
||||||
|
|
||||||
|
changeResourceCode(params.code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeResourceCode = async (redisCode: any) => {
|
||||||
|
emit('changeResourceCode', TagResourceTypeEnum.Redis.value, redisCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBizForm = async () => {
|
||||||
|
return formRef.value.validate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBizForm = () => {
|
||||||
|
//重置表单域
|
||||||
|
formRef.value.resetFields();
|
||||||
|
bizForm.value.id = 0;
|
||||||
|
bizForm.value.db = 0;
|
||||||
|
bizForm.value.cmd = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ validateBizForm, resetBizForm });
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,121 +1,587 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container personal">
|
||||||
<el-row :gutter="15">
|
<el-row :gutter="15">
|
||||||
<el-col :sm="6" class="mb15">
|
<!-- 个人信息 -->
|
||||||
<div @click="toPage({ id: 'personal' })" class="home-card-item home-card-first">
|
<el-col :xs="24" :sm="16">
|
||||||
<div class="flex-margin flex">
|
<el-card shadow="hover" header="个人信息">
|
||||||
<img :src="userInfo.photo" />
|
<div class="personal-user">
|
||||||
<div class="home-card-first-right ml15">
|
<div class="personal-user-left">
|
||||||
<div class="flex-margin">
|
<el-upload
|
||||||
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div>
|
class="h100 personal-user-left-upload"
|
||||||
</div>
|
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
|
||||||
|
:limit="1"
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="beforeAvatarUpload"
|
||||||
|
:on-success="handleAvatarSuccess"
|
||||||
|
accept=".png,.jpg,.jpeg"
|
||||||
|
>
|
||||||
|
<img :src="userInfo.photo" />
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<div class="personal-user-right">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" class="personal-title mb18"
|
||||||
|
>{{ currentTime }},{{ userInfo.name }},生活变的再糟糕,也不妨碍我变得更好!
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">用户名:</div>
|
||||||
|
<div class="personal-item-value">{{ userInfo.username }}</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">角色:</div>
|
||||||
|
<div class="personal-item-value">{{ roleInfo }}</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">上次登录IP:</div>
|
||||||
|
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">上次登录时间:</div>
|
||||||
|
<div class="personal-item-value">{{ formatDate(userInfo.lastLoginTime) }}</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList as any" :key="k">
|
|
||||||
<div @click="toPage(v)" class="home-card-item home-card-item-box" :style="{ background: v.color }">
|
<!-- 消息通知 -->
|
||||||
<div class="home-card-item-flex">
|
<el-col :xs="24" :sm="8" class="pl15 personal-info">
|
||||||
<div class="home-card-item-title pb3">{{ v.title }}</div>
|
<el-card shadow="hover">
|
||||||
<div class="home-card-item-title-num pb6" :id="v.id"></div>
|
<template #header>
|
||||||
|
<span>消息通知</span>
|
||||||
|
<span @click="showMsgs" class="personal-info-more">更多</span>
|
||||||
|
</template>
|
||||||
|
<div class="personal-info-box">
|
||||||
|
<ul class="personal-info-ul">
|
||||||
|
<li v-for="(v, k) in state.msgs as any" :key="k" class="personal-info-li">
|
||||||
|
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
|
</el-card>
|
||||||
</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="mt20 resource-info">
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('machine')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Machine.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Machine.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.machine.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" :tagInfos="state.machine.tagInfos" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('machine', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('db')">
|
||||||
|
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
|
||||||
|
<span class="">{{ state.db.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" :tagInfos="state.db.tagInfos" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('db', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="mt20 resource-info">
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('redis')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Redis.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Redis.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.redis.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" :tagInfos="state.redis.tagInfos" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('redis', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('mongo')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Mongo.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Mongo.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.mongo.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" :tagInfos="state.mongo.tagInfos" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('mongo', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
|
||||||
|
<el-table border :data="msgDialog.msgs.list" size="small">
|
||||||
|
<el-table-column property="type" label="类型" width="60">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ getMsgTypeDesc(scope.row.type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="msg" label="消息"></el-table-column>
|
||||||
|
<el-table-column property="createTime" label="时间" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-row type="flex" class="mt5" justify="center">
|
||||||
|
<el-pagination
|
||||||
|
small
|
||||||
|
@current-change="searchMsg"
|
||||||
|
style="text-align: center"
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, total, jumper"
|
||||||
|
:total="msgDialog.msgs.total"
|
||||||
|
v-model:current-page="msgDialog.query.pageNum"
|
||||||
|
:page-size="msgDialog.query.pageSize"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
|
import { computed, onMounted, reactive, toRefs } from 'vue';
|
||||||
// import * as echarts from 'echarts';
|
// import * as echarts from 'echarts';
|
||||||
import { CountUp } from 'countup.js';
|
import { formatAxis, formatDate } from '@/common/utils/format';
|
||||||
import { formatAxis } from '@/common/utils/format';
|
|
||||||
import { indexApi } from './api';
|
import { indexApi } from './api';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
|
import { personApi } from '../personal/api';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { resourceOpLogApi } from '../ops/tag/api';
|
||||||
|
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||||
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
|
import { getAllTagInfoByCodePaths } from '../ops/component/tag';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getFileUrl, getUploadFileUrl } from '@/common/request';
|
||||||
|
import { saveUser } from '@/common/utils/storage';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { userInfo } = storeToRefs(useUserInfo());
|
const { userInfo } = storeToRefs(useUserInfo());
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
topCardItemList: [
|
accountInfo: {
|
||||||
{
|
roles: [],
|
||||||
title: 'Linux机器',
|
},
|
||||||
id: 'machineNum',
|
msgs: [],
|
||||||
color: '#F95959',
|
msgDialog: {
|
||||||
|
visible: false,
|
||||||
|
query: {
|
||||||
|
pageSize: 10,
|
||||||
|
pageNum: 1,
|
||||||
},
|
},
|
||||||
{
|
msgs: {
|
||||||
title: '数据库',
|
list: [],
|
||||||
id: 'dbNum',
|
total: null,
|
||||||
color: '#8595F4',
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: 'redis',
|
resourceOpTableHeight: 180,
|
||||||
id: 'redisNum',
|
defaultLogSize: 5,
|
||||||
color: '#1abc9c',
|
machine: {
|
||||||
},
|
num: 0,
|
||||||
{
|
opLogs: [],
|
||||||
title: 'Mongo',
|
tagInfos: {},
|
||||||
id: 'mongoNum',
|
},
|
||||||
color: '#FEBB50',
|
db: {
|
||||||
},
|
num: 0,
|
||||||
],
|
opLogs: [],
|
||||||
|
tagInfos: {},
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
num: 0,
|
||||||
|
opLogs: [],
|
||||||
|
tagInfos: {},
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
num: 0,
|
||||||
|
opLogs: [],
|
||||||
|
tagInfos: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { topCardItemList } = toRefs(state);
|
const { msgDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const roleInfo = computed(() => {
|
||||||
|
if (state.accountInfo.roles.length == 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return state.accountInfo.roles.map((val: any) => val.roleName).join('、');
|
||||||
|
});
|
||||||
|
|
||||||
// 当前时间提示语
|
// 当前时间提示语
|
||||||
const currentTime = computed(() => {
|
const currentTime = computed(() => {
|
||||||
return formatAxis(new Date());
|
return formatAxis(new Date());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面加载时
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
getAccountInfo();
|
||||||
|
|
||||||
|
getMsgs().then((res) => {
|
||||||
|
state.msgs = res.list;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMsgs = async () => {
|
||||||
|
state.msgDialog.query.pageNum = 1;
|
||||||
|
searchMsg();
|
||||||
|
state.msgDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMsg = async () => {
|
||||||
|
state.msgDialog.msgs = await getMsgs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMsgTypeDesc = (type: number) => {
|
||||||
|
if (type == 1) {
|
||||||
|
return '登录';
|
||||||
|
}
|
||||||
|
if (type == 2) {
|
||||||
|
return '通知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountInfo = async () => {
|
||||||
|
state.accountInfo = await personApi.accountInfo.request();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMsgs = async () => {
|
||||||
|
return await personApi.getMsgs.request(state.msgDialog.query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeAvatarUpload = (rawFile: any) => {
|
||||||
|
if (rawFile.size >= 512 * 1024) {
|
||||||
|
ElMessage.error('头像不能超过512KB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarSuccess = (response: any, uploadFile: any) => {
|
||||||
|
userInfo.value.photo = URL.createObjectURL(uploadFile.raw);
|
||||||
|
|
||||||
|
const newUser = { ...userInfo.value };
|
||||||
|
newUser.photo = getFileUrl(`avatar_${userInfo.value.username}`);
|
||||||
|
// 存储用户信息到浏览器缓存
|
||||||
|
saveUser(newUser);
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化数字滚动
|
// 初始化数字滚动
|
||||||
const initNumCountUp = async () => {
|
const initData = async () => {
|
||||||
const res: any = await indexApi.getIndexCount.request();
|
resourceOpLogApi.getAccountResourceOpLogs
|
||||||
nextTick(() => {
|
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
|
||||||
new CountUp('mongoNum', res.mongoNum).start();
|
.then(async (res: any) => {
|
||||||
new CountUp('machineNum', res.machineNum).start();
|
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||||
new CountUp('dbNum', res.dbNum).start();
|
state.machine.tagInfos = tagInfos;
|
||||||
new CountUp('redisNum', res.redisNum).start();
|
state.machine.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs
|
||||||
|
.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize })
|
||||||
|
.then(async (res: any) => {
|
||||||
|
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||||
|
state.db.tagInfos = tagInfos;
|
||||||
|
state.db.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs
|
||||||
|
.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize })
|
||||||
|
.then(async (res: any) => {
|
||||||
|
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||||
|
state.redis.tagInfos = tagInfos;
|
||||||
|
state.redis.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs
|
||||||
|
.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize })
|
||||||
|
.then(async (res: any) => {
|
||||||
|
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||||
|
state.mongo.tagInfos = tagInfos;
|
||||||
|
state.mongo.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.machineDashbord.request().then((res: any) => {
|
||||||
|
state.machine.num = res.machineNum;
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.dbDashbord.request().then((res: any) => {
|
||||||
|
state.db.num = res.dbNum;
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.redisDashbord.request().then((res: any) => {
|
||||||
|
state.redis.num = res.redisNum;
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.mongoDashbord.request().then((res: any) => {
|
||||||
|
state.mongo.num = res.mongoNum;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toPage = (item: any) => {
|
const toPage = (item: any, codePath = '') => {
|
||||||
switch (item.id) {
|
let path;
|
||||||
|
switch (item) {
|
||||||
case 'personal': {
|
case 'personal': {
|
||||||
router.push('/personal');
|
router.push('/personal');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mongoNum': {
|
case 'mongo': {
|
||||||
router.push('/mongo/mongo-data-operation');
|
useAutoOpenResource().setMongoCodePath(codePath);
|
||||||
|
path = '/mongo/mongo-data-operation';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'machineNum': {
|
case 'machine': {
|
||||||
router.push('/machine/machines');
|
useAutoOpenResource().setMachineCodePath(codePath);
|
||||||
|
path = '/machine/machines-op';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'dbNum': {
|
case 'db': {
|
||||||
router.push('/dbms/sql-exec');
|
useAutoOpenResource().setDbCodePath(codePath);
|
||||||
|
path = '/dbms/sql-exec';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'redisNum': {
|
case 'redis': {
|
||||||
router.push('/redis/data-operation');
|
useAutoOpenResource().setRedisCodePath(codePath);
|
||||||
|
path = '/redis/data-operation';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载时
|
router.push({ path });
|
||||||
onMounted(() => {
|
};
|
||||||
initNumCountUp();
|
|
||||||
// initHomeLaboratory();
|
|
||||||
// initHomeOvertime();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@use '@/theme/mixins/index.scss' as mixins;
|
||||||
|
|
||||||
|
.personal {
|
||||||
|
.personal-user {
|
||||||
|
height: 130px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.personal-user-left {
|
||||||
|
width: 100px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
::v-deep(.el-upload) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-user-left-upload {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
animation: logoAnimation 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-user-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
.personal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
@include mixins.text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.personal-item-label {
|
||||||
|
color: gray;
|
||||||
|
@include mixins.text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-value {
|
||||||
|
@include mixins.text-ellipsis(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-info {
|
||||||
|
.personal-info-more {
|
||||||
|
float: right;
|
||||||
|
color: gray;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-info-box {
|
||||||
|
height: 130px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.personal-info-ul {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
.personal-info-li {
|
||||||
|
font-size: 13px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
.personal-info-li-title {
|
||||||
|
display: inline-block;
|
||||||
|
@include mixins.text-ellipsis(1);
|
||||||
|
color: grey;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& a:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-info {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
::v-deep(.el-card__header) {
|
||||||
|
padding: 2px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-num {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.home-container {
|
.home-container {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@@ -166,7 +632,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-card-item-title-num {
|
.home-card-item-title-num {
|
||||||
font-size: 18px;
|
font-size: 2vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-card-item-tip-num {
|
.home-card-item-tip-num {
|
||||||
@@ -174,124 +640,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-card-first {
|
|
||||||
background: var(--bg-main-color);
|
|
||||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 100%;
|
|
||||||
border: 2px solid var(--el-color-primary-light-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-card-first-right {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.home-card-first-right-msg {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-monitor {
|
|
||||||
height: 200px;
|
|
||||||
|
|
||||||
.flex-warp-item {
|
|
||||||
width: 50%;
|
|
||||||
height: 100px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.flex-warp-item-box {
|
|
||||||
margin: auto;
|
|
||||||
height: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-warning-card {
|
|
||||||
height: 292px;
|
|
||||||
|
|
||||||
::v-deep(.el-card) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic {
|
|
||||||
height: 200px;
|
|
||||||
|
|
||||||
.home-dynamic-item {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
.home-dynamic-item-line {
|
|
||||||
i {
|
|
||||||
color: orange !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-left {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.home-dynamic-item-left-time1 {
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-left-time2 {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-line {
|
|
||||||
height: 60px;
|
|
||||||
border-right: 2px dashed #dfdfdf;
|
|
||||||
margin: 0 20px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
font-size: 12px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: -6px;
|
|
||||||
transform: rotate(46deg);
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-right {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.home-dynamic-item-right-title {
|
|
||||||
i {
|
|
||||||
margin-right: 5px;
|
|
||||||
border: 1px solid #dfdfdf;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 100%;
|
|
||||||
padding: 3px 2px 2px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-right-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Api from '@/common/Api';
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
export const indexApi = {
|
export const indexApi = {
|
||||||
getIndexCount: Api.newGet("/common/index/count"),
|
machineDashbord: Api.newGet('/machines/dashbord'),
|
||||||
}
|
dbDashbord: Api.newGet('/dbs/dashbord'),
|
||||||
|
redisDashbord: Api.newGet('/redis/dashbord'),
|
||||||
|
mongoDashbord: Api.newGet('/mongos/dashbord'),
|
||||||
|
};
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { initRouter } from '@/router/index';
|
import { initRouter } from '@/router/index';
|
||||||
import { saveToken, saveUser } from '@/common/utils/storage';
|
import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
|
||||||
import { formatAxis } from '@/common/utils/format';
|
import { formatAxis } from '@/common/utils/format';
|
||||||
import openApi from '@/common/openApi';
|
import openApi from '@/common/openApi';
|
||||||
import { RsaEncrypt } from '@/common/rsa';
|
import { RsaEncrypt } from '@/common/rsa';
|
||||||
@@ -144,6 +144,7 @@ import { personApi } from '@/views/personal/api';
|
|||||||
import { AccountUsernamePattern } from '@/common/pattern';
|
import { AccountUsernamePattern } from '@/common/pattern';
|
||||||
import { getToken } from '@/common/utils/storage';
|
import { getToken } from '@/common/utils/storage';
|
||||||
import { useThemeConfig } from '@/store/themeConfig';
|
import { useThemeConfig } from '@/store/themeConfig';
|
||||||
|
import { getFileUrl } from '@/common/request';
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
@@ -279,19 +280,20 @@ const login = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const otpVerify = async () => {
|
const otpVerify = async () => {
|
||||||
otpFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await otpFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.otpConfirm = true;
|
|
||||||
const accessToken = await openApi.otpVerify(state.otpDialog.form);
|
try {
|
||||||
await signInSuccess(accessToken);
|
state.loading.otpConfirm = true;
|
||||||
state.otpDialog.visible = false;
|
const res = await openApi.otpVerify(state.otpDialog.form);
|
||||||
} finally {
|
await signInSuccess(res.token, res.refresh_token);
|
||||||
state.loading.otpConfirm = false;
|
state.otpDialog.visible = false;
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.otpConfirm = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
@@ -327,46 +329,55 @@ const onSignIn = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateUserInfo = async () => {
|
const updateUserInfo = async () => {
|
||||||
baseInfoFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await baseInfoFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.updateUserConfirm = true;
|
|
||||||
const form = state.baseInfoDialog.form;
|
try {
|
||||||
await personApi.updateAccount.request(state.baseInfoDialog.form);
|
state.loading.updateUserConfirm = true;
|
||||||
state.baseInfoDialog.visible = false;
|
const form = state.baseInfoDialog.form;
|
||||||
useUserInfo().userInfo.username = form.username;
|
await personApi.updateAccount.request(state.baseInfoDialog.form);
|
||||||
useUserInfo().userInfo.name = form.name;
|
state.baseInfoDialog.visible = false;
|
||||||
await toIndex();
|
useUserInfo().userInfo.username = form.username;
|
||||||
} finally {
|
useUserInfo().userInfo.name = form.name;
|
||||||
state.loading.updateUserConfirm = false;
|
await toIndex();
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.updateUserConfirm = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginResDeal = (loginRes: any) => {
|
const loginResDeal = async (loginRes: any) => {
|
||||||
state.loginRes = loginRes;
|
state.loginRes = loginRes;
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const userInfos = {
|
const userInfos = {
|
||||||
name: loginRes.name,
|
name: loginRes.name,
|
||||||
username: loginRes.username,
|
username: loginRes.username,
|
||||||
// 头像
|
|
||||||
photo: letterAvatar(loginRes.username),
|
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
lastLoginTime: loginRes.lastLoginTime,
|
lastLoginTime: loginRes.lastLoginTime,
|
||||||
lastLoginIp: loginRes.lastLoginIp,
|
lastLoginIp: loginRes.lastLoginIp,
|
||||||
|
photo: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const avatarFileKey = `avatar_${loginRes.username}`;
|
||||||
|
const avatarFileDetail = await openApi.getFileDetail([avatarFileKey]);
|
||||||
|
// 说明存在头像文件
|
||||||
|
if (avatarFileDetail.length > 0) {
|
||||||
|
userInfos.photo = getFileUrl(avatarFileKey);
|
||||||
|
} else {
|
||||||
|
userInfos.photo = letterAvatar(loginRes.username);
|
||||||
|
}
|
||||||
|
|
||||||
// 存储用户信息到浏览器缓存
|
// 存储用户信息到浏览器缓存
|
||||||
saveUser(userInfos);
|
saveUser(userInfos);
|
||||||
// 1、请注意执行顺序(存储用户信息到vuex)
|
// 1、请注意执行顺序(存储用户信息到vuex)
|
||||||
useUserInfo().setUserInfo(userInfos);
|
useUserInfo().setUserInfo(userInfos);
|
||||||
|
|
||||||
const token = loginRes.token;
|
const token = loginRes.token;
|
||||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
// 如果不需要otp校验,则该token即为accessToken,否则为otp校验token
|
||||||
if (loginRes.otp == -1) {
|
if (loginRes.otp == -1) {
|
||||||
signInSuccess(token);
|
signInSuccess(token, loginRes.refresh_token);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,12 +390,16 @@ const loginResDeal = (loginRes: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 登录成功后的跳转
|
// 登录成功后的跳转
|
||||||
const signInSuccess = async (accessToken: string = '') => {
|
const signInSuccess = async (accessToken: string = '', refreshToken = '') => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
accessToken = getToken();
|
accessToken = getToken();
|
||||||
}
|
}
|
||||||
|
if (!refreshToken) {
|
||||||
|
refreshToken = getRefreshToken();
|
||||||
|
}
|
||||||
// 存储 token 到浏览器缓存
|
// 存储 token 到浏览器缓存
|
||||||
saveToken(accessToken);
|
saveToken(accessToken);
|
||||||
|
saveRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 初始化路由
|
// 初始化路由
|
||||||
await initRouter();
|
await initRouter();
|
||||||
@@ -415,26 +430,27 @@ const toIndex = async () => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changePwd = () => {
|
const changePwd = async () => {
|
||||||
changePwdFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await changePwdFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.changePwd = true;
|
|
||||||
const form = state.changePwdDialog.form;
|
try {
|
||||||
const changePwdReq: any = { ...form };
|
state.loading.changePwd = true;
|
||||||
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
|
const form = state.changePwdDialog.form;
|
||||||
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
|
const changePwdReq: any = { ...form };
|
||||||
await openApi.changePwd(changePwdReq);
|
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
|
||||||
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
|
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
|
||||||
state.loginForm.password = state.changePwdDialog.form.newPassword;
|
await openApi.changePwd(changePwdReq);
|
||||||
state.changePwdDialog.visible = false;
|
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
|
||||||
getCaptcha();
|
state.loginForm.password = state.changePwdDialog.form.newPassword;
|
||||||
} finally {
|
state.changePwdDialog.visible = false;
|
||||||
state.loading.changePwd = false;
|
getCaptcha();
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.changePwd = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelChangePwd = () => {
|
const cancelChangePwd = () => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user