mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	Compare commits
	
		
			60 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | 
@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
 | 
			
		||||
    yarn build
 | 
			
		||||
 | 
			
		||||
# 构建后端资源
 | 
			
		||||
FROM golang:1.21.5 as be-builder
 | 
			
		||||
FROM golang:1.22 as be-builder
 | 
			
		||||
 | 
			
		||||
ENV GOPROXY https://goproxy.cn
 | 
			
		||||
WORKDIR /mayfly
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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"/>
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://github.com/golang/go" target="_blank">
 | 
			
		||||
    <img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
 | 
			
		||||
    <img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://cn.vuejs.org" target="_blank">
 | 
			
		||||
    <img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
 | 
			
		||||
### 介绍
 | 
			
		||||
 | 
			
		||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
 | 
			
		||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
 | 
			
		||||
 | 
			
		||||
### 开发语言与主要框架
 | 
			
		||||
 | 
			
		||||
@@ -45,56 +45,61 @@ http://go.mayfly.run
 | 
			
		||||
 | 
			
		||||
### 系统核心功能截图
 | 
			
		||||
 | 
			
		||||
##### 记录操作记录
 | 
			
		||||
#### 首页
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

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

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

 | 
			
		||||

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

 | 
			
		||||

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

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ services:
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
  server:
 | 
			
		||||
    image: mayfly-go:v1.3.1
 | 
			
		||||
    image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.3
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: Dockerfile
 | 
			
		||||
 
 | 
			
		||||
@@ -10,31 +10,31 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@element-plus/icons-vue": "^2.3.1",
 | 
			
		||||
    "@vueuse/core": "^10.7.2",
 | 
			
		||||
    "asciinema-player": "^3.6.3",
 | 
			
		||||
    "@vueuse/core": "^10.9.0",
 | 
			
		||||
    "asciinema-player": "^3.7.1",
 | 
			
		||||
    "axios": "^1.6.2",
 | 
			
		||||
    "clipboard": "^2.0.11",
 | 
			
		||||
    "countup.js": "^2.7.0",
 | 
			
		||||
    "cropperjs": "^1.5.11",
 | 
			
		||||
    "echarts": "^5.4.3",
 | 
			
		||||
    "element-plus": "^2.5.3",
 | 
			
		||||
    "js-base64": "^3.7.5",
 | 
			
		||||
    "cropperjs": "^1.6.1",
 | 
			
		||||
    "echarts": "^5.5.0",
 | 
			
		||||
    "element-plus": "^2.7.2",
 | 
			
		||||
    "js-base64": "^3.7.7",
 | 
			
		||||
    "jsencrypt": "^3.3.2",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "mitt": "^3.0.1",
 | 
			
		||||
    "monaco-editor": "^0.45.0",
 | 
			
		||||
    "monaco-editor": "^0.48.0",
 | 
			
		||||
    "monaco-sql-languages": "^0.11.0",
 | 
			
		||||
    "monaco-themes": "^0.4.4",
 | 
			
		||||
    "nprogress": "^0.2.0",
 | 
			
		||||
    "pinia": "^2.1.7",
 | 
			
		||||
    "qrcode.vue": "^3.4.1",
 | 
			
		||||
    "screenfull": "^6.0.2",
 | 
			
		||||
    "sortablejs": "^1.15.0",
 | 
			
		||||
    "sortablejs": "^1.15.2",
 | 
			
		||||
    "splitpanes": "^3.1.5",
 | 
			
		||||
    "sql-formatter": "^15.0.2",
 | 
			
		||||
    "trzsz": "^1.1.5",
 | 
			
		||||
    "uuid": "^9.0.1",
 | 
			
		||||
    "vue": "^3.4.15",
 | 
			
		||||
    "vue-router": "^4.2.5",
 | 
			
		||||
    "vue": "^3.4.27",
 | 
			
		||||
    "vue-router": "^4.3.2",
 | 
			
		||||
    "xterm": "^5.3.0",
 | 
			
		||||
    "xterm-addon-fit": "^0.8.0",
 | 
			
		||||
    "xterm-addon-search": "^0.13.0",
 | 
			
		||||
@@ -44,20 +44,20 @@
 | 
			
		||||
    "@types/lodash": "^4.14.178",
 | 
			
		||||
    "@types/node": "^18.14.0",
 | 
			
		||||
    "@types/nprogress": "^0.2.0",
 | 
			
		||||
    "@types/sortablejs": "^1.15.3",
 | 
			
		||||
    "@types/sortablejs": "^1.15.8",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^6.7.4",
 | 
			
		||||
    "@typescript-eslint/parser": "^6.7.4",
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.0.3",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.4.14",
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.0.4",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.4.27",
 | 
			
		||||
    "code-inspector-plugin": "^0.4.5",
 | 
			
		||||
    "dotenv": "^16.3.1",
 | 
			
		||||
    "eslint": "^8.35.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.19.2",
 | 
			
		||||
    "prettier": "^3.1.0",
 | 
			
		||||
    "sass": "^1.69.0",
 | 
			
		||||
    "typescript": "^5.3.2",
 | 
			
		||||
    "vite": "^5.0.12",
 | 
			
		||||
    "vue-eslint-parser": "^9.4.0"
 | 
			
		||||
    "eslint-plugin-vue": "^9.25.0",
 | 
			
		||||
    "prettier": "^3.2.5",
 | 
			
		||||
    "sass": "^1.76.0",
 | 
			
		||||
    "typescript": "^5.4.5",
 | 
			
		||||
    "vite": "^5.2.11",
 | 
			
		||||
    "vue-eslint-parser": "^9.4.2"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "> 1%",
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
            :zIndex="10000000"
 | 
			
		||||
            :width="210"
 | 
			
		||||
            v-if="themeConfig.isWatermark"
 | 
			
		||||
            :font="{ color: 'rgba(180, 180, 180, 0.5)' }"
 | 
			
		||||
            :font="{ color: 'rgba(180, 180, 180, 0.3)' }"
 | 
			
		||||
            :content="themeConfig.watermarkText"
 | 
			
		||||
            class="h100"
 | 
			
		||||
        >
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -88,6 +88,20 @@
 | 
			
		||||
      "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
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ export class EnumValue {
 | 
			
		||||
     */
 | 
			
		||||
    tag: EnumValueTag;
 | 
			
		||||
 | 
			
		||||
    extra: any;
 | 
			
		||||
 | 
			
		||||
    constructor(value: any, label: string) {
 | 
			
		||||
        this.value = value;
 | 
			
		||||
        this.label = label;
 | 
			
		||||
@@ -53,6 +55,11 @@ export class EnumValue {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setExtra(extra: any): EnumValue {
 | 
			
		||||
        this.extra = extra;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static of(value: any, label: string): EnumValue {
 | 
			
		||||
        return new EnumValue(value, label);
 | 
			
		||||
    }
 | 
			
		||||
@@ -60,11 +67,12 @@ export class EnumValue {
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据枚举值获取指定枚举值对象
 | 
			
		||||
     *
 | 
			
		||||
     * @param enumValues 所有枚举值
 | 
			
		||||
     * @param enums 枚举对象
 | 
			
		||||
     * @param value 需要匹配的枚举值
 | 
			
		||||
     * @returns 枚举值对象
 | 
			
		||||
     */
 | 
			
		||||
    static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null {
 | 
			
		||||
    static getEnumByValue(enums: any, value: any): EnumValue | null {
 | 
			
		||||
        const enumValues = Object.values(enums) as any;
 | 
			
		||||
        for (let enumValue of enumValues) {
 | 
			
		||||
            if (enumValue.value == value) {
 | 
			
		||||
                return enumValue;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,24 @@
 | 
			
		||||
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 = {
 | 
			
		||||
    Machine: EnumValue.of(1, '机器'),
 | 
			
		||||
    Db: EnumValue.of(2, '数据库'),
 | 
			
		||||
    Redis: EnumValue.of(3, 'redis'),
 | 
			
		||||
    Mongo: EnumValue.of(4, 'mongo'),
 | 
			
		||||
    AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
 | 
			
		||||
    Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
 | 
			
		||||
 | 
			
		||||
    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`,
 | 
			
		||||
 | 
			
		||||
    // 系统版本
 | 
			
		||||
    version: 'v1.7.2',
 | 
			
		||||
    version: 'v1.8.3',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,9 @@
 | 
			
		||||
export const AccountUsernamePattern = {
 | 
			
		||||
    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位大小写字母、数字、_-.:',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,24 +7,22 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
 | 
			
		||||
        for (let column of columns) {
 | 
			
		||||
            let val: any = data[column];
 | 
			
		||||
            if (val == null || val == undefined) {
 | 
			
		||||
                dataValueArr.push('');
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
                val = '';
 | 
			
		||||
            } else if (val && typeof val == 'string') {
 | 
			
		||||
                // 替换换行符
 | 
			
		||||
                val = val.replace(/[\r\n]/g, '\\n');
 | 
			
		||||
 | 
			
		||||
            if (typeof val == 'string' && val) {
 | 
			
		||||
                // csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
 | 
			
		||||
                if (val.indexOf(',') != -1) {
 | 
			
		||||
                    // 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
 | 
			
		||||
                    if (val.indexOf('"') != -1) {
 | 
			
		||||
                        val = val.replace(/\"/g, '""');
 | 
			
		||||
                        val = val.replace(/"/g, '""');
 | 
			
		||||
                    }
 | 
			
		||||
                    // 再将逗号转义
 | 
			
		||||
                    val = `"${val}"`;
 | 
			
		||||
                }
 | 
			
		||||
                dataValueArr.push(val + '\t');
 | 
			
		||||
            } else {
 | 
			
		||||
                dataValueArr.push(val + '\t');
 | 
			
		||||
            }
 | 
			
		||||
            dataValueArr.push(String(val));
 | 
			
		||||
        }
 | 
			
		||||
        cvsData.push(dataValueArr);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -46,60 +46,6 @@ export function convertToBytes(sizeStr: string) {
 | 
			
		||||
    return bytes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 格式化json字符串
 | 
			
		||||
 * @param txt  json字符串
 | 
			
		||||
 * @param compress 是否压缩
 | 
			
		||||
 * @returns 格式化后的字符串
 | 
			
		||||
 */
 | 
			
		||||
export function formatJsonString(txt: string, compress: boolean) {
 | 
			
		||||
    var indentChar = '    ';
 | 
			
		||||
    if (/^\s*$/.test(txt)) {
 | 
			
		||||
        console.log('数据为空,无法格式化! ');
 | 
			
		||||
        return txt;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
        var data = JSON.parse(txt);
 | 
			
		||||
    } 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) {
 | 
			
		||||
        nodeCount++; /*节点计数*/
 | 
			
		||||
        for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
 | 
			
		||||
        tab = compress ? '' : tab; /*压缩模式忽略缩进*/
 | 
			
		||||
        maxDepth = ++indent; /*缩进递增并记录*/
 | 
			
		||||
        if (value && value.constructor == Array) {
 | 
			
		||||
            /*处理数组*/
 | 
			
		||||
            draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
 | 
			
		||||
            for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
 | 
			
		||||
            draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
 | 
			
		||||
        } else if (value && typeof value == 'object') {
 | 
			
		||||
            /*处理对象*/
 | 
			
		||||
            draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
 | 
			
		||||
            var len = 0,
 | 
			
		||||
                i = 0;
 | 
			
		||||
            for (var key in value) len++;
 | 
			
		||||
            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('');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 年(Y) 可用1-4个占位符
 | 
			
		||||
 * 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
 | 
			
		||||
@@ -204,6 +150,45 @@ export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
 | 
			
		||||
 *
 | 
			
		||||
 * @param time 时间数
 | 
			
		||||
 * @param unit time对应的单位
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export function formatTime(time: number, unit: string = 's') {
 | 
			
		||||
    const units = {
 | 
			
		||||
        y: 31536000,
 | 
			
		||||
        M: 2592000,
 | 
			
		||||
        d: 86400,
 | 
			
		||||
        h: 3600,
 | 
			
		||||
        m: 60,
 | 
			
		||||
        s: 1,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!units[unit]) {
 | 
			
		||||
        return 'Invalid unit';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let seconds = time * units[unit];
 | 
			
		||||
    let result = '';
 | 
			
		||||
 | 
			
		||||
    const timeUnits = Object.entries(units).map(([unit, duration]) => {
 | 
			
		||||
        const value = Math.floor(seconds / duration);
 | 
			
		||||
        seconds %= duration;
 | 
			
		||||
        return { value, unit };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    timeUnits.forEach(({ value, unit }) => {
 | 
			
		||||
        if (value > 0) {
 | 
			
		||||
            result += `${value}${unit} `;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * formatAxis(new Date())   // 上午好
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								mayfly_go_web/src/common/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mayfly_go_web/src/common/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 根据对象访问路径,获取对应的值
 | 
			
		||||
 *
 | 
			
		||||
 * @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 || 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]);
 | 
			
		||||
            result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
 | 
			
		||||
        } else {
 | 
			
		||||
            result = result[key];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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>
 | 
			
		||||
@@ -40,7 +40,7 @@ onMounted(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const convert = (value: any) => {
 | 
			
		||||
    const enumValue = EnumValue.getEnumByValue(Object.values(props.enums as any) as any, value) as any;
 | 
			
		||||
    const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
 | 
			
		||||
    if (!enumValue) {
 | 
			
		||||
        state.enumLabel = '-';
 | 
			
		||||
        state.type = 'danger';
 | 
			
		||||
 
 | 
			
		||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        editorRef.value?.format();
 | 
			
		||||
        editorRef.value?.focus();
 | 
			
		||||
        editorRef.value?.format();
 | 
			
		||||
    }, 300);
 | 
			
		||||
 | 
			
		||||
    state.dialogVisible = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@
 | 
			
		||||
                                        trigger="click"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <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>
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <el-button icon="Operation" circle :size="props.size"></el-button>
 | 
			
		||||
@@ -115,18 +115,18 @@
 | 
			
		||||
                        >
 | 
			
		||||
                            <!-- 插槽:预留功能 -->
 | 
			
		||||
                            <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>
 | 
			
		||||
 | 
			
		||||
                            <!-- 枚举类型使用tab展示 -->
 | 
			
		||||
                            <template #default="scope" v-else-if="item.type == 'tag'">
 | 
			
		||||
                                <enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
 | 
			
		||||
                                <enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
 | 
			
		||||
                            </template>
 | 
			
		||||
 | 
			
		||||
                            <template #default="scope" v-else>
 | 
			
		||||
                                <!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
 | 
			
		||||
                                <el-popover
 | 
			
		||||
                                    v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
 | 
			
		||||
                                    v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
 | 
			
		||||
                                    effect="light"
 | 
			
		||||
                                    trigger="click"
 | 
			
		||||
                                    placement="top"
 | 
			
		||||
@@ -137,7 +137,7 @@
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                    <template #reference>
 | 
			
		||||
                                        <el-link
 | 
			
		||||
                                            @click="formatText(scope.row[item.prop])"
 | 
			
		||||
                                            @click="formatText(item.getValueByData(scope.row))"
 | 
			
		||||
                                            :underline="false"
 | 
			
		||||
                                            type="success"
 | 
			
		||||
                                            icon="MagicStick"
 | 
			
		||||
@@ -159,7 +159,7 @@
 | 
			
		||||
                    @current-change="handlePageNumChange"
 | 
			
		||||
                    @size-change="handlePageSizeChange"
 | 
			
		||||
                    style="text-align: right"
 | 
			
		||||
                    layout="prev, pager, next, total, sizes, jumper"
 | 
			
		||||
                    layout="prev, pager, next, total, sizes"
 | 
			
		||||
                    :total="total"
 | 
			
		||||
                    v-model:current-page="queryForm.pageNum"
 | 
			
		||||
                    v-model:page-size="queryForm.pageSize"
 | 
			
		||||
@@ -185,11 +185,11 @@ import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { usePageTable } from '@/hooks/usePageTable';
 | 
			
		||||
import { ElTable } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChange']);
 | 
			
		||||
const emit = defineEmits(['update:selectionData', 'pageChange']);
 | 
			
		||||
 | 
			
		||||
export interface PageTableProps {
 | 
			
		||||
    size?: string;
 | 
			
		||||
    pageApi: Api; // 请求表格数据的 api
 | 
			
		||||
    pageApi?: Api; // 请求表格数据的 api
 | 
			
		||||
    columns: TableColumn[]; // 列配置项  ==> 必传
 | 
			
		||||
    showSelection?: boolean;
 | 
			
		||||
    selectable?: (row: any) => boolean; // 是否可选
 | 
			
		||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
 | 
			
		||||
    nowSearchItem.value = searchItem;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
 | 
			
		||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
 | 
			
		||||
    props.pageable,
 | 
			
		||||
    props.pageApi,
 | 
			
		||||
    queryForm,
 | 
			
		||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
 | 
			
		||||
    calcuTableHeight();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.data,
 | 
			
		||||
    (newValue: any) => {
 | 
			
		||||
        tableData = newValue;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    calcuTableHeight();
 | 
			
		||||
    useEventListener(window, 'resize', calcuTableHeight);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
import { getValueByPath } from '@/common/utils/object';
 | 
			
		||||
import { getTextWidth } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
export class TableColumn {
 | 
			
		||||
@@ -29,10 +30,15 @@ export class TableColumn {
 | 
			
		||||
    minWidth: number | string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否插槽,是的话插槽名则为prop属性名
 | 
			
		||||
     * 是否为插槽,若slotName为空则插槽名为prop属性名
 | 
			
		||||
     */
 | 
			
		||||
    slot: boolean = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 插槽名,
 | 
			
		||||
     */
 | 
			
		||||
    slotName: string = '';
 | 
			
		||||
 | 
			
		||||
    showOverflowTooltip: boolean = true;
 | 
			
		||||
 | 
			
		||||
    sortable: boolean = false;
 | 
			
		||||
@@ -87,7 +93,7 @@ export class TableColumn {
 | 
			
		||||
        if (this.formatFunc) {
 | 
			
		||||
            return this.formatFunc(rowData, this.prop);
 | 
			
		||||
        }
 | 
			
		||||
        return rowData[this.prop];
 | 
			
		||||
        return getValueByPath(rowData, this.prop);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static new(prop: string, label: string): TableColumn {
 | 
			
		||||
@@ -144,8 +150,9 @@ export class TableColumn {
 | 
			
		||||
     * 标识该列为插槽
 | 
			
		||||
     * @returns this
 | 
			
		||||
     */
 | 
			
		||||
    isSlot(): TableColumn {
 | 
			
		||||
    isSlot(slotName: string = ''): TableColumn {
 | 
			
		||||
        this.slot = true;
 | 
			
		||||
        this.slotName = slotName;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -165,7 +172,7 @@ export class TableColumn {
 | 
			
		||||
     */
 | 
			
		||||
    isTime(): TableColumn {
 | 
			
		||||
        this.setFormatFunc((data: any, prop: string) => {
 | 
			
		||||
            return dateFormat(data[prop]);
 | 
			
		||||
            return dateFormat(getValueByPath(data, prop));
 | 
			
		||||
        });
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
@@ -176,7 +183,7 @@ export class TableColumn {
 | 
			
		||||
     */
 | 
			
		||||
    isEnum(enums: any): TableColumn {
 | 
			
		||||
        this.setFormatFunc((data: any, prop: string) => {
 | 
			
		||||
            return EnumValue.getLabelByValue(enums, data[prop]);
 | 
			
		||||
            return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
 | 
			
		||||
        });
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
@@ -218,7 +225,7 @@ export class TableColumn {
 | 
			
		||||
        // 获取该列中最长的数据(内容)
 | 
			
		||||
        for (let i = 0; i < tableData.length; i++) {
 | 
			
		||||
            let nowData = tableData[i];
 | 
			
		||||
            let nowValue = nowData[prop];
 | 
			
		||||
            let nowValue = getValueByPath(nowData, prop);
 | 
			
		||||
            if (!nowValue) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										480
									
								
								mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										480
									
								
								mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,480 @@
 | 
			
		||||
<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" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <el-dialog 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: {},
 | 
			
		||||
    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>
 | 
			
		||||
    <div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
 | 
			
		||||
    <div id="terminal-body" :style="{ height }">
 | 
			
		||||
        <div ref="terminalRef" class="terminal" />
 | 
			
		||||
 | 
			
		||||
        <TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import 'xterm/css/xterm.css';
 | 
			
		||||
import { Terminal } from 'xterm';
 | 
			
		||||
import { Terminal, ITheme } from 'xterm';
 | 
			
		||||
import { FitAddon } from 'xterm-addon-fit';
 | 
			
		||||
import { SearchAddon } from 'xterm-addon-search';
 | 
			
		||||
import { WebLinksAddon } from 'xterm-addon-web-links';
 | 
			
		||||
@@ -20,8 +20,15 @@ import TerminalSearch from './TerminalSearch.vue';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
import { TerminalStatus } from './common';
 | 
			
		||||
import { useEventListener } from '@vueuse/core';
 | 
			
		||||
import themes from './themes';
 | 
			
		||||
import { TrzszFilter } from 'trzsz';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    // mounted时,是否执行init方法
 | 
			
		||||
    mountInit: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化执行命令
 | 
			
		||||
     */
 | 
			
		||||
@@ -64,9 +71,9 @@ const state = reactive({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
    if (props.mountInit) {
 | 
			
		||||
        init();
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
@@ -76,6 +83,14 @@ watch(
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// 监听 themeConfig terminalTheme配置的变化
 | 
			
		||||
watch(
 | 
			
		||||
    () => themeConfig.value.terminalTheme,
 | 
			
		||||
    () => {
 | 
			
		||||
        term.options.theme = getTerminalTheme();
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
    close();
 | 
			
		||||
});
 | 
			
		||||
@@ -85,6 +100,12 @@ function init() {
 | 
			
		||||
        console.log('重新连接...');
 | 
			
		||||
        close();
 | 
			
		||||
    }
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
        initTerm();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initTerm() {
 | 
			
		||||
    term = new Terminal({
 | 
			
		||||
        fontSize: themeConfig.value.terminalFontSize || 15,
 | 
			
		||||
        fontWeight: themeConfig.value.terminalFontWeight || 'normal',
 | 
			
		||||
@@ -92,13 +113,10 @@ function init() {
 | 
			
		||||
        cursorBlink: true,
 | 
			
		||||
        disableStdin: false,
 | 
			
		||||
        allowProposedApi: true,
 | 
			
		||||
        theme: {
 | 
			
		||||
            foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
 | 
			
		||||
            background: themeConfig.value.terminalBackground || '#002833', //背景色
 | 
			
		||||
            cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
 | 
			
		||||
            // cursorAccent: "red",  // 光标停止颜色
 | 
			
		||||
        } as any,
 | 
			
		||||
        fastScrollModifier: 'ctrl',
 | 
			
		||||
        theme: getTerminalTheme(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    term.open(terminalRef.value);
 | 
			
		||||
 | 
			
		||||
    // 注册自适应组件
 | 
			
		||||
@@ -106,31 +124,12 @@ function init() {
 | 
			
		||||
    state.addon.fit = fitAddon;
 | 
			
		||||
    term.loadAddon(fitAddon);
 | 
			
		||||
    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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 连接成功
 | 
			
		||||
 */
 | 
			
		||||
const onConnected = () => {
 | 
			
		||||
    // 注册心跳
 | 
			
		||||
    pingInterval = setInterval(sendPing, 15000);
 | 
			
		||||
 | 
			
		||||
    // 注册 terminal 事件
 | 
			
		||||
    term.onResize((event) => sendResize(event.cols, event.rows));
 | 
			
		||||
    term.onData((event) => sendCmd(event));
 | 
			
		||||
    // 注册其他插件
 | 
			
		||||
    loadAddon();
 | 
			
		||||
 | 
			
		||||
    // 注册自定义快捷键
 | 
			
		||||
    term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
 | 
			
		||||
@@ -142,50 +141,25 @@ const onConnected = () => {
 | 
			
		||||
 | 
			
		||||
        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() {
 | 
			
		||||
    if (props.socketUrl) {
 | 
			
		||||
        let socketUrl = `${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`;
 | 
			
		||||
        socket = new WebSocket(socketUrl);
 | 
			
		||||
    if (!props.socketUrl) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
 | 
			
		||||
    // 监听socket连接
 | 
			
		||||
    socket.onopen = () => {
 | 
			
		||||
        onConnected();
 | 
			
		||||
        // 注册心跳
 | 
			
		||||
        pingInterval = setInterval(sendPing, 15000);
 | 
			
		||||
        state.status = TerminalStatus.Connected;
 | 
			
		||||
 | 
			
		||||
        focus();
 | 
			
		||||
 | 
			
		||||
        // 如果有初始要执行的命令,则发送执行命令
 | 
			
		||||
        if (props.cmd) {
 | 
			
		||||
            sendCmd(props.cmd + ' \r');
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 监听socket错误信息
 | 
			
		||||
@@ -197,20 +171,96 @@ function initSocket() {
 | 
			
		||||
 | 
			
		||||
    socket.onclose = (e: CloseEvent) => {
 | 
			
		||||
        console.log('terminal socket close...', e.reason);
 | 
			
		||||
        // 清除 ping
 | 
			
		||||
        pingInterval && clearInterval(pingInterval);
 | 
			
		||||
        state.status = TerminalStatus.Disconnected;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 监听socket消息
 | 
			
		||||
    socket.onmessage = getMessage;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getMessage(msg: any) {
 | 
			
		||||
    // msg.data是真正后端返回的数据
 | 
			
		||||
    term.write(msg.data);
 | 
			
		||||
function loadAddon() {
 | 
			
		||||
    // 注册搜索组件
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
    window.addEventListener('resize', () => state.addon.fit.fit());
 | 
			
		||||
    // 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 {
 | 
			
		||||
    Resize = 1,
 | 
			
		||||
    Data = 2,
 | 
			
		||||
@@ -218,29 +268,19 @@ enum MsgType {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) => {
 | 
			
		||||
    send({
 | 
			
		||||
        type: MsgType.Resize,
 | 
			
		||||
        Cols: cols,
 | 
			
		||||
        Rows: rows,
 | 
			
		||||
    });
 | 
			
		||||
    send(`${MsgType.Resize}|${rows}|${cols}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sendPing = () => {
 | 
			
		||||
    send({
 | 
			
		||||
        type: MsgType.Ping,
 | 
			
		||||
        msg: 'ping',
 | 
			
		||||
    });
 | 
			
		||||
    send(`${MsgType.Ping}|ping`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function sendCmd(key: any) {
 | 
			
		||||
    send({
 | 
			
		||||
        type: MsgType.Data,
 | 
			
		||||
        msg: key,
 | 
			
		||||
    });
 | 
			
		||||
    send(`${MsgType.Data}|${key}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeSocket() {
 | 
			
		||||
@@ -265,20 +305,19 @@ const getStatus = (): TerminalStatus => {
 | 
			
		||||
    return state.status;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
 | 
			
		||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
#terminal-body {
 | 
			
		||||
    background: #212529;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    .terminal {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        .xterm .xterm-viewport {
 | 
			
		||||
            overflow-y: hidden;
 | 
			
		||||
        }
 | 
			
		||||
        // .xterm .xterm-viewport {
 | 
			
		||||
        //     overflow-y: hidden;
 | 
			
		||||
        // }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
 | 
			
		||||
            <el-dialog
 | 
			
		||||
                title="终端"
 | 
			
		||||
                title="SSH终端"
 | 
			
		||||
                v-model="openTerminal.visible"
 | 
			
		||||
                top="32px"
 | 
			
		||||
                class="terminal-dialog"
 | 
			
		||||
@@ -58,7 +58,7 @@
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </template>
 | 
			
		||||
                <div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
 | 
			
		||||
                <div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
 | 
			
		||||
                    <TerminalBody
 | 
			
		||||
                        @status-change="terminalStatusChange(openTerminal.terminalId, $event)"
 | 
			
		||||
                        :ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
 | 
			
		||||
@@ -92,7 +92,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive } from 'vue';
 | 
			
		||||
import { reactive, toRefs } from 'vue';
 | 
			
		||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { TerminalStatus } from './common';
 | 
			
		||||
@@ -259,6 +259,10 @@ defineExpose({
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-dialog {
 | 
			
		||||
        padding: 1px 1px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 取消body最大高度,否则全屏有问题
 | 
			
		||||
    .el-dialog__body {
 | 
			
		||||
        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>
 | 
			
		||||
							
								
								
									
										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 { isReactive, reactive, toRefs, toValue } from 'vue';
 | 
			
		||||
import { reactive, toRefs, toValue } from 'vue';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @description table 页面操作方法封装
 | 
			
		||||
@@ -41,15 +41,10 @@ export const usePageTable = (
 | 
			
		||||
            let sp = toValue(state.searchParams);
 | 
			
		||||
            if (beforeQueryFn) {
 | 
			
		||||
                sp = beforeQueryFn(sp);
 | 
			
		||||
 | 
			
		||||
                if (isReactive(state.searchParams)) {
 | 
			
		||||
                    state.searchParams.value = sp;
 | 
			
		||||
                } else {
 | 
			
		||||
                    state.searchParams = sp;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let res = await api.request(sp);
 | 
			
		||||
            res.list = res.list || [];
 | 
			
		||||
            dataCallBack && (res = await dataCallBack(res));
 | 
			
		||||
 | 
			
		||||
            if (pageable) {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ const useCustomFetch = createFetch({
 | 
			
		||||
    combination: 'chain',
 | 
			
		||||
    options: {
 | 
			
		||||
        immediate: false,
 | 
			
		||||
        timeout: 60000,
 | 
			
		||||
        timeout: 600000,
 | 
			
		||||
        // beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
 | 
			
		||||
        async beforeFetch({ options }) {
 | 
			
		||||
            const token = getToken();
 | 
			
		||||
@@ -48,9 +48,6 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let paramsValue = unref(params);
 | 
			
		||||
            if (api.beforeHandler) {
 | 
			
		||||
                paramsValue = api.beforeHandler(paramsValue);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let apiUrl = url;
 | 
			
		||||
            // 简单判断该url是否是restful风格
 | 
			
		||||
@@ -58,6 +55,10 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
 | 
			
		||||
                apiUrl = templateResolve(apiUrl, paramsValue);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (api.beforeHandler) {
 | 
			
		||||
                paramsValue = api.beforeHandler(paramsValue);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (paramsValue) {
 | 
			
		||||
                const method = options.method?.toLowerCase();
 | 
			
		||||
                // post和put使用json格式传参
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
 | 
			
		||||
        <SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'"
 | 
			
		||||
            @click="onThemeConfigChange" />
 | 
			
		||||
        <SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
 | 
			
		||||
        <el-breadcrumb class="layout-navbars-breadcrumb-hide">
 | 
			
		||||
            <transition-group name="breadcrumb" mode="out-in">
 | 
			
		||||
                <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">
 | 
			
		||||
                        <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
 | 
			
		||||
                            v-if="themeConfig.isBreadcrumbIcon" />
 | 
			
		||||
                        <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
 | 
			
		||||
                        {{ v.meta.title }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <a v-else @click.prevent="onBreadcrumbClick(v)">
 | 
			
		||||
                        <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
 | 
			
		||||
                            v-if="themeConfig.isBreadcrumbIcon" />
 | 
			
		||||
                        <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
 | 
			
		||||
                        {{ v.meta.title }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </el-breadcrumb-item>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,26 +5,39 @@
 | 
			
		||||
                <!-- ssh终端主题 -->
 | 
			
		||||
                <el-divider content-position="left">终端主题</el-divider>
 | 
			
		||||
                <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                        <el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
 | 
			
		||||
                        </el-color-picker>
 | 
			
		||||
                        <el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalTheme" size="small" style="width: 140px">
 | 
			
		||||
                            <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 class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                        <el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
 | 
			
		||||
                        </el-color-picker>
 | 
			
		||||
                <template v-if="themeConfig.terminalTheme == 'custom'">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex mt10">
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                            <el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
 | 
			
		||||
                            </el-color-picker>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                        <el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                            <el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
 | 
			
		||||
                            </el-color-picker>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="layout-breadcrumb-seting-bar-flex mt15">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                            <el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
 | 
			
		||||
                            </el-color-picker>
 | 
			
		||||
                        </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-value">
 | 
			
		||||
                        <el-input-number
 | 
			
		||||
@@ -39,7 +52,7 @@
 | 
			
		||||
                        </el-input-number>
 | 
			
		||||
                    </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-value">
 | 
			
		||||
                        <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 { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
 | 
			
		||||
import mittBus from '@/common/utils/mitt';
 | 
			
		||||
import themes from '@/components/terminal/themes';
 | 
			
		||||
 | 
			
		||||
const copyConfigBtnRef = ref();
 | 
			
		||||
const { themeConfig } = storeToRefs(useThemeConfig());
 | 
			
		||||
@@ -615,6 +629,9 @@ const setLocalThemeConfigStyle = () => {
 | 
			
		||||
};
 | 
			
		||||
// 一键复制配置
 | 
			
		||||
const onCopyConfigClick = (target: any) => {
 | 
			
		||||
    if (!target) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    let copyThemeConfig = getLocal('themeConfig');
 | 
			
		||||
    copyThemeConfig.isDrawer = false;
 | 
			
		||||
    const clipboard = new ClipboardJS(target, {
 | 
			
		||||
@@ -690,6 +707,25 @@ defineExpose({ openDrawer });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<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 {
 | 
			
		||||
    height: calc(100vh - 50px);
 | 
			
		||||
    padding: 0 15px;
 | 
			
		||||
 
 | 
			
		||||
@@ -174,12 +174,7 @@ watch(preDark, (newValue) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const switchDark = () => {
 | 
			
		||||
    themeConfig.value.isDark = isDark.value;
 | 
			
		||||
    if (isDark.value) {
 | 
			
		||||
        themeConfig.value.editorTheme = 'vs-dark';
 | 
			
		||||
    } else {
 | 
			
		||||
        themeConfig.value.editorTheme = 'vs';
 | 
			
		||||
    }
 | 
			
		||||
    themeConfigStore.switchDark(isDark.value);
 | 
			
		||||
    saveThemeConfig(themeConfig.value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
 | 
			
		||||
    state.tagsRefsIndex = k;
 | 
			
		||||
    try {
 | 
			
		||||
        router.push(v);
 | 
			
		||||
    } catch (e) {}
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        // skip
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
// 更新滚动条显示
 | 
			
		||||
const updateScrollbar = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -89,13 +89,18 @@ type RouterConvCallbackFunc = (router: any) => void;
 | 
			
		||||
 * @param meta.link ==> 外链地址
 | 
			
		||||
 * */
 | 
			
		||||
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
 | 
			
		||||
    if (!routes) return [];
 | 
			
		||||
    return routes.map((item: any) => {
 | 
			
		||||
    if (!routes) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const routeItems = [];
 | 
			
		||||
    for (let item of routes) {
 | 
			
		||||
        if (!item.meta) {
 | 
			
		||||
            return item;
 | 
			
		||||
        }
 | 
			
		||||
        // 将json字符串的meta转为对象
 | 
			
		||||
        item.meta = JSON.parse(item.meta);
 | 
			
		||||
 | 
			
		||||
        // 将meta.comoponet 解析为route.component
 | 
			
		||||
        if (item.meta.component) {
 | 
			
		||||
            item.component = dynamicImport(dynamicViewsModules, item.meta.component);
 | 
			
		||||
@@ -126,8 +131,10 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
 | 
			
		||||
        // 存在回调,则执行回调
 | 
			
		||||
        callbackFunc && callbackFunc(item);
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.error(`未匹配到[${component}]组件名对应的组件文件`);
 | 
			
		||||
    console.warn(`未匹配到[${component}]组件名对应的组件文件`);
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,7 @@ router.beforeEach(async (to, from, next) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 终端不需要连接系统websocket消息
 | 
			
		||||
    if (to.path != '/machine/terminal') {
 | 
			
		||||
    if (to.path != '/machine/terminal' && to.path != '/machine/terminal-rdp') {
 | 
			
		||||
        syssocket.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -54,6 +54,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
 | 
			
		||||
            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;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
@@ -114,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 | 
			
		||||
            // 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
 | 
			
		||||
            layout: 'classic',
 | 
			
		||||
 | 
			
		||||
            terminalTheme: 'light',
 | 
			
		||||
            // ssh终端字体颜色
 | 
			
		||||
            terminalForeground: '#C5C8C6',
 | 
			
		||||
            // ssh终端背景色
 | 
			
		||||
@@ -192,5 +193,23 @@ export const useThemeConfig = defineStore('themeConfig', {
 | 
			
		||||
        setWatermarkNowTime() {
 | 
			
		||||
            this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', 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%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    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-tap-highlight-color: transparent;
 | 
			
		||||
    background-color: var(--bg-main-color);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,335 +7,353 @@
 | 
			
		||||
------------------------------- */
 | 
			
		||||
// 菜单搜索
 | 
			
		||||
.el-autocomplete-suggestion__wrap {
 | 
			
		||||
	max-height: 280px !important;
 | 
			
		||||
    max-height: 280px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Form 表单
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-form {
 | 
			
		||||
	// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
 | 
			
		||||
	.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 {
 | 
			
		||||
			margin-bottom: 22px !important;
 | 
			
		||||
		}
 | 
			
		||||
		.el-form-item--default.el-form-item:last-of-type,
 | 
			
		||||
		.el-form-item--small.el-form-item:last-of-type {
 | 
			
		||||
			margin-bottom: 18px !important;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
 | 
			
		||||
	.el-form-item .el-form-item__label .el-icon {
 | 
			
		||||
		margin-right: 0px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    // 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
 | 
			
		||||
    .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 {
 | 
			
		||||
            margin-bottom: 22px !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .el-form-item--default.el-form-item:last-of-type,
 | 
			
		||||
        .el-form-item--small.el-form-item:last-of-type {
 | 
			
		||||
            margin-bottom: 18px !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
 | 
			
		||||
    .el-form-item .el-form-item__label .el-icon {
 | 
			
		||||
        margin-right: 0px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Alert 警告
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-alert {
 | 
			
		||||
	border: 1px solid;
 | 
			
		||||
    border: 1px solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-alert__title {
 | 
			
		||||
	word-break: break-all;
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Message 消息提示
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-message {
 | 
			
		||||
	min-width: unset !important;
 | 
			
		||||
	padding: 15px !important;
 | 
			
		||||
	box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
 | 
			
		||||
    min-width: unset !important;
 | 
			
		||||
    padding: 15px !important;
 | 
			
		||||
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* NavMenu 导航菜单
 | 
			
		||||
------------------------------- */
 | 
			
		||||
// 鼠标 hover 时颜色
 | 
			
		||||
.el-menu-hover-bg-color {
 | 
			
		||||
	background-color: var(--bg-menuBarActiveColor) !important;
 | 
			
		||||
    background-color: var(--bg-menuBarActiveColor) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 默认样式修改
 | 
			
		||||
.el-menu {
 | 
			
		||||
	border-right: none !important;
 | 
			
		||||
	width: 220px;
 | 
			
		||||
    border-right: none !important;
 | 
			
		||||
    width: 220px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-menu-item {
 | 
			
		||||
	height: 56px !important;
 | 
			
		||||
	line-height: 56px !important;
 | 
			
		||||
    height: 56px !important;
 | 
			
		||||
    line-height: 56px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-menu-item,
 | 
			
		||||
.el-sub-menu__title {
 | 
			
		||||
	color: var(--bg-menuBarColor);
 | 
			
		||||
    color: var(--bg-menuBarColor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
 | 
			
		||||
.el-menu--collapse {
 | 
			
		||||
	width: 64px !important;
 | 
			
		||||
    width: 64px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 外部链接时
 | 
			
		||||
.el-menu-item a,
 | 
			
		||||
.el-menu-item a:hover,
 | 
			
		||||
.el-menu-item i,
 | 
			
		||||
.el-sub-menu__title i {
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 第三方图标字体间距/大小设置
 | 
			
		||||
.el-menu-item .iconfont,
 | 
			
		||||
.el-sub-menu .iconfont,
 | 
			
		||||
.el-menu-item .fa,
 | 
			
		||||
.el-sub-menu .fa {
 | 
			
		||||
	@include generalIcon;
 | 
			
		||||
    @include generalIcon;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
 | 
			
		||||
.el-menu-item.is-active,
 | 
			
		||||
.el-sub-menu.is-active .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 {
 | 
			
		||||
	@extend .el-menu-hover-bg-color;
 | 
			
		||||
    @extend .el-menu-hover-bg-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
 | 
			
		||||
	background-color: unset !important;
 | 
			
		||||
    background-color: unset !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 子级菜单背景颜色
 | 
			
		||||
// .el-menu--inline {
 | 
			
		||||
// 	background: var(--next-bg-menuBar-light-1);
 | 
			
		||||
// }
 | 
			
		||||
// 水平菜单、横向菜单折叠 a 标签
 | 
			
		||||
.el-popper.is-dark a {
 | 
			
		||||
	color: var(--el-color-white) !important;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
    color: var(--el-color-white) !important;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 水平菜单、横向菜单折叠背景色
 | 
			
		||||
.el-popper.is-pure.is-light {
 | 
			
		||||
	// 水平菜单
 | 
			
		||||
	.el-menu--vertical {
 | 
			
		||||
		background: var(--bg-menuBar);
 | 
			
		||||
		.el-sub-menu.is-active .el-sub-menu__title {
 | 
			
		||||
			color: var(--el-menu-active-color);
 | 
			
		||||
		}
 | 
			
		||||
		.el-popper.is-pure.is-light {
 | 
			
		||||
			.el-menu--vertical {
 | 
			
		||||
				.el-sub-menu .el-sub-menu__title {
 | 
			
		||||
					background-color: unset !important;
 | 
			
		||||
					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;
 | 
			
		||||
			color: var(--bg-topBarColor);
 | 
			
		||||
			.el-sub-menu__title {
 | 
			
		||||
				height: 48px !important;
 | 
			
		||||
				line-height: 48px !important;
 | 
			
		||||
				color: var(--bg-topBarColor);
 | 
			
		||||
			}
 | 
			
		||||
			.el-popper.is-pure.is-light {
 | 
			
		||||
				.el-menu--horizontal {
 | 
			
		||||
					.el-sub-menu .el-sub-menu__title {
 | 
			
		||||
						background-color: unset !important;
 | 
			
		||||
						color: var(--bg-topBarColor);
 | 
			
		||||
					}
 | 
			
		||||
					.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--vertical {
 | 
			
		||||
        background: var(--bg-menuBar);
 | 
			
		||||
 | 
			
		||||
        .el-sub-menu.is-active .el-sub-menu__title {
 | 
			
		||||
            color: var(--el-menu-active-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .el-popper.is-pure.is-light {
 | 
			
		||||
            .el-menu--vertical {
 | 
			
		||||
                .el-sub-menu .el-sub-menu__title {
 | 
			
		||||
                    background-color: unset !important;
 | 
			
		||||
                    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;
 | 
			
		||||
            color: var(--bg-topBarColor);
 | 
			
		||||
 | 
			
		||||
            .el-sub-menu__title {
 | 
			
		||||
                height: 48px !important;
 | 
			
		||||
                line-height: 48px !important;
 | 
			
		||||
                color: var(--bg-topBarColor);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .el-popper.is-pure.is-light {
 | 
			
		||||
                .el-menu--horizontal {
 | 
			
		||||
                    .el-sub-menu .el-sub-menu__title {
 | 
			
		||||
                        background-color: unset !important;
 | 
			
		||||
                        color: var(--bg-topBarColor);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    .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 {
 | 
			
		||||
	border-bottom: none !important;
 | 
			
		||||
	width: 100% !important;
 | 
			
		||||
	.el-menu-item,
 | 
			
		||||
	.el-sub-menu__title {
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
    border-bottom: none !important;
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
 | 
			
		||||
    .el-menu-item,
 | 
			
		||||
    .el-sub-menu__title {
 | 
			
		||||
        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--collapse {
 | 
			
		||||
	.el-menu-item .iconfont,
 | 
			
		||||
	.el-sub-menu .iconfont,
 | 
			
		||||
	.el-menu-item .fa,
 | 
			
		||||
	.el-sub-menu .fa {
 | 
			
		||||
		margin-right: 0 !important;
 | 
			
		||||
	}
 | 
			
		||||
	.el-sub-menu__title {
 | 
			
		||||
		padding-right: 0 !important;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    .el-menu-item .iconfont,
 | 
			
		||||
    .el-sub-menu .iconfont,
 | 
			
		||||
    .el-menu-item .fa,
 | 
			
		||||
    .el-sub-menu .fa {
 | 
			
		||||
        margin-right: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-sub-menu__title {
 | 
			
		||||
        padding-right: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Tabs 标签页
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-tabs__nav-wrap::after {
 | 
			
		||||
	height: 1px !important;
 | 
			
		||||
    height: 1px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dropdown 下拉菜单
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-dropdown-menu {
 | 
			
		||||
	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);
 | 
			
		||||
	}
 | 
			
		||||
    list-style: none !important;
 | 
			
		||||
    /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Steps 步骤条
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-step__icon-inner {
 | 
			
		||||
	font-size: 30px !important;
 | 
			
		||||
	font-weight: 400 !important;
 | 
			
		||||
}
 | 
			
		||||
.el-step__title {
 | 
			
		||||
	font-size: 14px;
 | 
			
		||||
.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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dialog 对话框
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-overlay {
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	.el-overlay-dialog {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		position: unset !important;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		.el-dialog {
 | 
			
		||||
			margin: 0 auto !important;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			.el-dialog__body {
 | 
			
		||||
				padding: 20px !important;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    .el-overlay-dialog {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        position: unset !important;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        .el-dialog {
 | 
			
		||||
            margin: 0 auto !important;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
 | 
			
		||||
            .el-dialog__body {
 | 
			
		||||
                padding: 20px !important;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-dialog__body {
 | 
			
		||||
	max-height: calc(90vh - 111px) !important;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	overflow-x: hidden;
 | 
			
		||||
    max-height: calc(90vh - 111px) !important;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Card 卡片
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-card__header {
 | 
			
		||||
	padding: 15px 20px;
 | 
			
		||||
    padding: 15px 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Table 表格 element plus 2.2.0 版本
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-table {
 | 
			
		||||
	.el-button.is-text {
 | 
			
		||||
		padding: 0;
 | 
			
		||||
	}
 | 
			
		||||
    .el-button.is-text {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* scrollbar
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-scrollbar__bar {
 | 
			
		||||
	z-index: 4;
 | 
			
		||||
    z-index: 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
 | 
			
		||||
.el-scrollbar__wrap {
 | 
			
		||||
	max-height: 100%;
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-select-dropdown .el-scrollbar__wrap {
 | 
			
		||||
	overflow-x: scroll !important;
 | 
			
		||||
    overflow-x: scroll !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*修复Select 选择器高度问题*/
 | 
			
		||||
.el-select-dropdown__wrap {
 | 
			
		||||
	max-height: 274px !important;
 | 
			
		||||
    max-height: 274px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*修复Cascader 级联选择器高度问题*/
 | 
			
		||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
 | 
			
		||||
	height: 204px !important;
 | 
			
		||||
    height: 204px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
 | 
			
		||||
.layout-container-view .el-scrollbar__view {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
 | 
			
		||||
.layout-columns-warp .layout-aside .el-scrollbar__view {
 | 
			
		||||
	height: unset !important;
 | 
			
		||||
    height: unset !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Pagination 分页
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-pagination__editor {
 | 
			
		||||
	margin-right: 8px;
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*深色模式时分页高亮问题*/
 | 
			
		||||
.el-pagination.is-background .btn-next.is-active,
 | 
			
		||||
.el-pagination.is-background .btn-prev.is-active,
 | 
			
		||||
.el-pagination.is-background .el-pager li.is-active {
 | 
			
		||||
	background-color: var(--el-color-primary) !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;
 | 
			
		||||
	}
 | 
			
		||||
    background-color: var(--el-color-primary) !important;
 | 
			
		||||
    color: var(--el-color-white) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Breadcrumb 面包屑
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-breadcrumb__inner a:hover,
 | 
			
		||||
.el-breadcrumb__inner.is-link:hover {
 | 
			
		||||
	color: var(--el-color-primary);
 | 
			
		||||
    color: var(--el-color-primary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-breadcrumb__inner a,
 | 
			
		||||
.el-breadcrumb__inner.is-link {
 | 
			
		||||
	color: var(--bg-topBarColor);
 | 
			
		||||
	font-weight: normal;
 | 
			
		||||
    color: var(--bg-topBarColor);
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// el-tooltip使用自定义主题时的样式
 | 
			
		||||
.el-popper.is-customized {
 | 
			
		||||
    /* 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));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-popper.is-customized .el-popper__arrow::before {
 | 
			
		||||
    background: linear-gradient(45deg, #b2e68d, #bce689);
 | 
			
		||||
    right: 0;
 | 
			
		||||
@@ -343,7 +361,9 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.el-dialog {
 | 
			
		||||
    border-radius: 6px; /* 设置圆角 */
 | 
			
		||||
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    /* 设置圆角 */
 | 
			
		||||
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
			
		||||
    /* 添加轻微阴影效果 */
 | 
			
		||||
    border: 1px solid var(--el-border-color-lighter);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
        globalI18n: string;
 | 
			
		||||
        globalComponentSize: string;
 | 
			
		||||
        terminalTheme: string;
 | 
			
		||||
        terminalForeground: string;
 | 
			
		||||
        terminalBackground: 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 插件模块
 | 
			
		||||
declare module 'sql-formatter';
 | 
			
		||||
declare module 'jsoneditor';
 | 
			
		||||
declare module 'asciinema-player';
 | 
			
		||||
declare module 'vue-grid-layout';
 | 
			
		||||
declare module 'splitpanes';
 | 
			
		||||
declare module 'uuid';
 | 
			
		||||
 
 | 
			
		||||
@@ -98,4 +98,3 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@/router/staticRouter
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										220
									
								
								mayfly_go_web/src/views/flow/ProcdefEdit.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										220
									
								
								mayfly_go_web/src/views/flow/ProcdefEdit.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
<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="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,
 | 
			
		||||
        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.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 = 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>
 | 
			
		||||
							
								
								
									
										171
									
								
								mayfly_go_web/src/views/flow/ProcinstDetail.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										171
									
								
								mayfly_go_web/src/views/flow/ProcinstDetail.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,171 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :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="2" 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" />
 | 
			
		||||
                        <!-- {{ procinst.creator }} -->
 | 
			
		||||
                    </el-descriptions-item>
 | 
			
		||||
                    <el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                    <div v-if="procinst.duration">
 | 
			
		||||
                        <el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
 | 
			
		||||
                        <el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <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="备注">
 | 
			
		||||
                        {{ 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]"
 | 
			
		||||
                    :biz-key="procinst.bizKey"
 | 
			
		||||
                    :biz-form="procinst.bizForm"
 | 
			
		||||
                >
 | 
			
		||||
                </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 { dateFormat } from '@/common/utils/date';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
 | 
			
		||||
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.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 = shallowReactive({
 | 
			
		||||
    db_sql_exec_flow: DbSqlExecBiz,
 | 
			
		||||
    redis_run_write_cmd_flow: RedisRunWriteCmdBiz,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
							
								
								
									
										126
									
								
								mayfly_go_web/src/views/flow/ProcinstList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								mayfly_go_web/src/views/flow/ProcinstList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
<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 v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</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"
 | 
			
		||||
        />
 | 
			
		||||
    </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';
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { selectionData, query, procinstDetail } = 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 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>
 | 
			
		||||
							
								
								
									
										20
									
								
								mayfly_go_web/src/views/flow/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mayfly_go_web/src/views/flow/api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
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'),
 | 
			
		||||
    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">{{ `${dateFormat(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 { dateFormat } from '@/common/utils/date';
 | 
			
		||||
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'),
 | 
			
		||||
    RedisRunWriteCmd: EnumValue.of('redis_run_write_cmd_flow', 'Redis-执行write命令'),
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										79
									
								
								mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										79
									
								
								mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-descriptions :column="3" border>
 | 
			
		||||
            <el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
            <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
            <el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item :span="1" label="类型">
 | 
			
		||||
                <SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
 | 
			
		||||
            </el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
            <el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item label="表">
 | 
			
		||||
                {{ sqlExec.table }}
 | 
			
		||||
            </el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item label="类型">
 | 
			
		||||
                <el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
 | 
			
		||||
            </el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item label="执行SQL">
 | 
			
		||||
                <monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
 | 
			
		||||
            </el-descriptions-item>
 | 
			
		||||
        </el-descriptions>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch, onMounted } from 'vue';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
import { dbApi } from '@/views/ops/db/api';
 | 
			
		||||
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    // 业务key
 | 
			
		||||
    bizKey: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        default: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    sqlExec: {
 | 
			
		||||
        sql: '',
 | 
			
		||||
    } as any,
 | 
			
		||||
    db: {} as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { sqlExec, db } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    getDbSqlExec(props.bizKey);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.bizKey,
 | 
			
		||||
    (newValue: any) => {
 | 
			
		||||
        getDbSqlExec(newValue);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const getDbSqlExec = async (bizKey: string) => {
 | 
			
		||||
    if (!bizKey) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
 | 
			
		||||
    if (!res.list) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    state.sqlExec = res.list?.[0];
 | 
			
		||||
    const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
 | 
			
		||||
    state.db = dbRes.list?.[0];
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
							
								
								
									
										80
									
								
								mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										80
									
								
								mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-descriptions :column="3" border>
 | 
			
		||||
            <el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
 | 
			
		||||
            <el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
            <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch, onMounted } from 'vue';
 | 
			
		||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
 | 
			
		||||
import { redisApi } from '@/views/ops/redis/api';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    // 业务表单
 | 
			
		||||
    bizForm: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        default: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    cmd: '',
 | 
			
		||||
    db: 0,
 | 
			
		||||
    redis: {} as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { cmd, redis } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    parseRunCmdForm(props.bizForm);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.bizForm,
 | 
			
		||||
    (newValue: any) => {
 | 
			
		||||
        parseRunCmdForm(newValue);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const parseRunCmdForm = async (bizForm: string) => {
 | 
			
		||||
    if (!bizForm) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const form = JSON.parse(bizForm);
 | 
			
		||||
 | 
			
		||||
    const cmds = form.cmd.map((item: any, index: number) => {
 | 
			
		||||
        if (index === 0) {
 | 
			
		||||
            return item; // 第一个元素直接返回原值
 | 
			
		||||
        }
 | 
			
		||||
        if (typeof item === 'string') {
 | 
			
		||||
            return `'${item}'`; // 字符串加单引号
 | 
			
		||||
        }
 | 
			
		||||
        return item; // 其他类型直接返回
 | 
			
		||||
    });
 | 
			
		||||
    state.cmd = cmds.join('  ');
 | 
			
		||||
    state.db = form.db;
 | 
			
		||||
 | 
			
		||||
    const res = await redisApi.redisList.request({ id: form.id });
 | 
			
		||||
    if (!res.list) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    state.redis = res.list?.[0];
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -1,137 +1,541 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="home-container">
 | 
			
		||||
    <div class="home-container personal">
 | 
			
		||||
        <el-row :gutter="15">
 | 
			
		||||
            <el-col :sm="6" class="mb15">
 | 
			
		||||
                <div @click="toPage({ id: 'personal' })" class="home-card-item home-card-first">
 | 
			
		||||
                    <div class="flex-margin flex">
 | 
			
		||||
                        <img :src="userInfo.photo" />
 | 
			
		||||
                        <div class="home-card-first-right ml15">
 | 
			
		||||
                            <div class="flex-margin">
 | 
			
		||||
                                <div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
            <!-- 个人信息 -->
 | 
			
		||||
            <el-col :xs="24" :sm="16">
 | 
			
		||||
                <el-card shadow="hover" header="个人信息">
 | 
			
		||||
                    <div class="personal-user">
 | 
			
		||||
                        <div class="personal-user-left">
 | 
			
		||||
                            <el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
 | 
			
		||||
                                <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">{{ dateFormat(userInfo.lastLoginTime) }}</div>
 | 
			
		||||
                                        </el-col>
 | 
			
		||||
                                    </el-row>
 | 
			
		||||
                                </el-col>
 | 
			
		||||
                            </el-row>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                </el-card>
 | 
			
		||||
            </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">
 | 
			
		||||
                        <div class="home-card-item-title pb3">{{ v.title }}</div>
 | 
			
		||||
                        <div class="home-card-item-title-num pb6" :id="v.id"></div>
 | 
			
		||||
 | 
			
		||||
            <!-- 消息通知 -->
 | 
			
		||||
            <el-col :xs="24" :sm="8" class="pl15 personal-info">
 | 
			
		||||
                <el-card shadow="hover">
 | 
			
		||||
                    <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>
 | 
			
		||||
                    <i :class="v.icon" :style="{ color: v.iconColor }"></i>
 | 
			
		||||
                </div>
 | 
			
		||||
                </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('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">
 | 
			
		||||
                                        {{ dateFormat(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" />
 | 
			
		||||
                                    </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">
 | 
			
		||||
                                        {{ dateFormat(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" />
 | 
			
		||||
                                    </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">
 | 
			
		||||
                                        {{ dateFormat(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" />
 | 
			
		||||
                                    </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">
 | 
			
		||||
                                        {{ dateFormat(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" />
 | 
			
		||||
                                    </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">
 | 
			
		||||
                        {{ dateFormat(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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
 | 
			
		||||
import { toRefs, reactive, onMounted, computed } from 'vue';
 | 
			
		||||
// import * as echarts from 'echarts';
 | 
			
		||||
import { CountUp } from 'countup.js';
 | 
			
		||||
import { formatAxis } from '@/common/utils/format';
 | 
			
		||||
import { indexApi } from './api';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { storeToRefs } from 'pinia';
 | 
			
		||||
import { useUserInfo } from '@/store/userInfo';
 | 
			
		||||
import { personApi } from '../personal/api';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const { userInfo } = storeToRefs(useUserInfo());
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    topCardItemList: [
 | 
			
		||||
        {
 | 
			
		||||
            title: 'Linux机器',
 | 
			
		||||
            id: 'machineNum',
 | 
			
		||||
            color: '#F95959',
 | 
			
		||||
    accountInfo: {
 | 
			
		||||
        roles: [],
 | 
			
		||||
    },
 | 
			
		||||
    msgs: [],
 | 
			
		||||
    msgDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        query: {
 | 
			
		||||
            pageSize: 10,
 | 
			
		||||
            pageNum: 1,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            title: '数据库',
 | 
			
		||||
            id: 'dbNum',
 | 
			
		||||
            color: '#8595F4',
 | 
			
		||||
        msgs: {
 | 
			
		||||
            list: [],
 | 
			
		||||
            total: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            title: 'redis',
 | 
			
		||||
            id: 'redisNum',
 | 
			
		||||
            color: '#1abc9c',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            title: 'Mongo',
 | 
			
		||||
            id: 'mongoNum',
 | 
			
		||||
            color: '#FEBB50',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    },
 | 
			
		||||
    resourceOpTableHeight: 180,
 | 
			
		||||
    defaultLogSize: 5,
 | 
			
		||||
    machine: {
 | 
			
		||||
        num: 0,
 | 
			
		||||
        opLogs: [],
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        num: 0,
 | 
			
		||||
        opLogs: [],
 | 
			
		||||
    },
 | 
			
		||||
    redis: {
 | 
			
		||||
        num: 0,
 | 
			
		||||
        opLogs: [],
 | 
			
		||||
    },
 | 
			
		||||
    mongo: {
 | 
			
		||||
        num: 0,
 | 
			
		||||
        opLogs: [],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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(() => {
 | 
			
		||||
    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 initNumCountUp = async () => {
 | 
			
		||||
    indexApi.machineDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('machineNum', res.machineNum).start();
 | 
			
		||||
const initData = async () => {
 | 
			
		||||
    resourceOpLogApi.getAccountResourceOpLogs
 | 
			
		||||
        .request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
 | 
			
		||||
        .then((res: any) => {
 | 
			
		||||
            state.machine.opLogs = res.list;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize }).then((res: any) => {
 | 
			
		||||
        state.db.opLogs = res.list;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize }).then((res: any) => {
 | 
			
		||||
        state.redis.opLogs = res.list;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
 | 
			
		||||
        state.mongo.opLogs = res.list;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.machineDashbord.request().then((res: any) => {
 | 
			
		||||
        state.machine.num = res.machineNum;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.dbDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('dbNum', res.dbNum).start();
 | 
			
		||||
        });
 | 
			
		||||
        state.db.num = res.dbNum;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.redisDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('redisNum', res.redisNum).start();
 | 
			
		||||
        });
 | 
			
		||||
        state.redis.num = res.redisNum;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.mongoDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('mongoNum', res.mongoNum).start();
 | 
			
		||||
        });
 | 
			
		||||
        state.mongo.num = res.mongoNum;
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toPage = (item: any) => {
 | 
			
		||||
    switch (item.id) {
 | 
			
		||||
const toPage = (item: any, codePath = '') => {
 | 
			
		||||
    let path;
 | 
			
		||||
    switch (item) {
 | 
			
		||||
        case 'personal': {
 | 
			
		||||
            router.push('/personal');
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'mongoNum': {
 | 
			
		||||
            router.push('/mongo/mongo-data-operation');
 | 
			
		||||
        case 'mongo': {
 | 
			
		||||
            useAutoOpenResource().setMongoCodePath(codePath);
 | 
			
		||||
            path = '/mongo/mongo-data-operation';
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'machineNum': {
 | 
			
		||||
            router.push('/machine/machines');
 | 
			
		||||
        case 'machine': {
 | 
			
		||||
            useAutoOpenResource().setMachineCodePath(codePath);
 | 
			
		||||
            path = '/machine/machines-op';
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dbNum': {
 | 
			
		||||
            router.push('/dbms/sql-exec');
 | 
			
		||||
        case 'db': {
 | 
			
		||||
            useAutoOpenResource().setDbCodePath(codePath);
 | 
			
		||||
            path = '/dbms/sql-exec';
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'redisNum': {
 | 
			
		||||
            router.push('/redis/data-operation');
 | 
			
		||||
        case 'redis': {
 | 
			
		||||
            useAutoOpenResource().setRedisCodePath(codePath);
 | 
			
		||||
            path = '/redis/data-operation';
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 页面加载时
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    initNumCountUp();
 | 
			
		||||
    // initHomeLaboratory();
 | 
			
		||||
    // initHomeOvertime();
 | 
			
		||||
});
 | 
			
		||||
    router.push({ path });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
@import '@/theme/mixins/index.scss';
 | 
			
		||||
 | 
			
		||||
.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 text-ellipsis(1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .personal-item {
 | 
			
		||||
                display: flex;
 | 
			
		||||
                align-items: center;
 | 
			
		||||
                font-size: 13px;
 | 
			
		||||
 | 
			
		||||
                .personal-item-label {
 | 
			
		||||
                    color: gray;
 | 
			
		||||
                    @include text-ellipsis(1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .personal-item-value {
 | 
			
		||||
                    @include 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 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 {
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +586,7 @@ onMounted(() => {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .home-card-item-title-num {
 | 
			
		||||
                font-size: 18px;
 | 
			
		||||
                font-size: 2vw;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .home-card-item-tip-num {
 | 
			
		||||
@@ -190,124 +594,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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								mayfly_go_web/src/views/ops/component/ResourceAuthCert.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								mayfly_go_web/src/views/ops/component/ResourceAuthCert.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="props.authCerts">
 | 
			
		||||
        <el-select value-key="name" v-model="selectAuthCert" size="small">
 | 
			
		||||
            <el-option v-for="item in props.authCerts" :key="item.name" :label="item.username" :value="item">
 | 
			
		||||
                {{ item.username }}
 | 
			
		||||
                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                <EnumTag :value="item.type" :enums="AuthCertTypeEnum" />
 | 
			
		||||
                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                <EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
 | 
			
		||||
            </el-option>
 | 
			
		||||
        </el-select>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    authCerts: {
 | 
			
		||||
        type: [Array<any>],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const selectAuthCert = defineModel('selectAuthCert');
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
							
								
								
									
										266
									
								
								mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,266 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="auth-cert-edit">
 | 
			
		||||
        <el-dialog :title="props.title" v-model="dialogVisible" :show-close="false" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
 | 
			
		||||
            <el-form ref="acForm" :model="state.form" label-width="auto" :rules="rules">
 | 
			
		||||
                <el-form-item prop="type" label="凭证类型" required>
 | 
			
		||||
                    <el-select @change="changeType" v-model="form.type" placeholder="请选择凭证类型">
 | 
			
		||||
                        <el-option
 | 
			
		||||
                            v-for="item in AuthCertTypeEnum"
 | 
			
		||||
                            :key="item.value"
 | 
			
		||||
                            :label="item.label"
 | 
			
		||||
                            :value="item.value"
 | 
			
		||||
                            v-show="!props.disableType?.includes(item.value)"
 | 
			
		||||
                        >
 | 
			
		||||
                        </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="ciphertextType" label="密文类型" required>
 | 
			
		||||
                    <el-select v-model="form.ciphertextType" placeholder="请选择密文类型" @change="changeCiphertextType">
 | 
			
		||||
                        <el-option
 | 
			
		||||
                            v-for="item in AuthCertCiphertextTypeEnum"
 | 
			
		||||
                            :key="item.value"
 | 
			
		||||
                            :label="item.label"
 | 
			
		||||
                            :value="item.value"
 | 
			
		||||
                            v-show="!props.disableCiphertextType?.includes(item.value)"
 | 
			
		||||
                            :disabled="item.value == AuthCertCiphertextTypeEnum.Public.value && form.type == AuthCertTypeEnum.Public.value"
 | 
			
		||||
                        >
 | 
			
		||||
                        </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <template v-if="showResourceEdit">
 | 
			
		||||
                    <el-form-item prop="type" label="资源类型" required>
 | 
			
		||||
                        <el-select :disabled="form.id" v-model="form.resourceType" placeholder="请选择资源类型">
 | 
			
		||||
                            <el-option
 | 
			
		||||
                                :key="TagResourceTypeEnum.Machine.value"
 | 
			
		||||
                                :label="TagResourceTypeEnum.Machine.label"
 | 
			
		||||
                                :value="TagResourceTypeEnum.Machine.value"
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                    <el-form-item prop="resourceCode" label="资源编号" required>
 | 
			
		||||
                        <el-input :disabled="form.id" v-model="form.resourceCode" placeholder="请输入资源编号"></el-input>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="name" label="名称" required>
 | 
			
		||||
                    <el-input :disabled="form.id" v-model="form.name" placeholder="请输入凭证名 (全局唯一)"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <template v-if="form.ciphertextType != AuthCertCiphertextTypeEnum.Public.value">
 | 
			
		||||
                    <el-form-item prop="username" label="用户名">
 | 
			
		||||
                        <el-input v-model="form.username"></el-input>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
 | 
			
		||||
                    <el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.Password.value" prop="ciphertext" label="密码">
 | 
			
		||||
                        <el-input type="password" show-password clearable v-model.trim="form.ciphertext" placeholder="请输入密码" autocomplete="new-password">
 | 
			
		||||
                            <template #suffix>
 | 
			
		||||
                                <SvgIcon v-if="form.id" v-auth="'authcert:showciphertext'" @click="getCiphertext" name="search" />
 | 
			
		||||
                            </template>
 | 
			
		||||
                        </el-input>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
 | 
			
		||||
                    <el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="ciphertext" label="秘钥">
 | 
			
		||||
                        <div class="w100" style="position: relative">
 | 
			
		||||
                            <SvgIcon
 | 
			
		||||
                                v-if="form.id"
 | 
			
		||||
                                v-auth="'authcert:showciphertext'"
 | 
			
		||||
                                @click="getCiphertext"
 | 
			
		||||
                                name="search"
 | 
			
		||||
                                style="position: absolute; top: 5px; right: 5px; cursor: pointer; z-index: 1"
 | 
			
		||||
                            />
 | 
			
		||||
                            <el-input type="textarea" :rows="5" v-model="form.ciphertext" placeholder="请将私钥文件内容拷贝至此"> </el-input>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
 | 
			
		||||
                    <el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="passphrase" label="秘钥密码">
 | 
			
		||||
                        <el-input type="password" show-password v-model="form.extra.passphrase"> </el-input>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <template v-else>
 | 
			
		||||
                    <el-form-item label="公共凭证">
 | 
			
		||||
                        <el-select default-first-option filterable v-model="form.ciphertext" @change="changePublicAuthCert">
 | 
			
		||||
                            <el-option v-for="item in state.publicAuthCerts" :key="item.name" :label="item.name" :value="item.name">
 | 
			
		||||
                                {{ item.name }}
 | 
			
		||||
                                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                                {{ item.username }}
 | 
			
		||||
                                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                                <EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
 | 
			
		||||
                                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                                {{ item.remark }}
 | 
			
		||||
                            </el-option>
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <el-form-item label="备注">
 | 
			
		||||
                    <el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </el-form>
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="cancelEdit">取 消</el-button>
 | 
			
		||||
                    <el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive, ref, toRefs, computed, watch } from 'vue';
 | 
			
		||||
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { resourceAuthCertApi } from '../tag/api';
 | 
			
		||||
import { ResourceCodePattern } from '@/common/pattern';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    title: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        default: '凭证保存',
 | 
			
		||||
    },
 | 
			
		||||
    authCert: {
 | 
			
		||||
        type: Object,
 | 
			
		||||
    },
 | 
			
		||||
    disableCiphertextType: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
    },
 | 
			
		||||
    disableType: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
    },
 | 
			
		||||
    // 是否为资源编辑该授权凭证,即机器编辑等页面等
 | 
			
		||||
    resourceEdit: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const DefaultForm = {
 | 
			
		||||
    id: null,
 | 
			
		||||
    name: '',
 | 
			
		||||
    username: '',
 | 
			
		||||
    ciphertextType: AuthCertCiphertextTypeEnum.Password.value,
 | 
			
		||||
    type: AuthCertTypeEnum.Private.value,
 | 
			
		||||
    resourceType: TagResourceTypeEnum.AuthCert.value,
 | 
			
		||||
    resourceCode: '',
 | 
			
		||||
    ciphertext: '',
 | 
			
		||||
    extra: {} as any,
 | 
			
		||||
    remark: '',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
    name: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请输入凭证名',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pattern: ResourceCodePattern.pattern,
 | 
			
		||||
            message: ResourceCodePattern.message,
 | 
			
		||||
            trigger: ['blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    resourceCode: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请输入资源编号',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['confirm', 'cancel']);
 | 
			
		||||
 | 
			
		||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
 | 
			
		||||
 | 
			
		||||
const acForm: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    form: { ...DefaultForm },
 | 
			
		||||
    btnLoading: false,
 | 
			
		||||
    publicAuthCerts: [] as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const showResourceEdit = computed(() => {
 | 
			
		||||
    return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(dialogVisible, (val: any) => {
 | 
			
		||||
    if (val) {
 | 
			
		||||
        setForm(props.authCert);
 | 
			
		||||
    } else {
 | 
			
		||||
        cancelEdit();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const setForm = (val: any) => {
 | 
			
		||||
    val = { ...val };
 | 
			
		||||
    if (!val.extra) {
 | 
			
		||||
        val.extra = {};
 | 
			
		||||
    }
 | 
			
		||||
    state.form = val;
 | 
			
		||||
    if (state.form.ciphertextType == AuthCertCiphertextTypeEnum.Public.value) {
 | 
			
		||||
        getPublicAuthCerts();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const { form, btnLoading } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const changeType = (val: any) => {
 | 
			
		||||
    // 如果选择了公共凭证,则需要保证密文类型不能为公共凭证
 | 
			
		||||
    if (val == AuthCertTypeEnum.Public.value && state.form.ciphertextType == AuthCertCiphertextTypeEnum.Public.value) {
 | 
			
		||||
        state.form.ciphertextType = AuthCertCiphertextTypeEnum.Password.value;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const changeCiphertextType = (val: any) => {
 | 
			
		||||
    if (val == AuthCertCiphertextTypeEnum.Public.value) {
 | 
			
		||||
        getPublicAuthCerts();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const changePublicAuthCert = (val: string) => {
 | 
			
		||||
    // 使用公共授权凭证名称赋值username
 | 
			
		||||
    state.form.username = val;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getPublicAuthCerts = async () => {
 | 
			
		||||
    const res = await resourceAuthCertApi.listByQuery.request({
 | 
			
		||||
        type: AuthCertTypeEnum.Public.value,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 100,
 | 
			
		||||
    });
 | 
			
		||||
    state.publicAuthCerts = res.list;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getCiphertext = async () => {
 | 
			
		||||
    const res = await resourceAuthCertApi.detail.request({ name: state.form.name });
 | 
			
		||||
    state.form.ciphertext = res.ciphertext;
 | 
			
		||||
    state.form.extra.passphrase = res.extra?.passphrase;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelEdit = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        state.form = { ...DefaultForm };
 | 
			
		||||
        acForm.value?.resetFields();
 | 
			
		||||
        emit('cancel');
 | 
			
		||||
    }, 300);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const btnOk = async () => {
 | 
			
		||||
    acForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (valid) {
 | 
			
		||||
            emit('confirm', { ...state.form });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,147 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="auth-cert-manage">
 | 
			
		||||
        <el-table :data="authCerts" max-height="180" stripe size="small">
 | 
			
		||||
            <el-table-column min-wdith="120px">
 | 
			
		||||
                <template #header>
 | 
			
		||||
                    <el-button v-auth="'authcert:save'" class="ml0" type="primary" circle size="small" icon="Plus" @click="edit(null)"> </el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
 | 
			
		||||
                    <el-button class="ml1" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
 | 
			
		||||
 | 
			
		||||
                    <el-button
 | 
			
		||||
                        title="测试连接"
 | 
			
		||||
                        :loading="props.testConnBtnLoading && scope.$index == state.idx"
 | 
			
		||||
                        :disabled="props.testConnBtnLoading"
 | 
			
		||||
                        class="ml1"
 | 
			
		||||
                        type="success"
 | 
			
		||||
                        @click="testConn(scope.row, scope.$index)"
 | 
			
		||||
                        icon="Link"
 | 
			
		||||
                        link
 | 
			
		||||
                    ></el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
 | 
			
		||||
            <el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column>
 | 
			
		||||
            <el-table-column prop="username" label="用户名" min-width="120px" show-overflow-tooltip> </el-table-column>
 | 
			
		||||
            <el-table-column prop="ciphertextType" label="密文类型" width="100px">
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <EnumTag :value="scope.row.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
            <el-table-column prop="type" label="凭证类型" width="100px">
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <EnumTag :value="scope.row.type" :enums="AuthCertTypeEnum" />
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
            <el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
 | 
			
		||||
        </el-table>
 | 
			
		||||
 | 
			
		||||
        <ResourceAuthCertEdit
 | 
			
		||||
            v-model:visible="state.dvisible"
 | 
			
		||||
            :auth-cert="state.form"
 | 
			
		||||
            @confirm="btnOk"
 | 
			
		||||
            @cancel="cancelEdit"
 | 
			
		||||
            :disable-type="[AuthCertTypeEnum.Public.value]"
 | 
			
		||||
            :disable-ciphertext-type="props.disableCiphertextType"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, reactive } from 'vue';
 | 
			
		||||
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
import { resourceAuthCertApi } from '../tag/api';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import ResourceAuthCertEdit from './ResourceAuthCertEdit.vue';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    resourceType: { type: Number },
 | 
			
		||||
    resourceCode: { type: String },
 | 
			
		||||
    disableCiphertextType: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
    },
 | 
			
		||||
    testConnBtnLoading: { type: Boolean },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const authCerts = defineModel<any>('modelValue', { required: true, default: [] });
 | 
			
		||||
const emit = defineEmits(['testConn']);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dvisible: false,
 | 
			
		||||
    params: [] as any,
 | 
			
		||||
    form: {},
 | 
			
		||||
    idx: -1,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    getAuthCerts();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getAuthCerts = async () => {
 | 
			
		||||
    if (!props.resourceCode || !props.resourceType) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await resourceAuthCertApi.listByQuery.request({
 | 
			
		||||
        resourceCode: props.resourceCode,
 | 
			
		||||
        resourceType: props.resourceType,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 100,
 | 
			
		||||
    });
 | 
			
		||||
    authCerts.value = res.list?.reverse() || [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const testConn = async (row: any, idx: number) => {
 | 
			
		||||
    state.idx = idx;
 | 
			
		||||
    emit('testConn', row);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const edit = (form: any, idx = -1) => {
 | 
			
		||||
    state.idx = idx;
 | 
			
		||||
    if (form) {
 | 
			
		||||
        state.form = form;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = { ciphertextType: AuthCertCiphertextTypeEnum.Password.value, type: AuthCertTypeEnum.Private.value, extra: {} };
 | 
			
		||||
    }
 | 
			
		||||
    state.dvisible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteRow = (idx: any) => {
 | 
			
		||||
    authCerts.value.splice(idx, 1);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelEdit = () => {
 | 
			
		||||
    state.dvisible = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const btnOk = async (authCert: any) => {
 | 
			
		||||
    const isEdit = authCert.id;
 | 
			
		||||
    if (!isEdit) {
 | 
			
		||||
        const res = await resourceAuthCertApi.listByQuery.request({
 | 
			
		||||
            name: authCert.name,
 | 
			
		||||
            pageNum: 1,
 | 
			
		||||
            pageSize: 100,
 | 
			
		||||
        });
 | 
			
		||||
        if (res.total) {
 | 
			
		||||
            ElMessage.error('该授权凭证名称已存在');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isEdit || state.idx >= 0) {
 | 
			
		||||
        authCerts.value[state.idx] = authCert;
 | 
			
		||||
        cancelEdit();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (authCerts.value?.filter((x: any) => x.username == authCert.username || x.name == authCert.name).length > 0) {
 | 
			
		||||
        ElMessage.error('该名称或用户名已存在于该账号列表中');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    authCerts.value.push(authCert);
 | 
			
		||||
    cancelEdit();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
 | 
			
		||||
        <el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
 | 
			
		||||
            <template #reference>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <!-- <el-button type="primary" link size="small">标签</el-button> -->
 | 
			
		||||
                    <SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
 | 
			
		||||
        </el-popover>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive, toRefs } from 'vue';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    resourceCode: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    resourceType: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tags: [] as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tags } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const getTags = async () => {
 | 
			
		||||
    state.tags = await tagApi.getTagResources.request({
 | 
			
		||||
        resourceCode: props.resourceCode,
 | 
			
		||||
        resourceType: props.resourceType,
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
							
								
								
									
										33
									
								
								mayfly_go_web/src/views/ops/component/ResourceTags.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mayfly_go_web/src/views/ops/component/ResourceTags.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="props.tags">
 | 
			
		||||
        <el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
 | 
			
		||||
            <TagInfo :tag-path="tag.codePath" />
 | 
			
		||||
            <span class="ml3">{{ tag.codePath }}</span>
 | 
			
		||||
 | 
			
		||||
            <!-- 展示剩余的标签信息 -->
 | 
			
		||||
            <el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
 | 
			
		||||
                <template #reference>
 | 
			
		||||
                    <SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <el-row v-for="i in props.tags.slice(1)" :key="i">
 | 
			
		||||
                    <TagInfo :tag-path="i.codePath" />
 | 
			
		||||
                    <span class="ml3">{{ i.codePath }}</span>
 | 
			
		||||
                </el-row>
 | 
			
		||||
            </el-popover>
 | 
			
		||||
        </el-row>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import TagInfo from './TagInfo.vue';
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    tags: {
 | 
			
		||||
        type: [Array<any>],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, onMounted } from 'vue';
 | 
			
		||||
import { machineApi } from '../machine/api';
 | 
			
		||||
import { MachineProtocolEnum } from '../machine/enums';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    modelValue: {
 | 
			
		||||
@@ -46,7 +47,7 @@ onMounted(async () => {
 | 
			
		||||
 | 
			
		||||
const getSshTunnelMachines = async () => {
 | 
			
		||||
    if (state.sshTunnelMachineList.length == 0) {
 | 
			
		||||
        const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
 | 
			
		||||
        const res = await machineApi.list.request({ pageNum: 1, pageSize: 100, protocol: MachineProtocolEnum.Ssh.value });
 | 
			
		||||
        state.sshTunnelMachineList = res.list;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								mayfly_go_web/src/views/ops/component/TagCodePath.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								mayfly_go_web/src/views/ops/component/TagCodePath.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="paths">
 | 
			
		||||
        <el-row v-for="(path, idx) in paths?.slice(0, 1)" :key="idx">
 | 
			
		||||
            <span v-for="item in parseTagPath(path)" :key="item.code">
 | 
			
		||||
                <SvgIcon
 | 
			
		||||
                    :name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
 | 
			
		||||
                    :color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
 | 
			
		||||
                    class="mr2"
 | 
			
		||||
                />
 | 
			
		||||
                <span> {{ item.code }}</span>
 | 
			
		||||
                <SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <!-- 展示剩余的标签信息 -->
 | 
			
		||||
            <el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
 | 
			
		||||
                <template #reference>
 | 
			
		||||
                    <SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <el-row v-for="i in paths.slice(1)" :key="i">
 | 
			
		||||
                    <span v-for="item in parseTagPath(i)" :key="item.code">
 | 
			
		||||
                        <SvgIcon
 | 
			
		||||
                            :name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
 | 
			
		||||
                            :color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
 | 
			
		||||
                            class="mr2"
 | 
			
		||||
                        />
 | 
			
		||||
                        <span> {{ item.code }}</span>
 | 
			
		||||
                        <SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
 | 
			
		||||
                    </span>
 | 
			
		||||
                </el-row>
 | 
			
		||||
            </el-popover>
 | 
			
		||||
        </el-row>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    path: {
 | 
			
		||||
        type: [String, Array<string>],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const paths = computed(() => {
 | 
			
		||||
    if (Array.isArray(props.path)) {
 | 
			
		||||
        return props.path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [props.path];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const parseTagPath = (tagPath: string = '') => {
 | 
			
		||||
    if (!tagPath) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    const res = [] as any;
 | 
			
		||||
    const codes = tagPath.split('/');
 | 
			
		||||
    for (let code of codes) {
 | 
			
		||||
        const typeAndCode = code.split('|');
 | 
			
		||||
 | 
			
		||||
        if (typeAndCode.length == 1) {
 | 
			
		||||
            const tagCode = typeAndCode[0];
 | 
			
		||||
            if (!tagCode) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            res.push({
 | 
			
		||||
                type: TagResourceTypeEnum.Tag.value,
 | 
			
		||||
                code: typeAndCode[0],
 | 
			
		||||
            });
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        res.push({
 | 
			
		||||
            type: typeAndCode[0],
 | 
			
		||||
            code: typeAndCode[1],
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res[res.length - 1].isEnd = true;
 | 
			
		||||
    return res;
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="tag-tree card pd5">
 | 
			
		||||
        <el-scrollbar>
 | 
			
		||||
            <el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
 | 
			
		||||
    <div class="card pd5">
 | 
			
		||||
        <el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
 | 
			
		||||
        <el-scrollbar class="tag-tree">
 | 
			
		||||
            <el-tree
 | 
			
		||||
                ref="treeRef"
 | 
			
		||||
                :highlight-current="true"
 | 
			
		||||
@@ -10,14 +10,15 @@
 | 
			
		||||
                :props="treeProps"
 | 
			
		||||
                lazy
 | 
			
		||||
                node-key="key"
 | 
			
		||||
                :expand-on-click-node="true"
 | 
			
		||||
                :expand-on-click-node="false"
 | 
			
		||||
                :filter-node-method="filterNode"
 | 
			
		||||
                @node-click="treeNodeClick"
 | 
			
		||||
                @node-expand="treeNodeClick"
 | 
			
		||||
                @node-contextmenu="nodeContextmenu"
 | 
			
		||||
                :default-expanded-keys="props.defaultExpandedKeys"
 | 
			
		||||
            >
 | 
			
		||||
                <template #default="{ node, data }">
 | 
			
		||||
                    <span>
 | 
			
		||||
                    <span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
 | 
			
		||||
                        <span v-if="data.type.value == TagTreeNode.TagPath">
 | 
			
		||||
                            <tag-info :tag-path="data.label" />
 | 
			
		||||
                        </span>
 | 
			
		||||
@@ -25,10 +26,18 @@
 | 
			
		||||
                        <slot v-else :node="node" :data="data" name="prefix"></slot>
 | 
			
		||||
 | 
			
		||||
                        <span class="ml3" :title="data.labelRemark">
 | 
			
		||||
                            <slot name="label" :data="data"> {{ data.label }}</slot>
 | 
			
		||||
                            <slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
 | 
			
		||||
                            <!-- 禁用状态 -->
 | 
			
		||||
                            <slot name="disabledLabel" :data="data" v-else>
 | 
			
		||||
                                <el-link type="danger" disabled :underline="false">
 | 
			
		||||
                                    {{ `${data.label}` }}
 | 
			
		||||
                                </el-link>
 | 
			
		||||
                            </slot>
 | 
			
		||||
                        </span>
 | 
			
		||||
 | 
			
		||||
                        <slot :node="node" :data="data" name="suffix"></slot>
 | 
			
		||||
                        <span class="label-suffix">
 | 
			
		||||
                            <slot :node="node" :data="data" name="suffix"></slot>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-tree>
 | 
			
		||||
@@ -50,6 +59,9 @@ const props = defineProps({
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    defaultExpandedKeys: {
 | 
			
		||||
        type: [Array],
 | 
			
		||||
    },
 | 
			
		||||
    tagPathNodeType: {
 | 
			
		||||
        type: [NodeType],
 | 
			
		||||
        required: true,
 | 
			
		||||
@@ -134,16 +146,30 @@ const loadNode = async (node: any, resolve: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const treeNodeClick = (data: any) => {
 | 
			
		||||
    emit('nodeClick', data);
 | 
			
		||||
    if (data.type.nodeClickFunc) {
 | 
			
		||||
    if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
 | 
			
		||||
        emit('nodeClick', data);
 | 
			
		||||
        data.type.nodeClickFunc(data);
 | 
			
		||||
    }
 | 
			
		||||
    // 关闭可能存在的右击菜单
 | 
			
		||||
    contextmenuRef.value.closeContextmenu();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 树节点双击事件
 | 
			
		||||
const treeNodeDblclick = (data: any) => {
 | 
			
		||||
    // emit('nodeDblick', data);
 | 
			
		||||
    if (!data.disabled && data.type.nodeDblclickFunc) {
 | 
			
		||||
        data.type.nodeDblclickFunc(data);
 | 
			
		||||
    }
 | 
			
		||||
    // 关闭可能存在的右击菜单
 | 
			
		||||
    contextmenuRef.value.closeContextmenu();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 树节点右击事件
 | 
			
		||||
const nodeContextmenu = (event: any, data: any) => {
 | 
			
		||||
    if (data.disabled) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 加载当前节点是否需要显示右击菜单
 | 
			
		||||
    let items = data.type.contextMenuItems;
 | 
			
		||||
    if (!items || items.length == 0) {
 | 
			
		||||
@@ -179,18 +205,32 @@ const getNode = (nodeKey: any) => {
 | 
			
		||||
    return node;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setCurrentKey = (nodeKey: any) => {
 | 
			
		||||
    treeRef.value.setCurrentKey(nodeKey);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
    reloadNode,
 | 
			
		||||
    getNode,
 | 
			
		||||
    setCurrentKey,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.tag-tree {
 | 
			
		||||
    height: calc(100vh - 108px);
 | 
			
		||||
    height: calc(100vh - 148px);
 | 
			
		||||
 | 
			
		||||
    .el-tree {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        min-width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .label-suffix {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        right: 10px;
 | 
			
		||||
        color: #c4c9c4;
 | 
			
		||||
        font-size: 10px;
 | 
			
		||||
        margin-top: 2px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										167
									
								
								mayfly_go_web/src/views/ops/component/TagTreeCheck.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										167
									
								
								mayfly_go_web/src/views/ops/component/TagTreeCheck.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="w100 tag-tree-check">
 | 
			
		||||
        <el-input v-model="filterTag" @input="onFilterValChanged" clearable placeholder="输入关键字过滤" size="small" />
 | 
			
		||||
        <div class="mt3" style="border: 1px solid var(--el-border-color)">
 | 
			
		||||
            <el-scrollbar :style="{ height: props.height }">
 | 
			
		||||
                <el-tree
 | 
			
		||||
                    v-bind="$attrs"
 | 
			
		||||
                    ref="tagTreeRef"
 | 
			
		||||
                    :data="state.tags"
 | 
			
		||||
                    :default-expanded-keys="checkedTags"
 | 
			
		||||
                    :default-checked-keys="checkedTags"
 | 
			
		||||
                    multiple
 | 
			
		||||
                    :render-after-expand="true"
 | 
			
		||||
                    show-checkbox
 | 
			
		||||
                    check-strictly
 | 
			
		||||
                    :node-key="$props.nodeKey"
 | 
			
		||||
                    :props="{
 | 
			
		||||
                        value: $props.nodeKey,
 | 
			
		||||
                        label: 'codePath',
 | 
			
		||||
                        children: 'children',
 | 
			
		||||
                        disabled: 'disabled',
 | 
			
		||||
                    }"
 | 
			
		||||
                    @check="tagTreeNodeCheck"
 | 
			
		||||
                    :filter-node-method="filterNode"
 | 
			
		||||
                >
 | 
			
		||||
                    <template #default="{ data }">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <SvgIcon
 | 
			
		||||
                                :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
 | 
			
		||||
                                :color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <span class="font13 ml5">
 | 
			
		||||
                                {{ data.code }}
 | 
			
		||||
                                <span style="color: #3c8dbc">【</span>
 | 
			
		||||
                                {{ data.name }}
 | 
			
		||||
                                <span style="color: #3c8dbc">】</span>
 | 
			
		||||
                                <el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-tree>
 | 
			
		||||
            </el-scrollbar>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, reactive, onMounted } from 'vue';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    height: {
 | 
			
		||||
        type: [String, Number],
 | 
			
		||||
        default: 'calc(100vh - 330px)',
 | 
			
		||||
    },
 | 
			
		||||
    tagType: {
 | 
			
		||||
        type: [Number, Array<Number>],
 | 
			
		||||
        default: TagResourceTypeEnum.Tag.value,
 | 
			
		||||
    },
 | 
			
		||||
    nodeKey: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        default: 'codePath',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const checkedTags = defineModel<Array<any>>('modelValue', {
 | 
			
		||||
    default: () => [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const tagTreeRef: any = ref(null);
 | 
			
		||||
const filterTag = ref('');
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tags: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    search();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    let tagType: any = props.tagType;
 | 
			
		||||
    if (Array.isArray(props.tagType)) {
 | 
			
		||||
        tagType = props.tagType.join(',');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.tags = await tagApi.getTagTrees.request({ type: tagType });
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        const checkedNodes = tagTreeRef.value.getCheckedNodes();
 | 
			
		||||
        console.log('check nodes: ', checkedNodes);
 | 
			
		||||
        // 禁用选中节点的所有父节点,不可选中
 | 
			
		||||
        for (let checkNodeData of checkedNodes) {
 | 
			
		||||
            disableParentNodes(tagTreeRef.value.getNode(checkNodeData.codePath).parent);
 | 
			
		||||
        }
 | 
			
		||||
    }, 200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterNode = (value: string, data: any) => {
 | 
			
		||||
    if (!value) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onFilterValChanged = (val: string) => {
 | 
			
		||||
    tagTreeRef.value!.filter(val);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const tagTreeNodeCheck = (data: any) => {
 | 
			
		||||
    const node = tagTreeRef.value.getNode(data.codePath);
 | 
			
		||||
    console.log('check node: ', node);
 | 
			
		||||
 | 
			
		||||
    if (node.checked) {
 | 
			
		||||
        // 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
 | 
			
		||||
        unCheckParentNodes(node.parent);
 | 
			
		||||
        disableParentNodes(node.parent);
 | 
			
		||||
    } else {
 | 
			
		||||
        // 如果取消了选中,则需要根据条件恢复父节点的选中状态
 | 
			
		||||
        disableParentNodes(node.parent, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 更新绑定的值
 | 
			
		||||
    checkedTags.value = tagTreeRef.value.getCheckedKeys(false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const unCheckParentNodes = (node: any) => {
 | 
			
		||||
    if (!node) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    tagTreeRef.value.setChecked(node, false, false);
 | 
			
		||||
    unCheckParentNodes(node.parent);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 禁用该节点以及所有父节点
 | 
			
		||||
 * @param node 节点
 | 
			
		||||
 * @param disable 是否禁用
 | 
			
		||||
 */
 | 
			
		||||
const disableParentNodes = (node: any, disable = true) => {
 | 
			
		||||
    if (!node) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!disable) {
 | 
			
		||||
        // 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
 | 
			
		||||
        for (let oneLevelNodes of node.childNodes) {
 | 
			
		||||
            if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    node.data.disabled = disable;
 | 
			
		||||
    disableParentNodes(node.parent, disable);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.tag-tree-check {
 | 
			
		||||
    .el-tree {
 | 
			
		||||
        min-width: 100%;
 | 
			
		||||
        // 横向滚动生效
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -14,6 +14,9 @@
 | 
			
		||||
        v-model="modelValue"
 | 
			
		||||
        @change="changeNode"
 | 
			
		||||
    >
 | 
			
		||||
        <template #prefix="{ node, data }">
 | 
			
		||||
            <slot name="iconPrefix" :node="node" :data="data" />
 | 
			
		||||
        </template>
 | 
			
		||||
        <template #default="{ node, data }">
 | 
			
		||||
            <span>
 | 
			
		||||
                <span v-if="data.type.value == TagTreeNode.TagPath">
 | 
			
		||||
@@ -33,7 +36,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
 | 
			
		||||
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { NodeType, TagTreeNode } from './tag';
 | 
			
		||||
import TagInfo from './TagInfo.vue';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
 
 | 
			
		||||
@@ -2,23 +2,22 @@
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-tree-select
 | 
			
		||||
            v-bind="$attrs"
 | 
			
		||||
            v-model="selectTags"
 | 
			
		||||
            v-model="state.selectTags"
 | 
			
		||||
            @change="changeTag"
 | 
			
		||||
            style="width: 100%"
 | 
			
		||||
            :data="tags"
 | 
			
		||||
            placeholder="请选择关联标签"
 | 
			
		||||
            :render-after-expand="true"
 | 
			
		||||
            :default-expanded-keys="[selectTags]"
 | 
			
		||||
            :default-expanded-keys="defaultExpandedKeys"
 | 
			
		||||
            show-checkbox
 | 
			
		||||
            node-key="id"
 | 
			
		||||
            node-key="codePath"
 | 
			
		||||
            :props="{
 | 
			
		||||
                value: 'id',
 | 
			
		||||
                value: 'codePath',
 | 
			
		||||
                label: 'codePath',
 | 
			
		||||
                children: 'children',
 | 
			
		||||
            }"
 | 
			
		||||
        >
 | 
			
		||||
            <template #default="{ data }">
 | 
			
		||||
                <span class="custom-tree-node">
 | 
			
		||||
                    <SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" class="mr2" />
 | 
			
		||||
                    <span style="font-size: 13px">
 | 
			
		||||
                        {{ data.code }}
 | 
			
		||||
                        <span style="color: #3c8dbc">【</span>
 | 
			
		||||
@@ -33,42 +32,45 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, onMounted } from 'vue';
 | 
			
		||||
import { toRefs, reactive, onMounted, computed } from 'vue';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    resourceCode: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        required: true,
 | 
			
		||||
    selectTags: {
 | 
			
		||||
        type: [Array<any>, Object],
 | 
			
		||||
    },
 | 
			
		||||
    resourceType: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        required: true,
 | 
			
		||||
    tagType: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
        default: TagResourceTypeEnum.Tag.value,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tags: [],
 | 
			
		||||
    // 单选则为id,多选为id数组
 | 
			
		||||
    selectTags: [],
 | 
			
		||||
    // 单选则为codePath,多选为codePath数组
 | 
			
		||||
    selectTags: [] as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tags, selectTags } = toRefs(state);
 | 
			
		||||
const { tags } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (props.resourceCode) {
 | 
			
		||||
        const resourceTags = await tagApi.getTagResources.request({
 | 
			
		||||
            resourceCode: props.resourceCode,
 | 
			
		||||
            resourceType: props.resourceType,
 | 
			
		||||
        });
 | 
			
		||||
        state.selectTags = resourceTags.map((x: any) => x.tagId);
 | 
			
		||||
        changeTag();
 | 
			
		||||
const defaultExpandedKeys = computed(() => {
 | 
			
		||||
    if (Array.isArray(state.selectTags)) {
 | 
			
		||||
        // 如果 state.selectTags 是数组,直接返回
 | 
			
		||||
        return state.selectTags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.tags = await tagApi.getTagTrees.request(null);
 | 
			
		||||
    // 如果 state.selectTags 不是数组,转换为包含 state.selectTags 的数组
 | 
			
		||||
    return [state.selectTags];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    state.selectTags = props.selectTags;
 | 
			
		||||
    state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const changeTag = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
 | 
			
		||||
     */
 | 
			
		||||
    isLeaf: boolean = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否禁用状态
 | 
			
		||||
     */
 | 
			
		||||
    disabled: boolean = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 额外需要传递的参数
 | 
			
		||||
     */
 | 
			
		||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    withDisabled(disabled: boolean) {
 | 
			
		||||
        this.disabled = disabled;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    withParams(params: any) {
 | 
			
		||||
        this.params = params;
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -91,8 +101,14 @@ export class NodeType {
 | 
			
		||||
 | 
			
		||||
    loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 节点点击事件
 | 
			
		||||
     */
 | 
			
		||||
    nodeClickFunc: (node: TagTreeNode) => void;
 | 
			
		||||
 | 
			
		||||
    // 节点双击事件
 | 
			
		||||
    nodeDblclickFunc: (node: TagTreeNode) => void;
 | 
			
		||||
 | 
			
		||||
    constructor(value: number) {
 | 
			
		||||
        this.value = value;
 | 
			
		||||
    }
 | 
			
		||||
@@ -117,6 +133,16 @@ export class NodeType {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 赋值节点双击事件回调函数
 | 
			
		||||
     * @param func 节点双击事件回调函数
 | 
			
		||||
     * @returns this
 | 
			
		||||
     */
 | 
			
		||||
    withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
 | 
			
		||||
        this.nodeDblclickFunc = func;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 赋值右击菜单按钮选项
 | 
			
		||||
     * @param contextMenuItems 右击菜单按钮选项
 | 
			
		||||
@@ -145,3 +171,44 @@ export function getTagPathSearchItem(resourceType: number) {
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 根据标签路径获取对应的类型与编号数组
 | 
			
		||||
 * @param codePath 编号路径  tag1/tag2/1|xxx/11|yyy/
 | 
			
		||||
 * @returns {1: ['xxx'], 11: ['yyy']}
 | 
			
		||||
 */
 | 
			
		||||
export function getTagTypeCodeByPath(codePath: string) {
 | 
			
		||||
    const result = {};
 | 
			
		||||
    const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
 | 
			
		||||
 | 
			
		||||
    for (let part of parts) {
 | 
			
		||||
        if (!part) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        let [key, value] = part.split('|'); // 分割数字和值部分
 | 
			
		||||
        // 如果不存在第二个参数,则说明为标签类型
 | 
			
		||||
        if (!value) {
 | 
			
		||||
            value = key;
 | 
			
		||||
            key = '-1';
 | 
			
		||||
        }
 | 
			
		||||
        if (!result[key]) {
 | 
			
		||||
            result[key] = [];
 | 
			
		||||
        }
 | 
			
		||||
        result[key].push(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function expandCodePath(codePath: string) {
 | 
			
		||||
    const parts = codePath.split('/');
 | 
			
		||||
    const result = [];
 | 
			
		||||
    let currentPath = '';
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < parts.length - 1; i++) {
 | 
			
		||||
        currentPath += parts[i] + '/';
 | 
			
		||||
        result.push(currentPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,11 @@
 | 
			
		||||
                <el-form-item prop="startTime" label="开始时间">
 | 
			
		||||
                    <el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="intervalDay" label="备份周期">
 | 
			
		||||
                    <el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天)"></el-input>
 | 
			
		||||
                <el-form-item prop="intervalDay" label="备份周期(天)">
 | 
			
		||||
                    <el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="maxSaveDays" label="备份历史保留天数">
 | 
			
		||||
                    <el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
@@ -92,6 +95,14 @@ const rules = {
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    maxSaveDays: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            pattern: /^[0-9]\d*$/,
 | 
			
		||||
            message: '请输入非负整数',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const backupForm: any = ref(null);
 | 
			
		||||
@@ -102,9 +113,10 @@ const state = reactive({
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        dbNames: '',
 | 
			
		||||
        name: '',
 | 
			
		||||
        intervalDay: null,
 | 
			
		||||
        intervalDay: 1,
 | 
			
		||||
        startTime: null as any,
 | 
			
		||||
        repeated: null as any,
 | 
			
		||||
        repeated: true,
 | 
			
		||||
        maxSaveDays: 0,
 | 
			
		||||
    },
 | 
			
		||||
    btnLoading: false,
 | 
			
		||||
    dbNamesSelected: [] as any,
 | 
			
		||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
 | 
			
		||||
        state.form.name = data.name;
 | 
			
		||||
        state.form.intervalDay = data.intervalDay;
 | 
			
		||||
        state.form.startTime = data.startTime;
 | 
			
		||||
        state.form.maxSaveDays = data.maxSaveDays;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.editOrCreate = false;
 | 
			
		||||
        state.form.name = '';
 | 
			
		||||
        state.form.intervalDay = null;
 | 
			
		||||
        state.form.intervalDay = 1;
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
 | 
			
		||||
        state.form.maxSaveDays = 0;
 | 
			
		||||
        getDbNamesWithoutBackup();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -10,48 +10,35 @@
 | 
			
		||||
            width="38%"
 | 
			
		||||
        >
 | 
			
		||||
            <el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
 | 
			
		||||
                <el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
 | 
			
		||||
                    <tag-tree-select
 | 
			
		||||
                        @change-tag="
 | 
			
		||||
                            (tagIds) => {
 | 
			
		||||
                                form.tagId = tagIds;
 | 
			
		||||
                                tagSelectRef.validate();
 | 
			
		||||
                            }
 | 
			
		||||
                        "
 | 
			
		||||
                        multiple
 | 
			
		||||
                        :resource-code="form.code"
 | 
			
		||||
                        :resource-type="TagResourceTypeEnum.Db.value"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                    />
 | 
			
		||||
                <el-form-item prop="code" label="编号" required>
 | 
			
		||||
                    <el-input
 | 
			
		||||
                        :disabled="form.id"
 | 
			
		||||
                        v-model.trim="form.code"
 | 
			
		||||
                        placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
 | 
			
		||||
                        auto-complete="off"
 | 
			
		||||
                    ></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="name" label="名称" required>
 | 
			
		||||
                    <el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="instanceId" label="数据库实例" required>
 | 
			
		||||
                    <el-select
 | 
			
		||||
                        :disabled="form.id !== undefined"
 | 
			
		||||
                        remote
 | 
			
		||||
                        :remote-method="getInstances"
 | 
			
		||||
                        @change="changeInstance"
 | 
			
		||||
                        v-model="form.instanceId"
 | 
			
		||||
                        placeholder="请输入实例名称搜索并选择实例"
 | 
			
		||||
                        filterable
 | 
			
		||||
                        clearable
 | 
			
		||||
                        class="w100"
 | 
			
		||||
                    >
 | 
			
		||||
                        <el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
 | 
			
		||||
                <el-form-item prop="authCertName" label="授权凭证" required>
 | 
			
		||||
                    <el-select @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
 | 
			
		||||
                        <el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
 | 
			
		||||
                            {{ item.name }}
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
                            {{ item.type }} / {{ item.host }}:{{ item.port }}
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            {{ item.username }}
 | 
			
		||||
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            <EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
 | 
			
		||||
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            {{ item.remark }}
 | 
			
		||||
                        </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="name" label="别名" required>
 | 
			
		||||
                    <el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="database" label="数据库名">
 | 
			
		||||
                    <el-select
 | 
			
		||||
                        v-model="dbNamesSelected"
 | 
			
		||||
@@ -80,7 +67,7 @@
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="cancel()">取 消</el-button>
 | 
			
		||||
                    <el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
 | 
			
		||||
                    <el-button type="primary" @click="btnOk">确 定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
@@ -88,17 +75,25 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch, ref } from 'vue';
 | 
			
		||||
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import type { CheckboxValueType } from 'element-plus';
 | 
			
		||||
import { DbType } from '@/views/ops/db/dialect';
 | 
			
		||||
import { ResourceCodePattern } from '@/common/pattern';
 | 
			
		||||
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
import { resourceAuthCertApi } from '../tag/api';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
    instance: {
 | 
			
		||||
        type: [Boolean, Object],
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: [Boolean, Object],
 | 
			
		||||
    },
 | 
			
		||||
@@ -108,7 +103,7 @@ const props = defineProps({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'confirm']);
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
    tagId: [
 | 
			
		||||
@@ -126,7 +121,18 @@ const rules = {
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    code: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请输入编码',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pattern: ResourceCodePattern.pattern,
 | 
			
		||||
            message: ResourceCodePattern.message,
 | 
			
		||||
            trigger: ['blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    name: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
@@ -134,6 +140,13 @@ const rules = {
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    authCertName: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请选择授权凭证',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    database: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
@@ -147,7 +160,7 @@ const checkAllDbNames = ref(false);
 | 
			
		||||
const indeterminateDbNames = ref(false);
 | 
			
		||||
 | 
			
		||||
const dbForm: any = ref(null);
 | 
			
		||||
const tagSelectRef: any = ref(null);
 | 
			
		||||
// const tagSelectRef: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
@@ -155,80 +168,83 @@ const state = reactive({
 | 
			
		||||
    dbNamesSelected: [] as any,
 | 
			
		||||
    dbNamesFiltered: [] as any,
 | 
			
		||||
    filterString: '',
 | 
			
		||||
    selectInstalce: {} as any,
 | 
			
		||||
    authCerts: [] as any,
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        tagId: [],
 | 
			
		||||
        // tagId: [],
 | 
			
		||||
        name: null,
 | 
			
		||||
        code: '',
 | 
			
		||||
        database: '',
 | 
			
		||||
        remark: '',
 | 
			
		||||
        instanceId: null as any,
 | 
			
		||||
        authCertName: '',
 | 
			
		||||
    },
 | 
			
		||||
    instances: [] as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveDbExec } = dbApi.saveDb.useApi(form);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue: any) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dialogVisible = props.visible;
 | 
			
		||||
    if (!state.dialogVisible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (newValue.db) {
 | 
			
		||||
        state.form = { ...newValue.db };
 | 
			
		||||
 | 
			
		||||
    const db: any = props.db;
 | 
			
		||||
    if (db.code) {
 | 
			
		||||
        state.form = { ...db };
 | 
			
		||||
        // state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
 | 
			
		||||
        // 将数据库名使用空格切割,获取所有数据库列表
 | 
			
		||||
        state.dbNamesSelected = newValue.db.database.split(' ');
 | 
			
		||||
        state.dbNamesSelected = db.database.split(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = {} as any;
 | 
			
		||||
        state.dbNamesSelected = [];
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const changeInstance = () => {
 | 
			
		||||
    state.dbNamesSelected = [];
 | 
			
		||||
    getAllDatabase();
 | 
			
		||||
const changeAuthCert = (val: string) => {
 | 
			
		||||
    getAllDatabase(val);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getAllDatabase = async () => {
 | 
			
		||||
    if (state.form.instanceId > 0) {
 | 
			
		||||
        state.allDatabases = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
 | 
			
		||||
    }
 | 
			
		||||
const getAuthCerts = async () => {
 | 
			
		||||
    const inst: any = props.instance;
 | 
			
		||||
    const res = await resourceAuthCertApi.listByQuery.request({
 | 
			
		||||
        resourceCode: inst.code,
 | 
			
		||||
        resourceType: TagResourceTypeEnum.Db.value,
 | 
			
		||||
        pageSize: 100,
 | 
			
		||||
    });
 | 
			
		||||
    state.authCerts = res.list || [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getInstances = async (instanceName: string = '', id = 0) => {
 | 
			
		||||
    if (!id && !instanceName) {
 | 
			
		||||
        state.instances = [];
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = await dbApi.instances.request({ id, name: instanceName });
 | 
			
		||||
    if (data) {
 | 
			
		||||
        state.instances = data.list;
 | 
			
		||||
const getAllDatabase = async (authCertName: string) => {
 | 
			
		||||
    const req = { ...(props.instance as any) };
 | 
			
		||||
    req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
 | 
			
		||||
    let dbs = await dbApi.getAllDatabase.request(req);
 | 
			
		||||
    state.allDatabases = dbs;
 | 
			
		||||
 | 
			
		||||
    // 如果是oracle,且没查出数据库列表,则取实例sid
 | 
			
		||||
    let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
 | 
			
		||||
    if (instance && instance.type === DbType.oracle && dbs.length === 0) {
 | 
			
		||||
        state.allDatabases = [instance.sid];
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const open = async () => {
 | 
			
		||||
    if (state.form.instanceId) {
 | 
			
		||||
        // 根据id获取,因为需要回显实例名称
 | 
			
		||||
        await getInstances('', state.form.instanceId);
 | 
			
		||||
    await getAuthCerts();
 | 
			
		||||
    if (state.form.authCertName) {
 | 
			
		||||
        await getAllDatabase(state.form.authCertName);
 | 
			
		||||
    }
 | 
			
		||||
    await getAllDatabase();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const btnOk = async () => {
 | 
			
		||||
    dbForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            ElMessage.error('请正确填写信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    try {
 | 
			
		||||
        await dbForm.value.validate();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
        ElMessage.error('请正确填写信息');
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        await saveDbExec();
 | 
			
		||||
        ElMessage.success('保存成功');
 | 
			
		||||
        emit('val-change', state.form);
 | 
			
		||||
        cancel();
 | 
			
		||||
    });
 | 
			
		||||
    emit('confirm', state.form);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resetInputDb = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,8 @@
 | 
			
		||||
            :before-query-fn="checkRouteTagPath"
 | 
			
		||||
            :search-items="searchItems"
 | 
			
		||||
            v-model:query-form="query"
 | 
			
		||||
            :show-selection="true"
 | 
			
		||||
            v-model:selection-data="state.selectionData"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
            lazy
 | 
			
		||||
        >
 | 
			
		||||
            <template #instanceSelect>
 | 
			
		||||
                <el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
 | 
			
		||||
@@ -23,11 +22,6 @@
 | 
			
		||||
                </el-select>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tableHeader>
 | 
			
		||||
                <el-button v-auth="perms.saveDb" type="primary" icon="plus" @click="editDb(false)">添加</el-button>
 | 
			
		||||
                <el-button v-auth="perms.delDb" :disabled="selectionData.length < 1" @click="deleteDb()" type="danger" icon="delete">删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #type="{ data }">
 | 
			
		||||
                <el-tooltip :content="data.type" placement="top">
 | 
			
		||||
                    <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
 | 
			
		||||
@@ -38,16 +32,26 @@
 | 
			
		||||
                {{ `${data.host}:${data.port}` }}
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #database="{ data }">
 | 
			
		||||
                <el-popover placement="bottom" :width="200" trigger="click">
 | 
			
		||||
                    <template #reference>
 | 
			
		||||
                        <el-button @click="state.currentDbs = data.database" type="primary" link>查看库</el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <el-table :data="filterDbs" size="small">
 | 
			
		||||
                        <el-table-column prop="dbName" label="数据库">
 | 
			
		||||
                            <template #header>
 | 
			
		||||
                                <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
 | 
			
		||||
                            </template>
 | 
			
		||||
                        </el-table-column>
 | 
			
		||||
                    </el-table>
 | 
			
		||||
                </el-popover>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
 | 
			
		||||
                <ResourceTags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <span v-if="actionBtns[perms.saveDb]">
 | 
			
		||||
                    <el-button type="primary" @click="editDb(data)" link>编辑</el-button>
 | 
			
		||||
                    <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                </span>
 | 
			
		||||
 | 
			
		||||
                <el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
 | 
			
		||||
                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
@@ -61,7 +65,7 @@
 | 
			
		||||
                    <template #dropdown>
 | 
			
		||||
                        <el-dropdown-menu>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
 | 
			
		||||
                                备份任务
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
@@ -83,21 +87,21 @@
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
        <el-dialog width="720px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
 | 
			
		||||
        <el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
 | 
			
		||||
            <el-row justify="space-between">
 | 
			
		||||
                <el-col :span="9">
 | 
			
		||||
                    <el-form-item label="导出内容: ">
 | 
			
		||||
                        <el-checkbox-group v-model="exportDialog.contents" :min="1">
 | 
			
		||||
                            <el-checkbox label="结构" />
 | 
			
		||||
                            <el-checkbox label="数据" />
 | 
			
		||||
                            <el-checkbox label="结构" value="结构" />
 | 
			
		||||
                            <el-checkbox label="数据" value="数据" />
 | 
			
		||||
                        </el-checkbox-group>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </el-col>
 | 
			
		||||
                <el-col :span="9">
 | 
			
		||||
                    <el-form-item label="扩展名: ">
 | 
			
		||||
                        <el-radio-group v-model="exportDialog.extName">
 | 
			
		||||
                            <el-radio label="sql" />
 | 
			
		||||
                            <el-radio label="gzip" />
 | 
			
		||||
                            <el-radio label="sql" value="sql" />
 | 
			
		||||
                            <el-radio label="gzip" value="gzip" />
 | 
			
		||||
                        </el-radio-group>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </el-col>
 | 
			
		||||
@@ -164,39 +168,44 @@
 | 
			
		||||
            <db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
 | 
			
		||||
        <el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
 | 
			
		||||
            <el-descriptions title="详情" :column="3" border>
 | 
			
		||||
                <!-- <el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data?.tagPath }}</el-descriptions-item> -->
 | 
			
		||||
                <el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="类型">{{ infoDialog.instance?.type }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="类型">
 | 
			
		||||
                    <SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
 | 
			
		||||
                </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
 | 
			
		||||
            </el-descriptions>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <db-edit @val-change="search" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
 | 
			
		||||
        <db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, Ref } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
import ResourceTag from '../component/ResourceTag.vue';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
@@ -210,33 +219,38 @@ import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import DbBackupList from './DbBackupList.vue';
 | 
			
		||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
 | 
			
		||||
import DbRestoreList from './DbRestoreList.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
 | 
			
		||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    base: 'db',
 | 
			
		||||
    saveDb: 'db:save',
 | 
			
		||||
    delDb: 'db:del',
 | 
			
		||||
    backupDb: 'db:backup',
 | 
			
		||||
    restoreDb: 'db:restore',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
 | 
			
		||||
const searchItems = [
 | 
			
		||||
    getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
 | 
			
		||||
    SearchItem.slot('instanceId', '实例', 'instanceSelect'),
 | 
			
		||||
    SearchItem.input('code', '编号'),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
 | 
			
		||||
    TableColumn.new('name', '名称'),
 | 
			
		||||
    TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
 | 
			
		||||
    TableColumn.new('instanceName', '实例名'),
 | 
			
		||||
    TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
 | 
			
		||||
    TableColumn.new('username', 'username'),
 | 
			
		||||
    TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
 | 
			
		||||
    TableColumn.new('authCertName', '授权凭证'),
 | 
			
		||||
    TableColumn.new('database', '库').isSlot().setMinWidth(80),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('code', '编号'),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    backupDb: 'db:backup',
 | 
			
		||||
    restoreDb: 'db:restore',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
 | 
			
		||||
const actionBtns = hasPerms(Object.values(perms));
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
@@ -244,6 +258,8 @@ const state = reactive({
 | 
			
		||||
    row: {} as any,
 | 
			
		||||
    dbId: 0,
 | 
			
		||||
    db: '',
 | 
			
		||||
    currentDbs: '',
 | 
			
		||||
    dbNameSearch: '',
 | 
			
		||||
    instances: [] as any,
 | 
			
		||||
    /**
 | 
			
		||||
     * 选中的数据
 | 
			
		||||
@@ -319,13 +335,26 @@ const state = reactive({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
 | 
			
		||||
    toRefs(state);
 | 
			
		||||
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (Object.keys(actionBtns).length > 0) {
 | 
			
		||||
        columns.value.push(actionColumn);
 | 
			
		||||
    }
 | 
			
		||||
    search();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterDbs = computed(() => {
 | 
			
		||||
    const dbsStr = state.currentDbs;
 | 
			
		||||
    if (!dbsStr) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    const dbs = dbsStr.split(' ').map((db: any) => {
 | 
			
		||||
        return { dbName: db };
 | 
			
		||||
    });
 | 
			
		||||
    return dbs.filter((db: any) => {
 | 
			
		||||
        return db.dbName.includes(state.dbNameSearch);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const checkRouteTagPath = (query: any) => {
 | 
			
		||||
@@ -335,7 +364,10 @@ const checkRouteTagPath = (query: any) => {
 | 
			
		||||
    return query;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
const search = async (tagPath: string = '') => {
 | 
			
		||||
    if (tagPath) {
 | 
			
		||||
        state.query.tagPath = tagPath;
 | 
			
		||||
    }
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -372,10 +404,6 @@ const handleMoreActionCommand = (commond: any) => {
 | 
			
		||||
            showInfo(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'edit': {
 | 
			
		||||
            editDb(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dumpDb': {
 | 
			
		||||
            onDumpDbs(data);
 | 
			
		||||
            return;
 | 
			
		||||
@@ -395,32 +423,6 @@ const handleMoreActionCommand = (commond: any) => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editDb = async (data: any) => {
 | 
			
		||||
    if (!data) {
 | 
			
		||||
        state.dbEditDialog.data = null;
 | 
			
		||||
        state.dbEditDialog.title = '新增数据库资源';
 | 
			
		||||
    } else {
 | 
			
		||||
        state.dbEditDialog.data = data;
 | 
			
		||||
        state.dbEditDialog.title = '修改数据库资源';
 | 
			
		||||
    }
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDb = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        search();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onShowSqlExec = async (row: any) => {
 | 
			
		||||
    state.sqlExecLogDialog.title = `${row.name}`;
 | 
			
		||||
    state.sqlExecLogDialog.dbId = row.id;
 | 
			
		||||
@@ -475,9 +477,8 @@ const onDumpDbs = async (row: any) => {
 | 
			
		||||
/**
 | 
			
		||||
 * 数据库信息导出
 | 
			
		||||
 */
 | 
			
		||||
const dumpDbs = () => {
 | 
			
		||||
const dumpDbs = async () => {
 | 
			
		||||
    isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
 | 
			
		||||
    const a = document.createElement('a');
 | 
			
		||||
    let type = 0;
 | 
			
		||||
    for (let c of state.exportDialog.contents) {
 | 
			
		||||
        if (c == '结构') {
 | 
			
		||||
@@ -486,13 +487,15 @@ const dumpDbs = () => {
 | 
			
		||||
            type += 2;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    a.setAttribute(
 | 
			
		||||
        'href',
 | 
			
		||||
        `${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
 | 
			
		||||
            state.exportDialog.extName
 | 
			
		||||
        }&${joinClientParams()}`
 | 
			
		||||
    );
 | 
			
		||||
    a.click();
 | 
			
		||||
    for (let db of state.exportDialog.value) {
 | 
			
		||||
        const a = document.createElement('a');
 | 
			
		||||
        a.setAttribute(
 | 
			
		||||
            'href',
 | 
			
		||||
            `${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${db}&type=${type}&extName=${state.exportDialog.extName}&${joinClientParams()}`
 | 
			
		||||
        );
 | 
			
		||||
        a.click();
 | 
			
		||||
        await sleep(500);
 | 
			
		||||
    }
 | 
			
		||||
    state.exportDialog.visible = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -505,6 +508,8 @@ const supportAction = (action: string, dbType: string): boolean => {
 | 
			
		||||
    }
 | 
			
		||||
    return actions.includes(action);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ search });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.db-list {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,10 @@
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-link
 | 
			
		||||
                    v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
 | 
			
		||||
                    v-if="
 | 
			
		||||
                        data.status == DbSqlExecStatusEnum.Success.value &&
 | 
			
		||||
                        (data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value)
 | 
			
		||||
                    "
 | 
			
		||||
                    type="primary"
 | 
			
		||||
                    plain
 | 
			
		||||
                    size="small"
 | 
			
		||||
@@ -38,7 +41,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { DbSqlExecTypeEnum } from './enums';
 | 
			
		||||
import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
@@ -66,9 +69,11 @@ const columns = ref([
 | 
			
		||||
    TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
 | 
			
		||||
    TableColumn.new('creator', '执行人'),
 | 
			
		||||
    TableColumn.new('sql', 'SQL').canBeautify(),
 | 
			
		||||
    TableColumn.new('oldValue', '原值').canBeautify(),
 | 
			
		||||
    TableColumn.new('createTime', '执行时间').isTime(),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('status', '执行状态').typeTag(DbSqlExecStatusEnum),
 | 
			
		||||
    TableColumn.new('res', '执行结果'),
 | 
			
		||||
    TableColumn.new('createTime', '执行时间').isTime(),
 | 
			
		||||
    TableColumn.new('oldValue', '原值').canBeautify(),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
@@ -80,6 +85,7 @@ const state = reactive({
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        db: '',
 | 
			
		||||
        table: '',
 | 
			
		||||
        status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
 | 
			
		||||
        type: null,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 10,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										331
									
								
								mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,331 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="db-transfer-edit">
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            :title="title"
 | 
			
		||||
            v-model="dialogVisible"
 | 
			
		||||
            :before-close="cancel"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            :close-on-press-escape="false"
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            width="850px"
 | 
			
		||||
        >
 | 
			
		||||
            <el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
 | 
			
		||||
                <el-tabs v-model="tabActiveName">
 | 
			
		||||
                    <el-tab-pane label="基本信息" :name="basicTab">
 | 
			
		||||
                        <el-form-item prop="srcDbId" label="源数据库" required>
 | 
			
		||||
                            <db-select-tree
 | 
			
		||||
                                placeholder="请选择源数据库"
 | 
			
		||||
                                v-model:db-id="form.srcDbId"
 | 
			
		||||
                                v-model:inst-name="form.srcInstName"
 | 
			
		||||
                                v-model:db-name="form.srcDbName"
 | 
			
		||||
                                v-model:tag-path="form.srcTagPath"
 | 
			
		||||
                                v-model:db-type="form.srcDbType"
 | 
			
		||||
                                @select-db="onSelectSrcDb"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="targetDbId" label="目标数据库" required>
 | 
			
		||||
                            <db-select-tree
 | 
			
		||||
                                placeholder="请选择目标数据库"
 | 
			
		||||
                                v-model:db-id="form.targetDbId"
 | 
			
		||||
                                v-model:inst-name="form.targetInstName"
 | 
			
		||||
                                v-model:db-name="form.targetDbName"
 | 
			
		||||
                                v-model:tag-path="form.targetTagPath"
 | 
			
		||||
                                v-model:db-type="form.targetDbType"
 | 
			
		||||
                                @select-db="onSelectTargetDb"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="strategy" label="迁移策略" required>
 | 
			
		||||
                            <el-select v-model="form.strategy" filterable placeholder="迁移策略">
 | 
			
		||||
                                <el-option label="全量" :value="1" />
 | 
			
		||||
                                <el-option label="增量(暂不可用)" disabled :value="2" />
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="nameCase" label="转换表、字段名" required>
 | 
			
		||||
                            <el-select v-model="form.nameCase">
 | 
			
		||||
                                <el-option label="无" :value="1" />
 | 
			
		||||
                                <el-option label="大写" :value="2" />
 | 
			
		||||
                                <el-option label="小写" :value="3" />
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="deleteTable" label="创建前删除表" required>
 | 
			
		||||
                            <el-select v-model="form.deleteTable">
 | 
			
		||||
                                <el-option label="是" :value="1" />
 | 
			
		||||
                                <el-option label="否" :value="2" />
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                    </el-tab-pane>
 | 
			
		||||
                    <el-tab-pane label="数据库对象" :name="tableTab" :disabled="!baseFieldCompleted">
 | 
			
		||||
                        <el-form-item>
 | 
			
		||||
                            <el-input v-model="state.filterSrcTableText" style="width: 240px" placeholder="过滤表" />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item>
 | 
			
		||||
                            <el-tree
 | 
			
		||||
                                ref="srcTreeRef"
 | 
			
		||||
                                style="width: 760px; max-height: 400px; overflow-y: auto"
 | 
			
		||||
                                default-expand-all
 | 
			
		||||
                                :expand-on-click-node="false"
 | 
			
		||||
                                :data="state.srcTableTree"
 | 
			
		||||
                                node-key="id"
 | 
			
		||||
                                show-checkbox
 | 
			
		||||
                                @check-change="handleSrcTableCheckChange"
 | 
			
		||||
                                :filter-node-method="filterSrcTableTreeNode"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                    </el-tab-pane>
 | 
			
		||||
                </el-tabs>
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <el-button @click="cancel()">取 消</el-button>
 | 
			
		||||
                    <el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    data: {
 | 
			
		||||
        type: [Boolean, Object],
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
 | 
			
		||||
 | 
			
		||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
 | 
			
		||||
 | 
			
		||||
const rules = {};
 | 
			
		||||
 | 
			
		||||
const dbForm: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const basicTab = 'basic';
 | 
			
		||||
const tableTab = 'table';
 | 
			
		||||
 | 
			
		||||
type FormData = {
 | 
			
		||||
    id?: number;
 | 
			
		||||
    srcDbId?: number;
 | 
			
		||||
    srcDbName?: string;
 | 
			
		||||
    srcDbType?: string;
 | 
			
		||||
    srcInstName?: string;
 | 
			
		||||
    srcTagPath?: string;
 | 
			
		||||
    srcTableNames?: string;
 | 
			
		||||
    targetDbId?: number;
 | 
			
		||||
    targetInstName?: string;
 | 
			
		||||
    targetDbName?: string;
 | 
			
		||||
    targetTagPath?: string;
 | 
			
		||||
    targetDbType?: string;
 | 
			
		||||
    strategy: 1 | 2;
 | 
			
		||||
    nameCase: 1 | 2 | 3;
 | 
			
		||||
    deleteTable?: 1 | 2;
 | 
			
		||||
    checkedKeys: string;
 | 
			
		||||
    runningState: 1 | 2;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const basicFormData = {
 | 
			
		||||
    strategy: 1,
 | 
			
		||||
    nameCase: 1,
 | 
			
		||||
    deleteTable: 1,
 | 
			
		||||
    checkedKeys: '',
 | 
			
		||||
    runningState: 1,
 | 
			
		||||
} as FormData;
 | 
			
		||||
 | 
			
		||||
const srcTableList = ref<{ tableName: string; tableComment: string }[]>([]);
 | 
			
		||||
const srcTableListDisabled = ref(false);
 | 
			
		||||
 | 
			
		||||
const defaultKeys = ['tab-check', 'all', 'table-list'];
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tabActiveName: 'basic',
 | 
			
		||||
    form: basicFormData,
 | 
			
		||||
    submitForm: {} as any,
 | 
			
		||||
    srcTableFields: [] as string[],
 | 
			
		||||
    targetColumnList: [] as any[],
 | 
			
		||||
    filterSrcTableText: '',
 | 
			
		||||
    srcTableTree: [
 | 
			
		||||
        {
 | 
			
		||||
            id: 'tab-check',
 | 
			
		||||
            label: '表',
 | 
			
		||||
            children: [
 | 
			
		||||
                { id: 'all', label: '全部表(*)' },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'table-list',
 | 
			
		||||
                    label: '自定义',
 | 
			
		||||
                    disabled: srcTableListDisabled,
 | 
			
		||||
                    children: [] as any[],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tabActiveName, form, submitForm } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
 | 
			
		||||
 | 
			
		||||
// 基础字段信息是否填写完整
 | 
			
		||||
const baseFieldCompleted = computed(() => {
 | 
			
		||||
    return state.form.srcDbId && state.form.targetDbId && state.form.targetDbName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(dialogVisible, async (newValue: boolean) => {
 | 
			
		||||
    if (!newValue) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    state.tabActiveName = 'basic';
 | 
			
		||||
    const propsData = props.data as any;
 | 
			
		||||
    if (!propsData?.id) {
 | 
			
		||||
        let d = {} as FormData;
 | 
			
		||||
        Object.assign(d, basicFormData);
 | 
			
		||||
        state.form = d;
 | 
			
		||||
        await nextTick(() => {
 | 
			
		||||
            srcTreeRef.value.setCheckedKeys([]);
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.form = props.data as FormData;
 | 
			
		||||
    let { srcDbId, targetDbId } = state.form;
 | 
			
		||||
 | 
			
		||||
    //  初始化src数据源
 | 
			
		||||
    if (srcDbId) {
 | 
			
		||||
        // 通过tagPath查询实例列表
 | 
			
		||||
        const dbInfoRes = await dbApi.dbs.request({ id: srcDbId });
 | 
			
		||||
        const db = dbInfoRes.list[0];
 | 
			
		||||
        // 初始化实例
 | 
			
		||||
        db.databases = db.database?.split(' ').sort() || [];
 | 
			
		||||
 | 
			
		||||
        if (srcDbId && state.form.srcDbName) {
 | 
			
		||||
            await loadDbTables(srcDbId, state.form.srcDbName);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //  初始化target数据源
 | 
			
		||||
    if (targetDbId) {
 | 
			
		||||
        // 通过tagPath查询实例列表
 | 
			
		||||
        const dbInfoRes = await dbApi.dbs.request({ id: targetDbId });
 | 
			
		||||
        const db = dbInfoRes.list[0];
 | 
			
		||||
        // 初始化实例
 | 
			
		||||
        db.databases = db.database?.split(' ').sort() || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 初始化勾选迁移表
 | 
			
		||||
    srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => state.filterSrcTableText,
 | 
			
		||||
    (val) => {
 | 
			
		||||
        srcTreeRef.value!.filter(val);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const onSelectSrcDb = async (params: any) => {
 | 
			
		||||
    //  初始化数据源
 | 
			
		||||
    params.databases = params.dbs; // 数据源里需要这个值
 | 
			
		||||
    await loadDbTables(params.id, params.db);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onSelectTargetDb = async (params: any) => {
 | 
			
		||||
    console.log(params);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const loadDbTables = async (dbId: number, db: string) => {
 | 
			
		||||
    // 加载db下的表
 | 
			
		||||
    srcTableList.value = await dbApi.tableInfos.request({ id: dbId, db });
 | 
			
		||||
    handleLoadSrcTableTree();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleSrcTableCheckChange = (data: { id: string; name: string }, checked: boolean) => {
 | 
			
		||||
    if (data.id === 'all') {
 | 
			
		||||
        srcTableListDisabled.value = checked;
 | 
			
		||||
        if (checked) {
 | 
			
		||||
            state.form.checkedKeys = 'all';
 | 
			
		||||
        } else {
 | 
			
		||||
            state.form.checkedKeys = '';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (data.id && (data.id + '').startsWith('list-item')) {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterSrcTableTreeNode = (value: string, data: any) => {
 | 
			
		||||
    if (!value) return true;
 | 
			
		||||
    return data.label.includes(value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleLoadSrcTableTree = () => {
 | 
			
		||||
    state.srcTableTree[0].children[1].children = srcTableList.value.map((item) => {
 | 
			
		||||
        return {
 | 
			
		||||
            id: item.tableName,
 | 
			
		||||
            label: item.tableName + (item.tableComment && '-' + item.tableComment),
 | 
			
		||||
            disabled: srcTableListDisabled,
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getReqForm = async () => {
 | 
			
		||||
    return { ...state.form };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const srcTreeRef = ref();
 | 
			
		||||
 | 
			
		||||
const getCheckedKeys = () => {
 | 
			
		||||
    let checks = srcTreeRef.value!.getCheckedKeys(false);
 | 
			
		||||
    if (checks.indexOf('all') >= 0) {
 | 
			
		||||
        return ['all'];
 | 
			
		||||
    }
 | 
			
		||||
    return checks.filter((item: any) => !defaultKeys.includes(item));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const btnOk = async () => {
 | 
			
		||||
    dbForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            ElMessage.error('请正确填写信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.submitForm = await getReqForm();
 | 
			
		||||
 | 
			
		||||
        let checkedKeys = getCheckedKeys();
 | 
			
		||||
        if (checkedKeys.length > 0) {
 | 
			
		||||
            state.submitForm.checkedKeys = checkedKeys.join(',');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!state.submitForm.checkedKeys) {
 | 
			
		||||
            ElMessage.error('请选择需要迁移的表');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await saveExec();
 | 
			
		||||
        ElMessage.success('保存成功');
 | 
			
		||||
        emit('val-change', state.form);
 | 
			
		||||
        cancel();
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.db-transfer-edit {
 | 
			
		||||
    .el-select {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										196
									
								
								mayfly_go_web/src/views/ops/db/DbTransferList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								mayfly_go_web/src/views/ops/db/DbTransferList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="db-list">
 | 
			
		||||
        <page-table
 | 
			
		||||
            ref="pageTableRef"
 | 
			
		||||
            :page-api="dbApi.dbTransferTasks"
 | 
			
		||||
            :searchItems="searchItems"
 | 
			
		||||
            v-model:query-form="query"
 | 
			
		||||
            :show-selection="true"
 | 
			
		||||
            v-model:selection-data="state.selectionData"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
        >
 | 
			
		||||
            <template #tableHeader>
 | 
			
		||||
                <el-button v-auth="perms.save" type="primary" icon="plus" @click="edit(false)">添加</el-button>
 | 
			
		||||
                <el-button v-auth="perms.del" :disabled="selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #srcDb="{ data }">
 | 
			
		||||
                <el-tooltip :content="`${data.srcTagPath} > ${data.srcInstName} > ${data.srcDbName}`">
 | 
			
		||||
                    <span>
 | 
			
		||||
                        <SvgIcon :name="getDbDialect(data.srcDbType).getInfo().icon" :size="18" />
 | 
			
		||||
                        {{ data.srcInstName }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template #targetDb="{ data }">
 | 
			
		||||
                <el-tooltip :content="`${data.targetTagPath} > ${data.targetInstName} > ${data.targetDbName}`">
 | 
			
		||||
                    <span>
 | 
			
		||||
                        <SvgIcon :name="getDbDialect(data.targetDbType).getInfo().icon" :size="18" />
 | 
			
		||||
                        {{ data.targetInstName }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <!-- 删除、启停用、编辑 -->
 | 
			
		||||
                <el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
 | 
			
		||||
                <el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
        <db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
 | 
			
		||||
 | 
			
		||||
        <TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import { DbTransferRunningStateEnum } from './enums';
 | 
			
		||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
 | 
			
		||||
 | 
			
		||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    save: 'db:transfer:save',
 | 
			
		||||
    del: 'db:transfer:del',
 | 
			
		||||
    status: 'db:transfer:status',
 | 
			
		||||
    log: 'db:transfer:log',
 | 
			
		||||
    run: 'db:transfer:run',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const searchItems = [SearchItem.input('name', '名称')];
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('srcDb', '源库').setMinWidth(200).isSlot(),
 | 
			
		||||
    TableColumn.new('targetDb', '目标库').setMinWidth(200).isSlot(),
 | 
			
		||||
    TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
 | 
			
		||||
    TableColumn.new('creator', '创建人'),
 | 
			
		||||
    TableColumn.new('createTime', '创建时间').isTime(),
 | 
			
		||||
    TableColumn.new('modifier', '修改人'),
 | 
			
		||||
    TableColumn.new('updateTime', '修改时间').isTime(),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run]);
 | 
			
		||||
const actionWidth = ((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0)) * 55;
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    row: {},
 | 
			
		||||
    dbId: 0,
 | 
			
		||||
    db: '',
 | 
			
		||||
    /**
 | 
			
		||||
     * 选中的数据
 | 
			
		||||
     */
 | 
			
		||||
    selectionData: [],
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询条件
 | 
			
		||||
     */
 | 
			
		||||
    query: {
 | 
			
		||||
        name: null,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 0,
 | 
			
		||||
    },
 | 
			
		||||
    editDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        title: '新增数据数据迁移任务',
 | 
			
		||||
    },
 | 
			
		||||
    logsDialog: {
 | 
			
		||||
        logId: 0,
 | 
			
		||||
        title: '数据库迁移日志',
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        running: false,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (Object.keys(actionBtns).length > 0) {
 | 
			
		||||
        columns.value.push(actionColumn);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const search = () => {
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const edit = async (data: any) => {
 | 
			
		||||
    if (!data) {
 | 
			
		||||
        state.editDialog.data = null;
 | 
			
		||||
        state.editDialog.title = '新增数据库迁移任务';
 | 
			
		||||
    } else {
 | 
			
		||||
        state.editDialog.data = data;
 | 
			
		||||
        state.editDialog.title = '修改数据库迁移任务';
 | 
			
		||||
    }
 | 
			
		||||
    state.editDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const stop = async (id: any) => {
 | 
			
		||||
    await ElMessageBox.confirm(`确定停止?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    await dbApi.stopDbTransferTask.request({ taskId: id });
 | 
			
		||||
    ElMessage.success(`停止成功`);
 | 
			
		||||
    search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const log = (data: any) => {
 | 
			
		||||
    state.logsDialog.logId = data.logId;
 | 
			
		||||
    state.logsDialog.visible = true;
 | 
			
		||||
    state.logsDialog.title = '数据库迁移日志';
 | 
			
		||||
    state.logsDialog.running = data.state === 1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const reRun = async (data: any) => {
 | 
			
		||||
    await ElMessageBox.confirm(`确定运行?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    try {
 | 
			
		||||
        let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
 | 
			
		||||
        console.log(res);
 | 
			
		||||
        ElMessage.success('运行成功');
 | 
			
		||||
        // 拿到日志id之后,弹出日志弹窗
 | 
			
		||||
        log({ logId: res, state: 1 });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
    // 延迟2秒执行,后端异步执行
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        search();
 | 
			
		||||
    }, 2000);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const del = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        await ElMessageBox.confirm(`确定删除任务?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        search();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
							
								
								
									
										170
									
								
								mayfly_go_web/src/views/ops/db/InstanceDbConf.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								mayfly_go_web/src/views/ops/db/InstanceDbConf.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
 | 
			
		||||
            <template #header>
 | 
			
		||||
                <DrawerHeader :header="title" :back="cancel" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <el-table :data="state.dbs" stripe>
 | 
			
		||||
                <el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
 | 
			
		||||
                <el-table-column prop="database" label="库" min-width="80">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-popover placement="bottom" :width="200" trigger="click">
 | 
			
		||||
                            <template #reference>
 | 
			
		||||
                                <el-button @click="state.currentDbs = scope.row.database" type="primary" link>查看库</el-button>
 | 
			
		||||
                            </template>
 | 
			
		||||
                            <el-table :data="filterDbs" size="small">
 | 
			
		||||
                                <el-table-column prop="dbName" label="数据库">
 | 
			
		||||
                                    <template #header>
 | 
			
		||||
                                        <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                </el-table-column>
 | 
			
		||||
                            </el-table>
 | 
			
		||||
                        </el-popover>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
 | 
			
		||||
                <el-table-column min-wdith="120px">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        操作
 | 
			
		||||
                        <el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
 | 
			
		||||
                        <el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
            </el-table>
 | 
			
		||||
 | 
			
		||||
            <db-edit
 | 
			
		||||
                @confirm="confirmEditDb"
 | 
			
		||||
                @cancel="cancelEditDb"
 | 
			
		||||
                :title="dbEditDialog.title"
 | 
			
		||||
                v-model:visible="dbEditDialog.visible"
 | 
			
		||||
                :instance="props.instance"
 | 
			
		||||
                v-model:db="dbEditDialog.data"
 | 
			
		||||
            ></db-edit>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, reactive, toRefs, watchEffect } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
			
		||||
import DbEdit from './DbEdit.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
    instance: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    base: 'db',
 | 
			
		||||
    saveDb: 'db:save',
 | 
			
		||||
    delDb: 'db:del',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    dbs: [] as any,
 | 
			
		||||
    currentDbs: '', // 当前数据库名,空格分割库名
 | 
			
		||||
    dbNameSearch: '',
 | 
			
		||||
    dbEditDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        title: '新增数据库',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, dbEditDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dialogVisible = props.visible;
 | 
			
		||||
    if (!state.dialogVisible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDbs();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterDbs = computed(() => {
 | 
			
		||||
    const dbsStr = state.currentDbs;
 | 
			
		||||
    if (!dbsStr) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    const dbs = dbsStr.split(' ').map((db: any) => {
 | 
			
		||||
        return { dbName: db };
 | 
			
		||||
    });
 | 
			
		||||
    return dbs.filter((db: any) => {
 | 
			
		||||
        return db.dbName.includes(state.dbNameSearch);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDbs = () => {
 | 
			
		||||
    dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
 | 
			
		||||
        state.dbs = res.list || [];
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editDb = (data: any) => {
 | 
			
		||||
    if (data) {
 | 
			
		||||
        state.dbEditDialog.data = { ...data };
 | 
			
		||||
    } else {
 | 
			
		||||
        state.dbEditDialog.data = {
 | 
			
		||||
            instanceId: props.instance.id,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDb = async (db: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        await dbApi.deleteDb.request({ id: db.id });
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        getDbs();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditDb = async (db: any) => {
 | 
			
		||||
    db.instanceId = props.instance.id;
 | 
			
		||||
    await dbApi.saveDb.request(db);
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    getDbs();
 | 
			
		||||
    cancelEditDb();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelEditDb = () => {
 | 
			
		||||
    state.dbEditDialog.visible = false;
 | 
			
		||||
    state.dbEditDialog.data = {};
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -1,111 +1,142 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
 | 
			
		||||
        <el-drawer :title="title" v-model="dialogVisible" :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="dbForm" :rules="rules" label-width="auto">
 | 
			
		||||
                <el-tabs v-model="tabActiveName">
 | 
			
		||||
                    <el-tab-pane label="基础信息" name="basic">
 | 
			
		||||
                        <el-form-item prop="name" label="别名" required>
 | 
			
		||||
                            <el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="type" label="类型" required>
 | 
			
		||||
                            <el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
 | 
			
		||||
                                <el-option
 | 
			
		||||
                                    v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
 | 
			
		||||
                                    :key="key"
 | 
			
		||||
                                    :value="dbTypeAndDialect[0]"
 | 
			
		||||
                                    :label="dbTypeAndDialect[1].getInfo().name"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
 | 
			
		||||
                                    {{ dbTypeAndDialect[1].getInfo().name }}
 | 
			
		||||
                                </el-option>
 | 
			
		||||
                <el-divider content-position="left">基本</el-divider>
 | 
			
		||||
 | 
			
		||||
                                <template #prefix>
 | 
			
		||||
                                    <SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
 | 
			
		||||
                            <el-col :span="18">
 | 
			
		||||
                                <el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
			
		||||
                            </el-col>
 | 
			
		||||
                            <el-col style="text-align: center" :span="1">:</el-col>
 | 
			
		||||
                            <el-col :span="5">
 | 
			
		||||
                                <el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
 | 
			
		||||
                            </el-col>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                <el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签">
 | 
			
		||||
                    <tag-tree-select
 | 
			
		||||
                        multiple
 | 
			
		||||
                        @change-tag="
 | 
			
		||||
                            (paths: any) => {
 | 
			
		||||
                                form.tagCodePaths = paths;
 | 
			
		||||
                                tagSelectRef.validate();
 | 
			
		||||
                            }
 | 
			
		||||
                        "
 | 
			
		||||
                        :select-tags="form.tagCodePaths"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                    />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
 | 
			
		||||
                            <el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                <el-form-item prop="code" label="编号" required>
 | 
			
		||||
                    <el-input
 | 
			
		||||
                        :disabled="form.id"
 | 
			
		||||
                        v-model.trim="form.code"
 | 
			
		||||
                        placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
 | 
			
		||||
                        auto-complete="off"
 | 
			
		||||
                    ></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
 | 
			
		||||
                            <el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
 | 
			
		||||
                            <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
 | 
			
		||||
                            <el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
 | 
			
		||||
                                <template v-if="form.id && form.id != 0" #suffix>
 | 
			
		||||
                                    <el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <el-link v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5"
 | 
			
		||||
                                                >原密码
 | 
			
		||||
                                            </el-link>
 | 
			
		||||
                                        </template>
 | 
			
		||||
                                    </el-popover>
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                <el-form-item prop="name" label="名称" required>
 | 
			
		||||
                    <el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="remark" label="备注">
 | 
			
		||||
                            <el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                    </el-tab-pane>
 | 
			
		||||
                <el-form-item prop="type" label="类型" required>
 | 
			
		||||
                    <el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
 | 
			
		||||
                        <el-option
 | 
			
		||||
                            v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
 | 
			
		||||
                            :key="key"
 | 
			
		||||
                            :value="dbTypeAndDialect[0]"
 | 
			
		||||
                            :label="dbTypeAndDialect[1].getInfo().name"
 | 
			
		||||
                        >
 | 
			
		||||
                            <SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
 | 
			
		||||
                            {{ dbTypeAndDialect[1].getInfo().name }}
 | 
			
		||||
                        </el-option>
 | 
			
		||||
 | 
			
		||||
                    <el-tab-pane label="其他配置" name="other">
 | 
			
		||||
                        <el-form-item prop="params" label="连接参数">
 | 
			
		||||
                            <el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
 | 
			
		||||
                                <!-- <template #suffix>
 | 
			
		||||
                                    <el-link
 | 
			
		||||
                                        target="_blank"
 | 
			
		||||
                                        href="https://github.com/go-sql-driver/mysql#parameters"
 | 
			
		||||
                                        :underline="false"
 | 
			
		||||
                                        type="primary"
 | 
			
		||||
                                        class="mr5"
 | 
			
		||||
                                        >参数参考</el-link
 | 
			
		||||
                                    >
 | 
			
		||||
                                </template> -->
 | 
			
		||||
                            </el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <template #prefix>
 | 
			
		||||
                            <SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="sshTunnelMachineId" label="SSH隧道">
 | 
			
		||||
                            <ssh-tunnel-select v-model="form.sshTunnelMachineId" />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                    </el-tab-pane>
 | 
			
		||||
                </el-tabs>
 | 
			
		||||
                <el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
 | 
			
		||||
                    <el-col :span="18">
 | 
			
		||||
                        <el-input v-model.trim="form.host" placeholder="请输入ip" auto-complete="off"></el-input>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                    <el-col style="text-align: center" :span="1">:</el-col>
 | 
			
		||||
                    <el-col :span="5">
 | 
			
		||||
                        <el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
 | 
			
		||||
                    <el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item v-if="form.type === DbType.oracle" label="SID|服务名">
 | 
			
		||||
                    <el-col :span="5">
 | 
			
		||||
                        <el-select
 | 
			
		||||
                            @change="
 | 
			
		||||
                                () => {
 | 
			
		||||
                                    state.extra.serviceName = '';
 | 
			
		||||
                                    state.extra.sid = '';
 | 
			
		||||
                                }
 | 
			
		||||
                            "
 | 
			
		||||
                            v-model="state.extra.stype"
 | 
			
		||||
                            placeholder="请选择"
 | 
			
		||||
                        >
 | 
			
		||||
                            <el-option label="服务名" :value="1" />
 | 
			
		||||
                            <el-option label="SID" :value="2" />
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                    <el-col style="text-align: center" :span="1">:</el-col>
 | 
			
		||||
                    <el-col :span="18">
 | 
			
		||||
                        <el-input v-if="state.extra.stype == 1" v-model="state.extra.serviceName" placeholder="请输入服务名"> </el-input>
 | 
			
		||||
                        <el-input v-else v-model="state.extra.sid" placeholder="请输入SID"> </el-input>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="remark" label="备注">
 | 
			
		||||
                    <el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-divider content-position="left">账号</el-divider>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <ResourceAuthCertTableEdit
 | 
			
		||||
                        v-model="form.authCerts"
 | 
			
		||||
                        :resource-code="form.code"
 | 
			
		||||
                        :resource-type="TagResourceTypeEnum.Db.value"
 | 
			
		||||
                        :test-conn-btn-loading="testConnBtnLoading"
 | 
			
		||||
                        @test-conn="testConn"
 | 
			
		||||
                        :disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <el-divider content-position="left">其他</el-divider>
 | 
			
		||||
                <el-form-item prop="params" label="连接参数">
 | 
			
		||||
                    <el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"> </el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="sshTunnelMachineId" label="SSH隧道">
 | 
			
		||||
                    <ssh-tunnel-select v-model="form.sshTunnelMachineId" />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
 | 
			
		||||
                    <el-button @click="cancel()">取 消</el-button>
 | 
			
		||||
                    <el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-button @click="cancel()">取 消</el-button>
 | 
			
		||||
                <el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { reactive, ref, toRefs, watchEffect } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { notBlank } from '@/common/assert';
 | 
			
		||||
import { RsaEncrypt } from '@/common/rsa';
 | 
			
		||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
 | 
			
		||||
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
			
		||||
import { ResourceCodePattern } from '@/common/pattern';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
 | 
			
		||||
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -123,6 +154,25 @@ const props = defineProps({
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
    tagCodePaths: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请选择标签',
 | 
			
		||||
            trigger: ['change'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    code: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请输入编码',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pattern: ResourceCodePattern.pattern,
 | 
			
		||||
            message: ResourceCodePattern.message,
 | 
			
		||||
            trigger: ['blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    name: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
@@ -144,13 +194,6 @@ const rules = {
 | 
			
		||||
            trigger: ['blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    username: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请输入用户名',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    sid: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
@@ -161,108 +204,109 @@ const rules = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const dbForm: any = ref(null);
 | 
			
		||||
const tagSelectRef: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const DefaultForm = {
 | 
			
		||||
    id: null,
 | 
			
		||||
    type: DbType.mysql,
 | 
			
		||||
    code: '',
 | 
			
		||||
    name: null,
 | 
			
		||||
    host: '',
 | 
			
		||||
    port: getDbDialect(DbType.mysql).getInfo().defaultPort,
 | 
			
		||||
    extra: '', // 连接需要的额外参数(json字符串)
 | 
			
		||||
    params: null,
 | 
			
		||||
    remark: '',
 | 
			
		||||
    sshTunnelMachineId: null as any,
 | 
			
		||||
    authCerts: [],
 | 
			
		||||
    tagCodePaths: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    tabActiveName: 'basic',
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        type: '',
 | 
			
		||||
        name: null,
 | 
			
		||||
        host: '',
 | 
			
		||||
        port: null,
 | 
			
		||||
        username: null,
 | 
			
		||||
        sid: null, // oracle类项目需要服务id
 | 
			
		||||
        password: null,
 | 
			
		||||
        params: null,
 | 
			
		||||
        remark: '',
 | 
			
		||||
        sshTunnelMachineId: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    submitForm: {},
 | 
			
		||||
    // 原密码
 | 
			
		||||
    pwd: '',
 | 
			
		||||
    // 原用户名
 | 
			
		||||
    oldUserName: null,
 | 
			
		||||
    extra: {} as any, // 连接需要的额外参数(json)
 | 
			
		||||
    form: DefaultForm,
 | 
			
		||||
    submitForm: {} as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
 | 
			
		||||
const { dialogVisible, form, submitForm } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec, data: saveInstanceRes } = dbApi.saveInstance.useApi(submitForm);
 | 
			
		||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
 | 
			
		||||
 | 
			
		||||
watch(props, (newValue: any) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dialogVisible = props.visible;
 | 
			
		||||
    if (!state.dialogVisible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    state.tabActiveName = 'basic';
 | 
			
		||||
    if (newValue.data) {
 | 
			
		||||
        state.form = { ...newValue.data };
 | 
			
		||||
        state.oldUserName = state.form.username;
 | 
			
		||||
    const dbInst: any = props.data;
 | 
			
		||||
    if (dbInst) {
 | 
			
		||||
        state.form = { ...dbInst };
 | 
			
		||||
        state.form.tagCodePaths = dbInst.tags.map((t: any) => t.codePath) || [];
 | 
			
		||||
        try {
 | 
			
		||||
            state.extra = JSON.parse(state.form.extra);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            state.extra = {};
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = { port: null, type: DbType.mysql } as any;
 | 
			
		||||
        state.oldUserName = null;
 | 
			
		||||
        state.form = { ...DefaultForm };
 | 
			
		||||
        state.form.authCerts = [];
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const changeDbType = (val: string) => {
 | 
			
		||||
    if (!state.form.id) {
 | 
			
		||||
        state.form.port = getDbDialect(val).getInfo().defaultPort as any;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDbPwd = async () => {
 | 
			
		||||
    state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getReqForm = async () => {
 | 
			
		||||
    const reqForm = { ...state.form };
 | 
			
		||||
    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
			
		||||
    const reqForm: any = { ...state.form };
 | 
			
		||||
    reqForm.selectAuthCert = null;
 | 
			
		||||
    reqForm.tags = null;
 | 
			
		||||
    if (!state.form.sshTunnelMachineId) {
 | 
			
		||||
        reqForm.sshTunnelMachineId = -1;
 | 
			
		||||
    }
 | 
			
		||||
    if (Object.keys(state.extra).length > 0) {
 | 
			
		||||
        reqForm.extra = JSON.stringify(state.extra);
 | 
			
		||||
    }
 | 
			
		||||
    return reqForm;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const testConn = async () => {
 | 
			
		||||
    dbForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            ElMessage.error('请正确填写信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
const testConn = async (authCert: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        await dbForm.value.validate();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
        ElMessage.error('请正确填写信息');
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        state.submitForm = await getReqForm();
 | 
			
		||||
        await testConnExec();
 | 
			
		||||
        ElMessage.success('连接成功');
 | 
			
		||||
    });
 | 
			
		||||
    state.submitForm = await getReqForm();
 | 
			
		||||
    state.submitForm.authCerts = [authCert];
 | 
			
		||||
    await testConnExec();
 | 
			
		||||
    ElMessage.success('连接成功');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const btnOk = async () => {
 | 
			
		||||
    if (state.form.type !== DbType.sqlite) {
 | 
			
		||||
        if (!state.form.id) {
 | 
			
		||||
            notBlank(state.form.password, '新增操作,密码不可为空');
 | 
			
		||||
        } else if (state.form.username != state.oldUserName) {
 | 
			
		||||
            notBlank(state.form.password, '已修改用户名,请输入密码');
 | 
			
		||||
        }
 | 
			
		||||
    try {
 | 
			
		||||
        await dbForm.value.validate();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
        ElMessage.error('请正确填写信息');
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            ElMessage.error('请正确填写信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.submitForm = await getReqForm();
 | 
			
		||||
        await saveInstanceExec();
 | 
			
		||||
        ElMessage.success('保存成功');
 | 
			
		||||
        emit('val-change', state.form);
 | 
			
		||||
        cancel();
 | 
			
		||||
    });
 | 
			
		||||
    state.submitForm = await getReqForm();
 | 
			
		||||
    await saveInstanceExec();
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    state.form.id = saveInstanceRes as any;
 | 
			
		||||
    emit('val-change', state.form);
 | 
			
		||||
    cancel();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.extra = {};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const changeDbType = (val: string) => {
 | 
			
		||||
    if (!state.form.id) {
 | 
			
		||||
        state.form.port = getDbDialect(val).getInfo().defaultPort as any;
 | 
			
		||||
    }
 | 
			
		||||
    state.extra = {};
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,13 @@
 | 
			
		||||
        <page-table
 | 
			
		||||
            ref="pageTableRef"
 | 
			
		||||
            :page-api="dbApi.instances"
 | 
			
		||||
            :data-handler-fn="handleData"
 | 
			
		||||
            :searchItems="searchItems"
 | 
			
		||||
            v-model:query-form="query"
 | 
			
		||||
            :show-selection="true"
 | 
			
		||||
            v-model:selection-data="state.selectionData"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
            lazy
 | 
			
		||||
        >
 | 
			
		||||
            <template #tableHeader>
 | 
			
		||||
                <el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">添加</el-button>
 | 
			
		||||
@@ -16,8 +18,16 @@
 | 
			
		||||
                >
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <ResourceTags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #authCert="{ data }">
 | 
			
		||||
                <ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #type="{ data }">
 | 
			
		||||
                <el-tooltip :content="data.type" placement="top">
 | 
			
		||||
                <el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
 | 
			
		||||
                    <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
            </template>
 | 
			
		||||
@@ -25,6 +35,7 @@
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button @click="showInfo(data)" link>详情</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +46,6 @@
 | 
			
		||||
                <el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="类型">{{ infoDialog.data.type }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
 | 
			
		||||
@@ -52,16 +62,18 @@
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <instance-edit
 | 
			
		||||
            @val-change="search"
 | 
			
		||||
            @val-change="search()"
 | 
			
		||||
            :title="instanceEditDialog.title"
 | 
			
		||||
            v-model:visible="instanceEditDialog.visible"
 | 
			
		||||
            v-model:data="instanceEditDialog.data"
 | 
			
		||||
        ></instance-edit>
 | 
			
		||||
 | 
			
		||||
        <instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, Ref } from 'vue';
 | 
			
		||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
@@ -71,28 +83,43 @@ import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { getDbDialect } from './dialect';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import { getTagPathSearchItem } from '../component/tag';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
 | 
			
		||||
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    lazy: {
 | 
			
		||||
        type: [Boolean],
 | 
			
		||||
        default: false,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    saveInstance: 'db:instance:save',
 | 
			
		||||
    delInstance: 'db:instance:del',
 | 
			
		||||
    saveDb: 'db:save',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const searchItems = [SearchItem.input('name', '名称')];
 | 
			
		||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
 | 
			
		||||
    TableColumn.new('name', '名称'),
 | 
			
		||||
    TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
 | 
			
		||||
    TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
 | 
			
		||||
    TableColumn.new('username', '用户名'),
 | 
			
		||||
    TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(10),
 | 
			
		||||
    TableColumn.new('params', '连接参数'),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('code', '编号'),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
const actionBtns = hasPerms([perms.saveInstance]);
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
 | 
			
		||||
const actionBtns = hasPerms(Object.values(perms));
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -108,6 +135,7 @@ const state = reactive({
 | 
			
		||||
     */
 | 
			
		||||
    query: {
 | 
			
		||||
        name: null,
 | 
			
		||||
        tagPath: '',
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 0,
 | 
			
		||||
    },
 | 
			
		||||
@@ -120,20 +148,40 @@ const state = reactive({
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        title: '新增数据库实例',
 | 
			
		||||
    },
 | 
			
		||||
    dbEditDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        instance: null as any,
 | 
			
		||||
        title: '新增数据库实例',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { selectionData, query, infoDialog, instanceEditDialog } = toRefs(state);
 | 
			
		||||
const { selectionData, query, infoDialog, instanceEditDialog, dbEditDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (Object.keys(actionBtns).length > 0) {
 | 
			
		||||
        columns.value.push(actionColumn);
 | 
			
		||||
    }
 | 
			
		||||
    if (!props.lazy) {
 | 
			
		||||
        search();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const search = () => {
 | 
			
		||||
const search = (tagPath: string = '') => {
 | 
			
		||||
    if (tagPath) {
 | 
			
		||||
        state.query.tagPath = tagPath;
 | 
			
		||||
    }
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleData = (res: any) => {
 | 
			
		||||
    const dataList = res.list;
 | 
			
		||||
    // 赋值授权凭证
 | 
			
		||||
    for (let x of dataList) {
 | 
			
		||||
        x.selectAuthCert = x.authCerts[0];
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showInfo = (info: any) => {
 | 
			
		||||
    state.infoDialog.data = info;
 | 
			
		||||
    state.infoDialog.visible = true;
 | 
			
		||||
@@ -164,5 +212,13 @@ const deleteInstance = async () => {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editDb = (data: any) => {
 | 
			
		||||
    state.dbEditDialog.instance = data;
 | 
			
		||||
    state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ search });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,12 @@
 | 
			
		||||
    <div class="db-sql-exec">
 | 
			
		||||
        <Splitpanes class="default-theme">
 | 
			
		||||
            <Pane size="20" max-size="30">
 | 
			
		||||
                <tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
 | 
			
		||||
                <tag-tree
 | 
			
		||||
                    :default-expanded-keys="state.defaultExpendKey"
 | 
			
		||||
                    :resource-type="TagResourceTypeEnum.DbName.value"
 | 
			
		||||
                    :tag-path-node-type="NodeTypeTagPath"
 | 
			
		||||
                    ref="tagTreeRef"
 | 
			
		||||
                >
 | 
			
		||||
                    <template #prefix="{ data }">
 | 
			
		||||
                        <span v-if="data.type.value == SqlExecNodeType.DbInst">
 | 
			
		||||
                            <el-popover
 | 
			
		||||
@@ -27,8 +32,8 @@
 | 
			
		||||
                                        <el-descriptions-item label="数据库版本">
 | 
			
		||||
                                            <span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
 | 
			
		||||
                                        </el-descriptions-item>
 | 
			
		||||
                                        <el-descriptions-item label="user">
 | 
			
		||||
                                            {{ data.params.username }}
 | 
			
		||||
                                        <el-descriptions-item label="授权凭证">
 | 
			
		||||
                                            {{ data.params.authCertName }}
 | 
			
		||||
                                        </el-descriptions-item>
 | 
			
		||||
                                        <el-descriptions-item label="备注">
 | 
			
		||||
                                            {{ data.params.remark }}
 | 
			
		||||
@@ -42,10 +47,8 @@
 | 
			
		||||
                    </template>
 | 
			
		||||
 | 
			
		||||
                    <template #suffix="{ data }">
 | 
			
		||||
                        <span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
 | 
			
		||||
                        <span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
 | 
			
		||||
                            ` ${data.params.dbTableSize}`
 | 
			
		||||
                        }}</span>
 | 
			
		||||
                        <span v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
 | 
			
		||||
                        <span v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{ ` ${data.params.dbTableSize}` }}</span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </tag-tree>
 | 
			
		||||
            </Pane>
 | 
			
		||||
@@ -71,7 +74,7 @@
 | 
			
		||||
                                <el-descriptions-item label-align="right">
 | 
			
		||||
                                    <template #label>
 | 
			
		||||
                                        <div>
 | 
			
		||||
                                            <SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
 | 
			
		||||
                                            <SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
 | 
			
		||||
                                            实例
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </template>
 | 
			
		||||
@@ -143,6 +146,7 @@
 | 
			
		||||
                                    :db-id="dt.params.id"
 | 
			
		||||
                                    :db="dt.params.db"
 | 
			
		||||
                                    :db-type="dt.params.type"
 | 
			
		||||
                                    :flow-procdef="dt.params.flowProcdef"
 | 
			
		||||
                                    :height="state.tablesOpHeight"
 | 
			
		||||
                                />
 | 
			
		||||
                            </el-tab-pane>
 | 
			
		||||
@@ -157,6 +161,7 @@
 | 
			
		||||
            :dbId="tableCreateDialog.dbId"
 | 
			
		||||
            :db="tableCreateDialog.db"
 | 
			
		||||
            :dbType="tableCreateDialog.dbType"
 | 
			
		||||
            :flow-procdef="tableCreateDialog.flowProcdef"
 | 
			
		||||
            :data="tableCreateDialog.data"
 | 
			
		||||
            v-model:visible="tableCreateDialog.visible"
 | 
			
		||||
            @submit-sql="onSubmitEditTableSql"
 | 
			
		||||
@@ -165,11 +170,11 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
 | 
			
		||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { formatByteSize } from '@/common/utils/format';
 | 
			
		||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
 | 
			
		||||
import { NodeType, TagTreeNode } from '../component/tag';
 | 
			
		||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
 | 
			
		||||
import TagTree from '../component/TagTree.vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
 | 
			
		||||
@@ -180,6 +185,10 @@ import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
import { useEventListener } from '@vueuse/core';
 | 
			
		||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
 | 
			
		||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
			
		||||
import { storeToRefs } from 'pinia';
 | 
			
		||||
import { procdefApi } from '@/views/flow/api';
 | 
			
		||||
 | 
			
		||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
 | 
			
		||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
 | 
			
		||||
@@ -225,7 +234,18 @@ const SqlIcon = {
 | 
			
		||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
 | 
			
		||||
    const params = nodeData.params;
 | 
			
		||||
    if (params.db) {
 | 
			
		||||
        changeDb({ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.dbs }, params.db);
 | 
			
		||||
        changeDb(
 | 
			
		||||
            {
 | 
			
		||||
                id: params.id,
 | 
			
		||||
                host: `${params.host}`,
 | 
			
		||||
                name: params.name,
 | 
			
		||||
                type: params.type,
 | 
			
		||||
                tagPath: params.tagPath,
 | 
			
		||||
                databases: params.dbs,
 | 
			
		||||
                flowProcdef: params.flowProcdef,
 | 
			
		||||
            },
 | 
			
		||||
            params.db
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -244,15 +264,17 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
 | 
			
		||||
        await sleep(100);
 | 
			
		||||
        return dbInfos?.map((x: any) => {
 | 
			
		||||
            x.tagPath = parentNode.key;
 | 
			
		||||
            return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
 | 
			
		||||
            return new TagTreeNode(`${x.code}`, x.name, NodeTypeDbInst).withParams(x);
 | 
			
		||||
        });
 | 
			
		||||
    })
 | 
			
		||||
    .withContextMenuItems([ContextmenuItemRefresh]);
 | 
			
		||||
 | 
			
		||||
// 数据库实例节点类型
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
    const params = parentNode.params;
 | 
			
		||||
    const dbs = params.database.split(' ')?.sort();
 | 
			
		||||
 | 
			
		||||
    const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
 | 
			
		||||
    return dbs.map((x: any) => {
 | 
			
		||||
        return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
 | 
			
		||||
            .withParams({
 | 
			
		||||
@@ -263,6 +285,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
 | 
			
		||||
                host: `${params.host}:${params.port}`,
 | 
			
		||||
                dbs: dbs,
 | 
			
		||||
                db: x,
 | 
			
		||||
                flowProcdef: flowProcdef,
 | 
			
		||||
            })
 | 
			
		||||
            .withIcon(DbIcon);
 | 
			
		||||
    });
 | 
			
		||||
@@ -287,11 +310,12 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
 | 
			
		||||
                return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return NodeTypeTables(params);
 | 
			
		||||
 | 
			
		||||
        return getNodeTypeTables(params);
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
 | 
			
		||||
const NodeTypeTables = (params: any) => {
 | 
			
		||||
const getNodeTypeTables = (params: any) => {
 | 
			
		||||
    let tableKey = `${params.id}.${params.db}.table-menu`;
 | 
			
		||||
    let sqlKey = getSqlMenuNodeKey(params.id, params.db);
 | 
			
		||||
    return [
 | 
			
		||||
@@ -306,7 +330,7 @@ const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const params = parentNode.params;
 | 
			
		||||
        params.parentKey = parentNode.key;
 | 
			
		||||
        return NodeTypeTables(params);
 | 
			
		||||
        return getNodeTypeTables(params);
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
 | 
			
		||||
@@ -322,7 +346,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
 | 
			
		||||
    ])
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const params = parentNode.params;
 | 
			
		||||
        let { id, db, type } = params;
 | 
			
		||||
        let { id, db, type, flowProcdef, schema } = params;
 | 
			
		||||
        // 获取当前库的所有表信息
 | 
			
		||||
        let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
 | 
			
		||||
        state.reloadStatus = false;
 | 
			
		||||
@@ -337,6 +361,8 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
 | 
			
		||||
                    id,
 | 
			
		||||
                    db,
 | 
			
		||||
                    type,
 | 
			
		||||
                    schema,
 | 
			
		||||
                    flowProcdef: flowProcdef,
 | 
			
		||||
                    key: key,
 | 
			
		||||
                    parentKey: parentNode.key,
 | 
			
		||||
                    tableName: x.tableName,
 | 
			
		||||
@@ -350,7 +376,10 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
 | 
			
		||||
        parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
 | 
			
		||||
        return tablesNode;
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
    .withNodeDblclickFunc((node: TagTreeNode) => {
 | 
			
		||||
        const params = node.params;
 | 
			
		||||
        addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: node.key });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
// 数据库sql模板菜单节点
 | 
			
		||||
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
 | 
			
		||||
@@ -374,6 +403,7 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
 | 
			
		||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
 | 
			
		||||
    .withContextMenuItems([
 | 
			
		||||
        new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
 | 
			
		||||
        new ContextmenuItem('renameTable', '重命名').withIcon('edit').withOnClick((data: any) => onRenameTable(data)),
 | 
			
		||||
        new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
 | 
			
		||||
        new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
 | 
			
		||||
    ])
 | 
			
		||||
@@ -396,6 +426,7 @@ const tagTreeRef: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const tabs: Map<string, TabInfo> = new Map();
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    defaultExpendKey: [] as any,
 | 
			
		||||
    /**
 | 
			
		||||
     * 当前操作的数据库实例
 | 
			
		||||
     */
 | 
			
		||||
@@ -417,6 +448,7 @@ const state = reactive({
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        db: '',
 | 
			
		||||
        dbType: '',
 | 
			
		||||
        flowProcdef: null as any,
 | 
			
		||||
        data: {},
 | 
			
		||||
        parentKey: '',
 | 
			
		||||
    },
 | 
			
		||||
@@ -429,7 +461,11 @@ const serverInfoReqParam = ref({
 | 
			
		||||
});
 | 
			
		||||
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
 | 
			
		||||
 | 
			
		||||
const autoOpenResourceStore = useAutoOpenResource();
 | 
			
		||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    autoOpenDb(autoOpenResource.value.dbCodePath);
 | 
			
		||||
    setHeight();
 | 
			
		||||
    // 监听浏览器窗口大小变化,更新对应组件高度
 | 
			
		||||
    useEventListener(window, 'resize', setHeight);
 | 
			
		||||
@@ -439,6 +475,31 @@ onBeforeUnmount(() => {
 | 
			
		||||
    dispposeCompletionItemProvider('sql');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => autoOpenResource.value.dbCodePath,
 | 
			
		||||
    (codePath: any) => {
 | 
			
		||||
        autoOpenDb(codePath);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const autoOpenDb = (codePath: string) => {
 | 
			
		||||
    if (!codePath) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const typeAndCodes = getTagTypeCodeByPath(codePath);
 | 
			
		||||
    const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
 | 
			
		||||
 | 
			
		||||
    const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
 | 
			
		||||
    state.defaultExpendKey = [tagPath, dbCode];
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        // 置空
 | 
			
		||||
        autoOpenResourceStore.setDbCodePath('');
 | 
			
		||||
        tagTreeRef.value.setCurrentKey(dbCode);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 设置editor高度和数据表高度
 | 
			
		||||
 */
 | 
			
		||||
@@ -608,6 +669,8 @@ const onTabChange = () => {
 | 
			
		||||
        // 注册sql提示
 | 
			
		||||
        registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const reloadSqls = (dbId: number, db: string) => {
 | 
			
		||||
@@ -639,7 +702,7 @@ const reloadNode = (nodeKey: string) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onEditTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
 | 
			
		||||
    let { db, id, tableName, tableComment, type, parentKey, key, flowProcdef } = data.params;
 | 
			
		||||
    // data.label就是表名
 | 
			
		||||
    if (tableName) {
 | 
			
		||||
        state.tableCreateDialog.title = '修改表';
 | 
			
		||||
@@ -654,22 +717,31 @@ const onEditTable = async (data: any) => {
 | 
			
		||||
        state.tableCreateDialog.parentKey = key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.tableCreateDialog.visible = true;
 | 
			
		||||
    state.tableCreateDialog.activeName = '1';
 | 
			
		||||
    state.tableCreateDialog.dbId = id;
 | 
			
		||||
    state.tableCreateDialog.db = db;
 | 
			
		||||
    state.tableCreateDialog.dbType = type;
 | 
			
		||||
    state.tableCreateDialog.flowProcdef = flowProcdef;
 | 
			
		||||
    state.tableCreateDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onDeleteTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey } = data.params;
 | 
			
		||||
    let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
 | 
			
		||||
    await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 执行sql
 | 
			
		||||
    dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => {
 | 
			
		||||
    let dialect = getDbDialect(state.nowDbInst.type);
 | 
			
		||||
    let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
 | 
			
		||||
 | 
			
		||||
    dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
 | 
			
		||||
        if (flowProcdef) {
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            parentKey && reloadNode(parentKey);
 | 
			
		||||
@@ -677,6 +749,39 @@ const onDeleteTable = async (data: any) => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onRenameTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey, flowProcdef } = data.params;
 | 
			
		||||
    let tableData = { db, oldTableName: tableName, tableName };
 | 
			
		||||
 | 
			
		||||
    let value = ref(tableName);
 | 
			
		||||
    // 弹出确认框
 | 
			
		||||
    const promptValue = await ElMessageBox.prompt('', `重命名表【${db}.${tableName}】`, {
 | 
			
		||||
        inputValue: value.value,
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    tableData.tableName = promptValue.value;
 | 
			
		||||
    let sql = nowDbInst.value.getDialect().getModifyTableInfoSql(tableData);
 | 
			
		||||
    if (!sql) {
 | 
			
		||||
        ElMessage.warning('无更改');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SqlExecBox({
 | 
			
		||||
        sql: sql,
 | 
			
		||||
        dbId: id as any,
 | 
			
		||||
        db: db as any,
 | 
			
		||||
        dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
 | 
			
		||||
        flowProcdef: flowProcdef,
 | 
			
		||||
        runSuccessCallback: () => {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                parentKey && reloadNode(parentKey);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onCopyTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey } = data.params;
 | 
			
		||||
 | 
			
		||||
@@ -728,6 +833,7 @@ const getNowDbInfo = () => {
 | 
			
		||||
        name: di.name,
 | 
			
		||||
        type: di.type,
 | 
			
		||||
        host: di.host,
 | 
			
		||||
        flowProcdef: di.flowProcdef,
 | 
			
		||||
        dbName: state.db,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -735,13 +841,8 @@ const getNowDbInfo = () => {
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.db-sql-exec {
 | 
			
		||||
    .db-table-size {
 | 
			
		||||
        color: #c4c9c4;
 | 
			
		||||
        font-size: 9px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .db-op {
 | 
			
		||||
        height: calc(100vh - 108px);
 | 
			
		||||
        height: calc(100vh - 106px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #data-exec {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
                            <el-row>
 | 
			
		||||
                                <el-col :span="11">
 | 
			
		||||
                                    <el-form-item prop="taskName" label="任务名" required>
 | 
			
		||||
                                        <el-input v-model.trim="form.taskName" placeholder="请输入数据库别名" auto-complete="off" />
 | 
			
		||||
                                        <el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
 | 
			
		||||
                                    </el-form-item>
 | 
			
		||||
                                </el-col>
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +45,7 @@
 | 
			
		||||
                            <db-select-tree
 | 
			
		||||
                                placeholder="请选择源数据库"
 | 
			
		||||
                                v-model:db-id="form.srcDbId"
 | 
			
		||||
                                v-model:inst-name="form.srcInstName"
 | 
			
		||||
                                v-model:db-name="form.srcDbName"
 | 
			
		||||
                                v-model:tag-path="form.srcTagPath"
 | 
			
		||||
                                v-model:db-type="form.srcDbType"
 | 
			
		||||
@@ -56,8 +57,10 @@
 | 
			
		||||
                            <db-select-tree
 | 
			
		||||
                                placeholder="请选择目标数据库"
 | 
			
		||||
                                v-model:db-id="form.targetDbId"
 | 
			
		||||
                                v-model:inst-name="form.targetInstName"
 | 
			
		||||
                                v-model:db-name="form.targetDbName"
 | 
			
		||||
                                v-model:tag-path="form.targetTagPath"
 | 
			
		||||
                                v-model:db-type="form.targetDbType"
 | 
			
		||||
                                @select-db="onSelectTargetDb"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
@@ -86,9 +89,11 @@
 | 
			
		||||
                                </el-col>
 | 
			
		||||
 | 
			
		||||
                                <el-col :span="8">
 | 
			
		||||
                                    <el-form-item prop="updField" label="更新字段" required>
 | 
			
		||||
                                        <el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
 | 
			
		||||
                                    </el-form-item>
 | 
			
		||||
                                    <el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
 | 
			
		||||
                                        <el-form-item prop="updField" label="更新字段" required>
 | 
			
		||||
                                            <el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
 | 
			
		||||
                                        </el-form-item>
 | 
			
		||||
                                    </el-tooltip>
 | 
			
		||||
                                </el-col>
 | 
			
		||||
 | 
			
		||||
                                <el-col :span="8">
 | 
			
		||||
@@ -122,10 +127,17 @@
 | 
			
		||||
 | 
			
		||||
                    <el-tab-pane label="sql预览" :name="sqlPreviewTab" :disabled="!baseFieldCompleted">
 | 
			
		||||
                        <el-form-item prop="fieldMap" label="查询sql">
 | 
			
		||||
                            <el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '190px' }" />
 | 
			
		||||
                            <el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '170px' }" />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="fieldMap" label="插入sql">
 | 
			
		||||
                            <el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '190px' }" />
 | 
			
		||||
                            <el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '170px' }" />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="isReplace" v-if="compatibleDuplicateStrategy(form.targetDbType!)" label="键冲突策略">
 | 
			
		||||
                            <el-select v-model="form.duplicateStrategy" @change="handleDuplicateStrategy" style="width: 100px">
 | 
			
		||||
                                <el-option label="无" :value="DuplicateStrategy.NONE" />
 | 
			
		||||
                                <el-option label="忽略" :value="DuplicateStrategy.IGNORE" />
 | 
			
		||||
                                <el-option label="替换" :value="DuplicateStrategy.REPLACE" />
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                    </el-tab-pane>
 | 
			
		||||
                </el-tabs>
 | 
			
		||||
@@ -182,7 +194,7 @@ import { ElMessage } from 'element-plus';
 | 
			
		||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
 | 
			
		||||
import {DbType, getDbDialect} from '@/views/ops/db/dialect'
 | 
			
		||||
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
@@ -227,19 +239,23 @@ type FormData = {
 | 
			
		||||
    taskName?: string;
 | 
			
		||||
    taskCron: string;
 | 
			
		||||
    srcDbId?: number;
 | 
			
		||||
    srcInstName?: string;
 | 
			
		||||
    srcDbName?: string;
 | 
			
		||||
    srcDbType?: string;
 | 
			
		||||
    srcTagPath?: string;
 | 
			
		||||
    targetDbId?: number;
 | 
			
		||||
    targetInstName?: string;
 | 
			
		||||
    targetDbName?: string;
 | 
			
		||||
    targetTagPath?: string;
 | 
			
		||||
    targetTableName?: string;
 | 
			
		||||
    targetDbType?: string;
 | 
			
		||||
    dataSql?: string;
 | 
			
		||||
    pageSize?: number;
 | 
			
		||||
    updField?: string;
 | 
			
		||||
    updFieldVal?: string;
 | 
			
		||||
    fieldMap?: { src: string; target: string }[];
 | 
			
		||||
    status?: 1 | 2;
 | 
			
		||||
    duplicateStrategy?: -1 | 1 | 2;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const basicFormData = {
 | 
			
		||||
@@ -251,6 +267,7 @@ const basicFormData = {
 | 
			
		||||
    updFieldVal: '0',
 | 
			
		||||
    fieldMap: [{ src: 'a', target: 'b' }],
 | 
			
		||||
    status: 1,
 | 
			
		||||
    duplicateStrategy: -1,
 | 
			
		||||
} as FormData;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -265,6 +282,7 @@ const state = reactive({
 | 
			
		||||
    previewRes: {} as any,
 | 
			
		||||
    previewDataSql: '',
 | 
			
		||||
    previewInsertSql: '',
 | 
			
		||||
    previewFieldArr: [] as string[],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tabActiveName, form, submitForm } = toRefs(state);
 | 
			
		||||
@@ -283,12 +301,17 @@ watch(dialogVisible, async (newValue: boolean) => {
 | 
			
		||||
    state.tabActiveName = 'basic';
 | 
			
		||||
    const propsData = props.data as any;
 | 
			
		||||
    if (!propsData?.id) {
 | 
			
		||||
        state.form = basicFormData;
 | 
			
		||||
        let d = {} as FormData;
 | 
			
		||||
        Object.assign(d, basicFormData);
 | 
			
		||||
        state.form = d;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
 | 
			
		||||
    state.form = data;
 | 
			
		||||
    if (!state.form.duplicateStrategy) {
 | 
			
		||||
        state.form.duplicateStrategy = -1;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
        state.form.fieldMap = JSON.parse(data.fieldMap);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -304,7 +327,8 @@ watch(dialogVisible, async (newValue: boolean) => {
 | 
			
		||||
        // 初始化实例
 | 
			
		||||
        db.databases = db.database?.split(' ').sort() || [];
 | 
			
		||||
        state.srcDbInst = DbInst.getOrNewInst(db);
 | 
			
		||||
        state.form.srcDbType = state.srcDbInst.type
 | 
			
		||||
        state.form.srcDbType = state.srcDbInst.type;
 | 
			
		||||
        state.form.srcInstName = db.instanceName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //  初始化target数据源
 | 
			
		||||
@@ -315,6 +339,8 @@ watch(dialogVisible, async (newValue: boolean) => {
 | 
			
		||||
        // 初始化实例
 | 
			
		||||
        db.databases = db.database?.split(' ').sort() || [];
 | 
			
		||||
        state.targetDbInst = DbInst.getOrNewInst(db);
 | 
			
		||||
        state.form.targetDbType = state.targetDbInst.type;
 | 
			
		||||
        state.form.targetInstName = db.instanceName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (targetDbId && state.form.targetDbName) {
 | 
			
		||||
@@ -334,13 +360,12 @@ watch(tabActiveName, async (newValue: string) => {
 | 
			
		||||
            await handleGetTargetFields();
 | 
			
		||||
            break;
 | 
			
		||||
        case sqlPreviewTab:
 | 
			
		||||
            let srcDbDialect = getDbDialect(state.srcDbInst.type);
 | 
			
		||||
            let targetDbDialect = getDbDialect(state.targetDbInst.type);
 | 
			
		||||
            let updField = state.form.updField!;
 | 
			
		||||
 | 
			
		||||
            let updField = srcDbDialect.quoteIdentifier(state.form.updField!);
 | 
			
		||||
            state.previewDataSql = `SELECT * FROM (\n ${state.form.dataSql?.trim() || '请输入数据sql'} \n ) t \n where ${updField} > '${
 | 
			
		||||
                state.form.updFieldVal || ''
 | 
			
		||||
            }'`;
 | 
			
		||||
            // 判断sql是否以where .*结尾
 | 
			
		||||
            let hasCondition = /where/i.test(state.form.dataSql!);
 | 
			
		||||
            state.previewDataSql = `${state.form.dataSql?.trim() || '请输入数据sql'} \n ${hasCondition ? 'and' : 'where'} ${updField} > '${state.form.updFieldVal || ''}'`;
 | 
			
		||||
 | 
			
		||||
            // 检查字段映射中是否存在重复的目标字段
 | 
			
		||||
            let fields = new Set();
 | 
			
		||||
@@ -356,17 +381,19 @@ watch(tabActiveName, async (newValue: string) => {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let fieldArr = state.form.fieldMap?.map((a: any) => targetDbDialect.quoteIdentifier(a.target)) || [];
 | 
			
		||||
            let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
 | 
			
		||||
 | 
			
		||||
            state.previewInsertSql = ` insert into ${targetDbDialect.quoteIdentifier(state.form.targetTableName!)}(${fieldArr.join(
 | 
			
		||||
                ','
 | 
			
		||||
            )}) values (${placeholder});`;
 | 
			
		||||
            state.previewFieldArr = fieldArr;
 | 
			
		||||
            refreshPreviewInsertSql();
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const refreshPreviewInsertSql = () => {
 | 
			
		||||
    let targetDbDialect = getDbDialect(state.targetDbInst.type);
 | 
			
		||||
    state.previewInsertSql = targetDbDialect.getBatchInsertPreviewSql(state.form.targetTableName!, state.previewFieldArr, state.form.duplicateStrategy!);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onSelectSrcDb = async (params: any) => {
 | 
			
		||||
    //  初始化数据源
 | 
			
		||||
    params.databases = params.dbs; // 数据源里需要这个值
 | 
			
		||||
@@ -413,16 +440,24 @@ const handleGetSrcFields = async () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 执行sql
 | 
			
		||||
    // oracle的分页关键字不一样
 | 
			
		||||
    let limit = ' limit 1'
 | 
			
		||||
    if(state.form.srcDbType === DbType.oracle){
 | 
			
		||||
      limit = ' where rownum <= 1'
 | 
			
		||||
    let sql: string;
 | 
			
		||||
 | 
			
		||||
    if (state.form.srcDbType === DbType.mssql) {
 | 
			
		||||
        // mssql的分页语法不一样
 | 
			
		||||
        let top1 = `select top 1`;
 | 
			
		||||
        sql = `${top1} * from (${state.form.dataSql}) a`;
 | 
			
		||||
    } else if (state.form.srcDbType === DbType.oracle) {
 | 
			
		||||
        // oracle的分页关键字不一样
 | 
			
		||||
        let hasCondition = /where/i.test(state.form.dataSql!);
 | 
			
		||||
        sql = `${state.form.dataSql} ${hasCondition ? 'and' : 'where'} rownum <= 1`;
 | 
			
		||||
    } else {
 | 
			
		||||
        sql = `${state.form.dataSql} limit 1`;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const res = await dbApi.sqlExec.request({
 | 
			
		||||
        id: state.form.srcDbId,
 | 
			
		||||
        db: state.form.srcDbName,
 | 
			
		||||
        sql: `select * from (${state.form.dataSql}) t ${limit}`
 | 
			
		||||
        sql,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!res.columns) {
 | 
			
		||||
@@ -496,6 +531,11 @@ const btnOk = async () => {
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.form = basicFormData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDuplicateStrategy = () => {
 | 
			
		||||
    refreshPreviewInsertSql();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -71,11 +71,13 @@ const searchItems = [SearchItem.input('name', '名称')];
 | 
			
		||||
// 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('taskName', '任务名'),
 | 
			
		||||
    TableColumn.new('runningState', '运行状态').alignCenter().typeTag(DbDataSyncRunningStateEnum),
 | 
			
		||||
    TableColumn.new('recentState', '最近任务状态').alignCenter().typeTag(DbDataSyncRecentStateEnum),
 | 
			
		||||
    TableColumn.new('status', '状态').alignCenter().isSlot(),
 | 
			
		||||
    TableColumn.new('modifier', '修改人').alignCenter(),
 | 
			
		||||
    TableColumn.new('updateTime', '修改时间').alignCenter().isTime(),
 | 
			
		||||
    TableColumn.new('runningState', '运行状态').typeTag(DbDataSyncRunningStateEnum),
 | 
			
		||||
    TableColumn.new('recentState', '最近任务状态').typeTag(DbDataSyncRecentStateEnum),
 | 
			
		||||
    TableColumn.new('status', '状态').isSlot(),
 | 
			
		||||
    TableColumn.new('creator', '创建人'),
 | 
			
		||||
    TableColumn.new('createTime', '创建时间').isTime(),
 | 
			
		||||
    TableColumn.new('modifier', '修改人'),
 | 
			
		||||
    TableColumn.new('updateTime', '修改时间').isTime(),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
 
 | 
			
		||||
@@ -35,15 +35,14 @@ export const dbApi = {
 | 
			
		||||
    getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
 | 
			
		||||
    deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
 | 
			
		||||
    // 获取数据库sql执行记录
 | 
			
		||||
    getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'),
 | 
			
		||||
    getSqlExecs: Api.newGet('/dbs/sql-execs'),
 | 
			
		||||
 | 
			
		||||
    instances: Api.newGet('/instances'),
 | 
			
		||||
    getInstance: Api.newGet('/instances/{instanceId}'),
 | 
			
		||||
    getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
 | 
			
		||||
    getAllDatabase: Api.newPost('/instances/databases'),
 | 
			
		||||
    getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
 | 
			
		||||
    testConn: Api.newPost('/instances/test-conn'),
 | 
			
		||||
    saveInstance: Api.newPost('/instances'),
 | 
			
		||||
    getInstancePwd: Api.newGet('/instances/{id}/pwd'),
 | 
			
		||||
    deleteInstance: Api.newDelete('/instances/{id}'),
 | 
			
		||||
 | 
			
		||||
    // 获取数据库备份列表
 | 
			
		||||
@@ -83,4 +82,17 @@ export const dbApi = {
 | 
			
		||||
    runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
 | 
			
		||||
    stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
 | 
			
		||||
    datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
 | 
			
		||||
 | 
			
		||||
    // 数据库迁移相关
 | 
			
		||||
    dbTransferTasks: Api.newGet('/dbTransfer'),
 | 
			
		||||
    saveDbTransferTask: Api.newPost('/dbTransfer/save'),
 | 
			
		||||
    deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
 | 
			
		||||
    runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
 | 
			
		||||
    stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
 | 
			
		||||
    dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dbSqlExecApi = {
 | 
			
		||||
    // 根据业务key获取sql执行信息
 | 
			
		||||
    getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,9 @@
 | 
			
		||||
        :resource-type="TagResourceTypeEnum.Db.value"
 | 
			
		||||
        :tag-path-node-type="NodeTypeTagPath"
 | 
			
		||||
    >
 | 
			
		||||
        <template #iconPrefix>
 | 
			
		||||
            <SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
 | 
			
		||||
        </template>
 | 
			
		||||
        <template #prefix="{ data }">
 | 
			
		||||
            <SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
 | 
			
		||||
            <SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
 | 
			
		||||
@@ -27,6 +30,9 @@ const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    },
 | 
			
		||||
    instName: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    dbName: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
@@ -38,7 +44,7 @@ const props = defineProps({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'update:dbType', 'selectDb']);
 | 
			
		||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 树节点类型
 | 
			
		||||
@@ -56,7 +62,7 @@ class SqlExecNodeType {
 | 
			
		||||
 | 
			
		||||
const selectNode = computed({
 | 
			
		||||
    get: () => {
 | 
			
		||||
        return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
 | 
			
		||||
        return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
 | 
			
		||||
    },
 | 
			
		||||
    set: () => {
 | 
			
		||||
        //
 | 
			
		||||
@@ -151,6 +157,7 @@ const changeNode = (nodeData: TagTreeNode) => {
 | 
			
		||||
    const params = nodeData.params;
 | 
			
		||||
    // postgres
 | 
			
		||||
    emits('update:dbName', params.db);
 | 
			
		||||
    emits('update:instName', params.name);
 | 
			
		||||
    emits('update:dbId', params.id);
 | 
			
		||||
    emits('update:tagPath', params.tagPath);
 | 
			
		||||
    emits('update:dbType', params.type);
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@
 | 
			
		||||
                                :loading="dt.loading"
 | 
			
		||||
                                :abort-fn="dt.abortFn"
 | 
			
		||||
                                :height="tableDataHeight"
 | 
			
		||||
                                empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
 | 
			
		||||
                                :empty-text="state.tableDataEmptyText"
 | 
			
		||||
                                @change-updated-field="changeUpdatedField($event, dt)"
 | 
			
		||||
                                @data-delete="onDeleteData($event, dt)"
 | 
			
		||||
                            ></db-table-data>
 | 
			
		||||
@@ -148,7 +148,6 @@ import { buildProgressProps } from '@/components/progress-notify/progress-notify
 | 
			
		||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
 | 
			
		||||
import syssocket from '@/common/syssocket';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { getDbDialect } from '../../dialect';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['saveSqlSuccess']);
 | 
			
		||||
@@ -222,6 +221,7 @@ const state = reactive({
 | 
			
		||||
    activeTab: 1,
 | 
			
		||||
    editorHeight: '500',
 | 
			
		||||
    tableDataHeight: '250px',
 | 
			
		||||
    tableDataEmptyText: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tableDataHeight } = toRefs(state);
 | 
			
		||||
@@ -298,13 +298,16 @@ const onRunSql = async (newTab = false) => {
 | 
			
		||||
    sql = sql.replace(/(^\s*)/g, '');
 | 
			
		||||
    let execRemark = '';
 | 
			
		||||
    let canRun = true;
 | 
			
		||||
 | 
			
		||||
    // 简单截取前十个字符
 | 
			
		||||
    const sqlPrefix = sql.slice(0, 10).toLowerCase();
 | 
			
		||||
    if (
 | 
			
		||||
        sql.startsWith('update') ||
 | 
			
		||||
        sql.startsWith('UPDATE') ||
 | 
			
		||||
        sql.startsWith('INSERT') ||
 | 
			
		||||
        sql.startsWith('insert') ||
 | 
			
		||||
        sql.startsWith('DELETE') ||
 | 
			
		||||
        sql.startsWith('delete')
 | 
			
		||||
        sqlPrefix.startsWith('update') ||
 | 
			
		||||
        sqlPrefix.startsWith('insert') ||
 | 
			
		||||
        sqlPrefix.startsWith('delete') ||
 | 
			
		||||
        sqlPrefix.startsWith('alert') ||
 | 
			
		||||
        sqlPrefix.startsWith('drop') ||
 | 
			
		||||
        sqlPrefix.startsWith('create')
 | 
			
		||||
    ) {
 | 
			
		||||
        const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
@@ -321,6 +324,18 @@ const onRunSql = async (newTab = false) => {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 启用工单审批
 | 
			
		||||
    if (execRemark && getNowDbInst().flowProcdef) {
 | 
			
		||||
        try {
 | 
			
		||||
            await getNowDbInst().runSql(props.dbName, sql, execRemark);
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            ElMessage.success('工单提交失败');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let execRes: ExecResTab;
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    let id;
 | 
			
		||||
@@ -354,9 +369,8 @@ const onRunSql = async (newTab = false) => {
 | 
			
		||||
 | 
			
		||||
        await execute();
 | 
			
		||||
        const colAndData: any = data.value;
 | 
			
		||||
        if (!colAndData.res || colAndData.res.length === 0) {
 | 
			
		||||
            ElMessage.warning('未查询到结果集');
 | 
			
		||||
            return;
 | 
			
		||||
        if (colAndData.res.length == 0) {
 | 
			
		||||
            state.tableDataEmptyText = '查无数据';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 要实时响应,故需要用索引改变数据才生效
 | 
			
		||||
@@ -453,7 +467,7 @@ const formatSql = () => {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
 | 
			
		||||
    const formatDialect: any = getNowDbInst().getDialect().getInfo().formatSqlDialect;
 | 
			
		||||
 | 
			
		||||
    let sql = monacoEditor.getModel()?.getValueInRange(selection);
 | 
			
		||||
    // 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { h, render, VNode } from 'vue';
 | 
			
		||||
import { h, render } from 'vue';
 | 
			
		||||
import SqlExecDialog from './SqlExecDialog.vue';
 | 
			
		||||
 | 
			
		||||
export type SqlExecProps = {
 | 
			
		||||
@@ -6,31 +6,26 @@ export type SqlExecProps = {
 | 
			
		||||
    dbId: number;
 | 
			
		||||
    db: string;
 | 
			
		||||
    dbType?: string;
 | 
			
		||||
    flowProcdef?: any;
 | 
			
		||||
    runSuccessCallback?: Function;
 | 
			
		||||
    cancelCallback?: Function;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const boxId = 'sql-exec-dialog-id';
 | 
			
		||||
 | 
			
		||||
let boxInstance: VNode;
 | 
			
		||||
 | 
			
		||||
const SqlExecBox = (props: SqlExecProps): void => {
 | 
			
		||||
    if (!boxInstance) {
 | 
			
		||||
        const container = document.createElement('div');
 | 
			
		||||
        container.id = boxId;
 | 
			
		||||
        // 创建 虚拟dom
 | 
			
		||||
        boxInstance = h(SqlExecDialog);
 | 
			
		||||
        // 将虚拟dom渲染到 container dom 上
 | 
			
		||||
        render(boxInstance, container);
 | 
			
		||||
        // 最后将 container 追加到 body 上
 | 
			
		||||
        document.body.appendChild(container);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const boxVue = boxInstance.component;
 | 
			
		||||
    if (boxVue) {
 | 
			
		||||
        // 调用open方法显示弹框,注意不能使用boxVue.ctx来调用组件函数(build打包后ctx会获取不到)
 | 
			
		||||
        boxVue.exposed?.open(props);
 | 
			
		||||
    }
 | 
			
		||||
    const propsCancelFn = props.cancelCallback;
 | 
			
		||||
    //  包装取消回调函数,新增销毁组件代码
 | 
			
		||||
    props.cancelCallback = () => {
 | 
			
		||||
        propsCancelFn && propsCancelFn();
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            // 销毁组件
 | 
			
		||||
            render(null, document.body);
 | 
			
		||||
        }, 500);
 | 
			
		||||
    };
 | 
			
		||||
    const vnode = h(SqlExecDialog, {
 | 
			
		||||
        ...props,
 | 
			
		||||
        visible: true,
 | 
			
		||||
    });
 | 
			
		||||
    render(vnode, document.body);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SqlExecBox;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
 | 
			
		||||
        <el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" :close-on-click-modal="false">
 | 
			
		||||
            <monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
 | 
			
		||||
            <el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
 | 
			
		||||
            <el-input
 | 
			
		||||
                @keyup.enter="runSql"
 | 
			
		||||
                ref="remarkInputRef"
 | 
			
		||||
                v-model="remark"
 | 
			
		||||
                :placeholder="props.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
 | 
			
		||||
                class="mt5"
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <div v-if="props.flowProcdef">
 | 
			
		||||
                <el-divider content-position="left">审批节点</el-divider>
 | 
			
		||||
                <procdef-tasks :procdef="props.flowProcdef" />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <span class="dialog-footer">
 | 
			
		||||
                    <el-button @click="cancel">取 消</el-button>
 | 
			
		||||
@@ -14,52 +26,40 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, ref, nextTick, reactive } from 'vue';
 | 
			
		||||
import { toRefs, ref, reactive, onMounted } from 'vue';
 | 
			
		||||
import { dbApi } from '@/views/ops/db/api';
 | 
			
		||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
 | 
			
		||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
 | 
			
		||||
// import base style
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { format as sqlFormatter } from 'sql-formatter';
 | 
			
		||||
 | 
			
		||||
import { SqlExecProps } from './SqlExecBox';
 | 
			
		||||
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
    dbId: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    sql: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
const props = withDefaults(defineProps<SqlExecProps>(), {});
 | 
			
		||||
 | 
			
		||||
const remarkInputRef = ref<InputInstance>();
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    sqlValue: '',
 | 
			
		||||
    dbId: 0,
 | 
			
		||||
    db: '',
 | 
			
		||||
    remark: '',
 | 
			
		||||
    btnLoading: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
state.sqlValue = props.sql as any;
 | 
			
		||||
let runSuccessCallback: any;
 | 
			
		||||
let cancelCallback: any;
 | 
			
		||||
let runSuccess: boolean = false;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    open();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 执行sql
 | 
			
		||||
 */
 | 
			
		||||
const runSql = async () => {
 | 
			
		||||
    if (!state.remark) {
 | 
			
		||||
    // 存在流程审批,则备注为必填
 | 
			
		||||
    if (!state.remark && props.flowProcdef) {
 | 
			
		||||
        ElMessage.error('请输入执行的备注信息');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -67,12 +67,19 @@ const runSql = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.btnLoading = true;
 | 
			
		||||
        const res = await dbApi.sqlExec.request({
 | 
			
		||||
            id: state.dbId,
 | 
			
		||||
            db: state.db,
 | 
			
		||||
            id: props.dbId,
 | 
			
		||||
            db: props.db,
 | 
			
		||||
            remark: state.remark,
 | 
			
		||||
            sql: state.sqlValue.trim(),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 存在流程审批
 | 
			
		||||
        if (props.flowProcdef) {
 | 
			
		||||
            runSuccess = false;
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let re of res.res) {
 | 
			
		||||
            if (re.result !== 'success') {
 | 
			
		||||
                ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
 | 
			
		||||
@@ -84,45 +91,33 @@ const runSql = async () => {
 | 
			
		||||
        ElMessage.success('执行成功');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        runSuccess = false;
 | 
			
		||||
    }
 | 
			
		||||
    if (runSuccess) {
 | 
			
		||||
        if (runSuccessCallback) {
 | 
			
		||||
            runSuccessCallback();
 | 
			
		||||
    } finally {
 | 
			
		||||
        if (runSuccess) {
 | 
			
		||||
            if (props.runSuccessCallback) {
 | 
			
		||||
                props.runSuccessCallback();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        state.btnLoading = false;
 | 
			
		||||
        cancel();
 | 
			
		||||
    }
 | 
			
		||||
    state.btnLoading = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    state.dialogVisible = false;
 | 
			
		||||
    // 没有执行成功,并且取消回调函数存在,则执行
 | 
			
		||||
    if (!runSuccess && cancelCallback) {
 | 
			
		||||
        cancelCallback();
 | 
			
		||||
    }
 | 
			
		||||
    props.cancelCallback && props.cancelCallback();
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        state.dbId = 0;
 | 
			
		||||
        state.sqlValue = '';
 | 
			
		||||
        state.remark = '';
 | 
			
		||||
        runSuccessCallback = null;
 | 
			
		||||
        cancelCallback = null;
 | 
			
		||||
        runSuccess = false;
 | 
			
		||||
    }, 200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const open = (props: SqlExecProps) => {
 | 
			
		||||
    runSuccessCallback = props.runSuccessCallback;
 | 
			
		||||
    cancelCallback = props.cancelCallback;
 | 
			
		||||
    props.dbType = props.dbType || 'mysql';
 | 
			
		||||
    state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
 | 
			
		||||
    state.dbId = props.dbId;
 | 
			
		||||
    state.db = props.db;
 | 
			
		||||
const open = () => {
 | 
			
		||||
    state.sqlValue = sqlFormatter(props.sql, { language: (props.dbType || 'mysql') as any });
 | 
			
		||||
    state.dialogVisible = true;
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            remarkInputRef.value?.focus();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        remarkInputRef.value?.focus();
 | 
			
		||||
    }, 200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ open });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="string-input-container w100" v-if="dataType == DataType.String">
 | 
			
		||||
    <div class="string-input-container w100" v-if="dataType == DataType.String || dataType == DataType.Number">
 | 
			
		||||
        <el-input
 | 
			
		||||
            v-if="dataType == DataType.String"
 | 
			
		||||
            :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
            :disabled="disabled"
 | 
			
		||||
            @blur="handleBlur"
 | 
			
		||||
            :class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
 | 
			
		||||
            input-style="text-align: center; height: 26px;"
 | 
			
		||||
            size="small"
 | 
			
		||||
            v-model="itemValue"
 | 
			
		||||
            :placeholder="placeholder"
 | 
			
		||||
@@ -14,19 +12,6 @@
 | 
			
		||||
        <SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-input
 | 
			
		||||
        v-else-if="dataType == DataType.Number"
 | 
			
		||||
        :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        @blur="handleBlur"
 | 
			
		||||
        class="w100 mb4"
 | 
			
		||||
        input-style="text-align: center; height: 26px;"
 | 
			
		||||
        size="small"
 | 
			
		||||
        v-model.number="itemValue"
 | 
			
		||||
        :placeholder="placeholder"
 | 
			
		||||
        type="number"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <el-date-picker
 | 
			
		||||
        v-else-if="dataType == DataType.Date"
 | 
			
		||||
        :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
@@ -77,7 +62,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref, Ref } from 'vue';
 | 
			
		||||
import { ElInput } from 'element-plus';
 | 
			
		||||
import { ElInput, ElMessage } from 'element-plus';
 | 
			
		||||
import { DataType } from '../../dialect/index';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
 | 
			
		||||
@@ -132,6 +117,10 @@ const handleBlur = () => {
 | 
			
		||||
    if (editorOpening.value) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (props.dataType == DataType.Number && !/^-?\d*\.?\d+$/.test(itemValue.value)) {
 | 
			
		||||
        ElMessage.error('输入内容与类型不匹配');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    emit('update:modelValue', itemValue.value);
 | 
			
		||||
    emit('blur');
 | 
			
		||||
};
 | 
			
		||||
@@ -185,9 +174,6 @@ const getEditorLangByValue = (value: any) => {
 | 
			
		||||
    .el-input__prefix {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
    .el-input__inner {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.edit-time-picker-popper {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
                                    <!-- 字段列的数据类型 -->
 | 
			
		||||
                                    <div class="column-type">
 | 
			
		||||
                                        <span v-if="column.dataTypeSubscript === 'icon-clock'">
 | 
			
		||||
                                            <SvgIcon :size="10" name="Clock" style="cursor: unset" />
 | 
			
		||||
                                            <SvgIcon :size="9" name="Clock" style="cursor: unset" />
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                        <span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
 | 
			
		||||
                                    </div>
 | 
			
		||||
@@ -121,7 +121,7 @@
 | 
			
		||||
 | 
			
		||||
                    <template #empty>
 | 
			
		||||
                        <div style="text-align: center">
 | 
			
		||||
                            <el-empty class="h100" :description="state.emptyText" :image-size="100" />
 | 
			
		||||
                            <el-empty class="h100" :description="props.emptyText" :image-size="100" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-v2>
 | 
			
		||||
@@ -137,13 +137,25 @@
 | 
			
		||||
            <el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <DbTableDataForm
 | 
			
		||||
            v-if="state.tableDataFormDialog.visible"
 | 
			
		||||
            :db-inst="getNowDbInst()"
 | 
			
		||||
            :db-name="db"
 | 
			
		||||
            :columns="columns!"
 | 
			
		||||
            :title="state.tableDataFormDialog.title"
 | 
			
		||||
            :table-name="table"
 | 
			
		||||
            v-model:visible="state.tableDataFormDialog.visible"
 | 
			
		||||
            v-model="state.tableDataFormDialog.data"
 | 
			
		||||
            @submit-success="emits('changeUpdatedField')"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { ElInput } from 'element-plus';
 | 
			
		||||
import { ElInput, ElMessage } from 'element-plus';
 | 
			
		||||
import { copyToClipboard } from '@/common/utils/string';
 | 
			
		||||
import { DbInst } from '@/views/ops/db/db';
 | 
			
		||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
@@ -153,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
 | 
			
		||||
import { useIntervalFn, useStorage } from '@vueuse/core';
 | 
			
		||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
 | 
			
		||||
import ColumnFormItem from './ColumnFormItem.vue';
 | 
			
		||||
import DbTableDataForm from './DbTableDataForm.vue';
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
 | 
			
		||||
 | 
			
		||||
@@ -246,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
 | 
			
		||||
        return state.table == '';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
 | 
			
		||||
    .withIcon('edit')
 | 
			
		||||
    .withOnClick(() => onEditRowData())
 | 
			
		||||
    .withHideFunc(() => {
 | 
			
		||||
        return state.table == '';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
 | 
			
		||||
    .withIcon('tickets')
 | 
			
		||||
    .withOnClick(() => onGenerateInsertSql())
 | 
			
		||||
@@ -322,8 +342,6 @@ const state = reactive({
 | 
			
		||||
    columns: [] as any,
 | 
			
		||||
    loading: false,
 | 
			
		||||
    tableHeight: '600px',
 | 
			
		||||
    emptyText: '',
 | 
			
		||||
 | 
			
		||||
    execTime: 0,
 | 
			
		||||
    contextmenu: {
 | 
			
		||||
        dropdown: {
 | 
			
		||||
@@ -332,7 +350,11 @@ const state = reactive({
 | 
			
		||||
        },
 | 
			
		||||
        items: [] as ContextmenuItem[],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    tableDataFormDialog: {
 | 
			
		||||
        data: {},
 | 
			
		||||
        title: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
    },
 | 
			
		||||
    genTxtDialog: {
 | 
			
		||||
        title: 'SQL',
 | 
			
		||||
        visible: false,
 | 
			
		||||
@@ -410,7 +432,6 @@ onMounted(async () => {
 | 
			
		||||
    console.log('in DbTable mounted');
 | 
			
		||||
    state.tableHeight = props.height;
 | 
			
		||||
    state.loading = props.loading;
 | 
			
		||||
    state.emptyText = props.emptyText;
 | 
			
		||||
 | 
			
		||||
    state.dbId = props.dbId;
 | 
			
		||||
    state.dbType = getNowDbInst().type;
 | 
			
		||||
@@ -437,13 +458,13 @@ const formatDataValues = (datas: any) => {
 | 
			
		||||
 | 
			
		||||
    for (let data of datas) {
 | 
			
		||||
        for (let column of props.columns!) {
 | 
			
		||||
            data[column.columnName] = getFormatTimeValue(dbDialect.getDataType(column.columnType), data[column.columnName]);
 | 
			
		||||
            data[column.columnName] = getFormatTimeValue(dbDialect.getDataType(column.dataType), data[column.columnName]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setTableData = (datas: any) => {
 | 
			
		||||
    tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
 | 
			
		||||
    tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
 | 
			
		||||
    selectionRowsMap.clear();
 | 
			
		||||
    cellUpdateMap.clear();
 | 
			
		||||
    formatDataValues(datas);
 | 
			
		||||
@@ -575,7 +596,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
 | 
			
		||||
    const { clientX, clientY } = event;
 | 
			
		||||
    state.contextmenu.dropdown.x = clientX;
 | 
			
		||||
    state.contextmenu.dropdown.y = clientY;
 | 
			
		||||
    state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
 | 
			
		||||
    state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
 | 
			
		||||
    contextmenuRef.value.openContextmenu({ column, rowData: data });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -600,6 +621,18 @@ const onDeleteData = async () => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onEditRowData = () => {
 | 
			
		||||
    const selectionDatas = Array.from(selectionRowsMap.values());
 | 
			
		||||
    if (selectionDatas.length > 1) {
 | 
			
		||||
        ElMessage.warning('只能编辑一行数据');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = selectionDatas[0];
 | 
			
		||||
    state.tableDataFormDialog.data = data;
 | 
			
		||||
    state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
 | 
			
		||||
    state.tableDataFormDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onGenerateInsertSql = async () => {
 | 
			
		||||
    const selectionDatas = Array.from(selectionRowsMap.values());
 | 
			
		||||
    state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
 | 
			
		||||
@@ -713,52 +746,28 @@ const submitUpdateFields = async () => {
 | 
			
		||||
 | 
			
		||||
    const db = state.db;
 | 
			
		||||
    let res = '';
 | 
			
		||||
    const dbDialect = getDbDialect(dbInst.type);
 | 
			
		||||
    let schema = '';
 | 
			
		||||
    let dbArr = db.split('/');
 | 
			
		||||
    if (dbArr.length == 2) {
 | 
			
		||||
        schema = dbInst.wrapName(dbArr[1]) + '.';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let updateRow of cellUpdateMap.values()) {
 | 
			
		||||
        let sql = `UPDATE ${schema}${dbInst.wrapName(state.table)} SET `;
 | 
			
		||||
        const rowData = updateRow.rowData;
 | 
			
		||||
        // 主键列信息
 | 
			
		||||
        const primaryKey = await dbInst.loadTableColumn(db, state.table);
 | 
			
		||||
        let primaryKeyType = primaryKey.columnType;
 | 
			
		||||
        let primaryKeyName = primaryKey.columnName;
 | 
			
		||||
        let primaryKeyValue = rowData[primaryKeyName];
 | 
			
		||||
        const rowData = { ...updateRow.rowData };
 | 
			
		||||
        let updateColumnValue = {};
 | 
			
		||||
 | 
			
		||||
        for (let k of updateRow.columnsMap.keys()) {
 | 
			
		||||
            const v = updateRow.columnsMap.get(k);
 | 
			
		||||
            if (!v) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // 更新字段列信息
 | 
			
		||||
            const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
 | 
			
		||||
 | 
			
		||||
            sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
 | 
			
		||||
 | 
			
		||||
            // 如果修改的字段是主键
 | 
			
		||||
            if (k === primaryKeyName) {
 | 
			
		||||
                primaryKeyValue = v.oldValue;
 | 
			
		||||
            }
 | 
			
		||||
            updateColumnValue[k] = rowData[k];
 | 
			
		||||
            // 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
 | 
			
		||||
            rowData[k] = v.oldValue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sql = sql.substring(0, sql.length - 1);
 | 
			
		||||
        sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
 | 
			
		||||
        res += sql;
 | 
			
		||||
        res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbInst.promptExeSql(
 | 
			
		||||
        db,
 | 
			
		||||
        res,
 | 
			
		||||
        () => {},
 | 
			
		||||
        () => {
 | 
			
		||||
            triggerRefresh();
 | 
			
		||||
            cellUpdateMap.clear();
 | 
			
		||||
            changeUpdatedField();
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
 | 
			
		||||
        triggerRefresh();
 | 
			
		||||
        cellUpdateMap.clear();
 | 
			
		||||
        changeUpdatedField();
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelUpdateFields = () => {
 | 
			
		||||
@@ -871,8 +880,8 @@ defineExpose({
 | 
			
		||||
        color: var(--el-color-info-light-3);
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: -5px;
 | 
			
		||||
        padding: 2px;
 | 
			
		||||
        top: -7px;
 | 
			
		||||
        padding: 1px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .column-right {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,121 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
 | 
			
		||||
        <el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
 | 
			
		||||
            <el-form-item
 | 
			
		||||
                v-for="column in columns"
 | 
			
		||||
                :key="column.columnName"
 | 
			
		||||
                class="w100 mb5"
 | 
			
		||||
                :prop="column.columnName"
 | 
			
		||||
                :required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
 | 
			
		||||
            >
 | 
			
		||||
                <template #label>
 | 
			
		||||
                    <span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
 | 
			
		||||
                        {{ column.columnName }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <ColumnFormItem
 | 
			
		||||
                    v-model="modelValue[`${column.columnName}`]"
 | 
			
		||||
                    :data-type="dbInst.getDialect().getDataType(column.dataType)"
 | 
			
		||||
                    :placeholder="`${column.columnType}  ${column.columnComment}`"
 | 
			
		||||
                    :column-name="column.columnName"
 | 
			
		||||
                    :disabled="column.isIdentity"
 | 
			
		||||
                />
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
        </el-form>
 | 
			
		||||
        <template #footer>
 | 
			
		||||
            <span class="dialog-footer">
 | 
			
		||||
                <el-button @click="closeDialog">取消</el-button>
 | 
			
		||||
                <el-button type="primary" @click="confirm">确定</el-button>
 | 
			
		||||
            </span>
 | 
			
		||||
        </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch, onMounted } from 'vue';
 | 
			
		||||
import ColumnFormItem from './ColumnFormItem.vue';
 | 
			
		||||
import { DbInst } from '../../db';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
export interface ColumnFormItemProps {
 | 
			
		||||
    dbInst: DbInst;
 | 
			
		||||
    dbName: string;
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    columns: any[];
 | 
			
		||||
    title?: string; // dialog title
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
 | 
			
		||||
    title: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const modelValue = defineModel<any>('modelValue');
 | 
			
		||||
 | 
			
		||||
const visible = defineModel<boolean>('visible', {
 | 
			
		||||
    default: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['submitSuccess']);
 | 
			
		||||
 | 
			
		||||
const dataForm: any = ref(null);
 | 
			
		||||
 | 
			
		||||
let oldValue = null as any;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    setOldValue();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(visible, (newValue) => {
 | 
			
		||||
    if (newValue) {
 | 
			
		||||
        setOldValue();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const setOldValue = () => {
 | 
			
		||||
    // 空对象则为insert操作,否则为update
 | 
			
		||||
    if (Object.keys(modelValue.value).length > 0) {
 | 
			
		||||
        oldValue = Object.assign({}, modelValue.value);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeDialog = () => {
 | 
			
		||||
    visible.value = false;
 | 
			
		||||
    modelValue.value = {};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirm = async () => {
 | 
			
		||||
    dataForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            ElMessage.error('请正确填写数据信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const dbInst = props.dbInst;
 | 
			
		||||
        const data = modelValue.value;
 | 
			
		||||
        const db = props.dbName;
 | 
			
		||||
        const tableName = props.tableName;
 | 
			
		||||
 | 
			
		||||
        let sql = '';
 | 
			
		||||
        if (oldValue) {
 | 
			
		||||
            const updateColumnValue = {};
 | 
			
		||||
            Object.keys(oldValue).forEach((key) => {
 | 
			
		||||
                // 如果新旧值不相等,则为需要更新的字段
 | 
			
		||||
                if (oldValue[key] !== modelValue.value[key]) {
 | 
			
		||||
                    updateColumnValue[key] = modelValue.value[key];
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
 | 
			
		||||
        } else {
 | 
			
		||||
            sql = await dbInst.genInsertSql(db, tableName, [data], true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        dbInst.promptExeSql(db, sql, null, () => {
 | 
			
		||||
            closeDialog();
 | 
			
		||||
            emit('submitSuccess');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -12,15 +12,29 @@
 | 
			
		||||
                        width="auto"
 | 
			
		||||
                        title="表格字段配置"
 | 
			
		||||
                        trigger="click"
 | 
			
		||||
                        @hide="triggerCheckedColumns"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div v-for="(item, index) in columns" :key="index">
 | 
			
		||||
                        <div><el-input v-model="checkedShowColumns.searchKey" size="small" placeholder="输入列名或备注过滤" /></div>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <el-checkbox
 | 
			
		||||
                                v-model="item.show"
 | 
			
		||||
                                :label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
 | 
			
		||||
                                :true-label="true"
 | 
			
		||||
                                :false-label="false"
 | 
			
		||||
                                v-model="checkedShowColumns.checkedAllColumn"
 | 
			
		||||
                                :indeterminate="checkedShowColumns.isIndeterminate"
 | 
			
		||||
                                @change="handleCheckAllColumnChange"
 | 
			
		||||
                                size="small"
 | 
			
		||||
                            />
 | 
			
		||||
                            >
 | 
			
		||||
                                选择所有
 | 
			
		||||
                            </el-checkbox>
 | 
			
		||||
 | 
			
		||||
                            <el-checkbox-group v-model="checkedShowColumns.columnNames" @change="handleCheckedColumnChange">
 | 
			
		||||
                                <div v-for="(item, index) in filterCheckedColumns" :key="index">
 | 
			
		||||
                                    <el-checkbox
 | 
			
		||||
                                        :key="index"
 | 
			
		||||
                                        :label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
 | 
			
		||||
                                        :value="item.columnName"
 | 
			
		||||
                                        size="small"
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </el-checkbox-group>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <template #reference>
 | 
			
		||||
                            <el-link icon="Operation" size="small" :underline="false"></el-link>
 | 
			
		||||
@@ -55,7 +69,7 @@
 | 
			
		||||
                        title="展示配置"
 | 
			
		||||
                        trigger="click"
 | 
			
		||||
                    >
 | 
			
		||||
                        <el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-label="true" :false-label="false" size="small" />
 | 
			
		||||
                        <el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
 | 
			
		||||
                        <template #reference>
 | 
			
		||||
                            <el-link type="primary" icon="setting" :underline="false"></el-link>
 | 
			
		||||
                        </template>
 | 
			
		||||
@@ -205,8 +219,8 @@
 | 
			
		||||
            </el-col>
 | 
			
		||||
        </el-row>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
 | 
			
		||||
            <el-row>
 | 
			
		||||
        <el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="460px">
 | 
			
		||||
            <el-row gutter="5">
 | 
			
		||||
                <el-col :span="5">
 | 
			
		||||
                    <el-select v-model="conditionDialog.condition">
 | 
			
		||||
                        <el-option label="=" value="="> </el-option>
 | 
			
		||||
@@ -234,32 +248,16 @@
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
 | 
			
		||||
            <el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
 | 
			
		||||
                <el-form-item
 | 
			
		||||
                    v-for="column in columns"
 | 
			
		||||
                    :key="column.columnName"
 | 
			
		||||
                    class="w100 mb5"
 | 
			
		||||
                    :prop="column.columnName"
 | 
			
		||||
                    :label="column.columnName"
 | 
			
		||||
                    :required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
 | 
			
		||||
                >
 | 
			
		||||
                    <ColumnFormItem
 | 
			
		||||
                        v-model="addDataDialog.data[`${column.columnName}`]"
 | 
			
		||||
                        :data-type="dbDialect.getDataType(column.columnType)"
 | 
			
		||||
                        :placeholder="`${column.columnType}  ${column.columnComment}`"
 | 
			
		||||
                        :column-name="column.columnName"
 | 
			
		||||
                        :disabled="column.isIdentity"
 | 
			
		||||
                    />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </el-form>
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <span class="dialog-footer">
 | 
			
		||||
                    <el-button @click="closeAddDataDialog">取消</el-button>
 | 
			
		||||
                    <el-button type="primary" @click="addRow">确定</el-button>
 | 
			
		||||
                </span>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
        <DbTableDataForm
 | 
			
		||||
            :db-inst="getNowDbInst()"
 | 
			
		||||
            :db-name="dbName"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
            :title="addDataDialog.title"
 | 
			
		||||
            :table-name="tableName"
 | 
			
		||||
            v-model:visible="addDataDialog.visible"
 | 
			
		||||
            v-model="addDataDialog.data"
 | 
			
		||||
            @submit-success="onRefresh"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -269,11 +267,11 @@ import { ElMessage } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
import { DbInst } from '@/views/ops/db/db';
 | 
			
		||||
import DbTableData from './DbTableData.vue';
 | 
			
		||||
import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import { DbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import ColumnFormItem from './ColumnFormItem.vue';
 | 
			
		||||
import { useEventListener, useStorage } from '@vueuse/core';
 | 
			
		||||
import { copyToClipboard } from '@/common/utils/string';
 | 
			
		||||
import DbTableDataForm from './DbTableDataForm.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
@@ -294,7 +292,6 @@ const props = defineProps({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const dataForm: any = ref(null);
 | 
			
		||||
const dbTableRef: Ref = ref(null);
 | 
			
		||||
const condInputRef: Ref = ref(null);
 | 
			
		||||
const columnNameSearchInputRef: Ref = ref(null);
 | 
			
		||||
@@ -341,15 +338,22 @@ const state = reactive({
 | 
			
		||||
    addDataDialog: {
 | 
			
		||||
        data: {},
 | 
			
		||||
        title: '',
 | 
			
		||||
        placeholder: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
    },
 | 
			
		||||
    tableHeight: '600px',
 | 
			
		||||
    hasUpdatedFileds: false,
 | 
			
		||||
    dbDialect: {} as DbDialect,
 | 
			
		||||
 | 
			
		||||
    checkedShowColumns: {
 | 
			
		||||
        searchKey: '',
 | 
			
		||||
        checkedAllColumn: true,
 | 
			
		||||
        isIndeterminate: false,
 | 
			
		||||
        columnNames: [] as any,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
 | 
			
		||||
const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
 | 
			
		||||
    toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.tableHeight,
 | 
			
		||||
@@ -367,8 +371,10 @@ onMounted(async () => {
 | 
			
		||||
    state.tableHeight = props.tableHeight;
 | 
			
		||||
    await onRefresh();
 | 
			
		||||
 | 
			
		||||
    state.dbDialect = getDbDialect(getNowDbInst().type);
 | 
			
		||||
    state.dbDialect = getNowDbInst().getDialect();
 | 
			
		||||
    useEventListener('click', handlerWindowClick);
 | 
			
		||||
 | 
			
		||||
    state.checkedShowColumns.columnNames = state.columns.map((item: any) => item.columnName);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handlerWindowClick = () => {
 | 
			
		||||
@@ -432,6 +438,7 @@ const handleSetPageNum = async () => {
 | 
			
		||||
    state.pageNum = state.setPageNum;
 | 
			
		||||
    await selectData();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCount = async () => {
 | 
			
		||||
    state.counting = true;
 | 
			
		||||
 | 
			
		||||
@@ -449,6 +456,24 @@ const handleCount = async () => {
 | 
			
		||||
    state.counting = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCheckAllColumnChange = (val: boolean) => {
 | 
			
		||||
    state.checkedShowColumns.columnNames = val ? state.columns.map((x: any) => x.columnName) : [];
 | 
			
		||||
    state.checkedShowColumns.isIndeterminate = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCheckedColumnChange = (value: string[]) => {
 | 
			
		||||
    const checkedCount = value.length;
 | 
			
		||||
    state.checkedShowColumns.checkedAllColumn = checkedCount === state.columns.length;
 | 
			
		||||
    state.checkedShowColumns.isIndeterminate = checkedCount > 0 && checkedCount < state.columns.length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const triggerCheckedColumns = () => {
 | 
			
		||||
    const checkedColumnNames = state.checkedShowColumns.columnNames;
 | 
			
		||||
    for (let column of state.columns) {
 | 
			
		||||
        column.show = checkedColumnNames.includes(column.columnName);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
 | 
			
		||||
let completeCond = '';
 | 
			
		||||
// 是否存在列建议
 | 
			
		||||
@@ -481,7 +506,7 @@ const handlerColumnSelect = (column: any) => {
 | 
			
		||||
    // 默认拼接上 columnName =
 | 
			
		||||
    let value = column.columnName + ' = ';
 | 
			
		||||
    // 不是数字类型默认拼接上''
 | 
			
		||||
    if (!DbInst.isNumber(column.columnType)) {
 | 
			
		||||
    if (!DbInst.isNumber(column.dataType)) {
 | 
			
		||||
        value = `${value} ''`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -508,16 +533,23 @@ const chooseCondColumnName = () => {
 | 
			
		||||
 * 过滤条件列名
 | 
			
		||||
 */
 | 
			
		||||
const filterCondColumns = computed(() => {
 | 
			
		||||
    return filterColumns(state.columnNameSearch);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterCheckedColumns = computed(() => {
 | 
			
		||||
    return filterColumns(state.checkedShowColumns.searchKey);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterColumns = (searchKey: string) => {
 | 
			
		||||
    const columns = state.columns;
 | 
			
		||||
    let columnNameSearch = state.columnNameSearch;
 | 
			
		||||
    if (!columnNameSearch) {
 | 
			
		||||
    if (!searchKey) {
 | 
			
		||||
        return columns;
 | 
			
		||||
    }
 | 
			
		||||
    columnNameSearch = columnNameSearch.toLowerCase();
 | 
			
		||||
    searchKey = searchKey.toLowerCase();
 | 
			
		||||
    return columns.filter((data: any) => {
 | 
			
		||||
        return data.columnName.toLowerCase().includes(columnNameSearch) || data.columnComment.toLowerCase().includes(columnNameSearch);
 | 
			
		||||
        return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 条件查询,点击列信息后显示输入对应的值
 | 
			
		||||
@@ -542,7 +574,7 @@ const onConfirmCondition = () => {
 | 
			
		||||
    }
 | 
			
		||||
    const row = conditionDialog.columnRow as any;
 | 
			
		||||
    condition += `${row.columnName} ${conditionDialog.condition} `;
 | 
			
		||||
    state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
 | 
			
		||||
    state.condition = condition + state.dbDialect.wrapValue(row.dataType, conditionDialog.value!);
 | 
			
		||||
    onCancelCondition();
 | 
			
		||||
    condInputRef.value.focus();
 | 
			
		||||
};
 | 
			
		||||
@@ -601,46 +633,6 @@ const onShowAddDataDialog = async () => {
 | 
			
		||||
    state.addDataDialog.title = `添加'${props.tableName}'表数据`;
 | 
			
		||||
    state.addDataDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeAddDataDialog = () => {
 | 
			
		||||
    state.addDataDialog.visible = false;
 | 
			
		||||
    state.addDataDialog.data = {};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 添加新数据行
 | 
			
		||||
const addRow = async () => {
 | 
			
		||||
    dataForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (valid) {
 | 
			
		||||
            const dbInst = getNowDbInst();
 | 
			
		||||
            const data = state.addDataDialog.data;
 | 
			
		||||
            // key: 字段名,value: 字段名提示
 | 
			
		||||
            let obj: any = {};
 | 
			
		||||
            for (let item of state.columns) {
 | 
			
		||||
                const value = data[item.columnName];
 | 
			
		||||
                if (!value) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
 | 
			
		||||
            }
 | 
			
		||||
            let columnNames = Object.keys(obj).join(',');
 | 
			
		||||
            let values = Object.values(obj).join(',');
 | 
			
		||||
            // 获取schema
 | 
			
		||||
            let schema = '';
 | 
			
		||||
            let arr = props.dbName?.split('/');
 | 
			
		||||
            if (arr && arr.length > 1) {
 | 
			
		||||
                schema = dbInst.wrapName(arr[1]) + '.';
 | 
			
		||||
            }
 | 
			
		||||
            let sql = `INSERT INTO ${schema}${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
 | 
			
		||||
            dbInst.promptExeSql(props.dbName, sql, null, () => {
 | 
			
		||||
                closeAddDataDialog();
 | 
			
		||||
                onRefresh();
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            ElMessage.error('请正确填写数据信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
 | 
			
		||||
                                    <el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
 | 
			
		||||
                                        <el-option
 | 
			
		||||
                                            v-for="pgsqlType in state.columnTypeList"
 | 
			
		||||
                                            v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
 | 
			
		||||
                                            :key="pgsqlType.dataType"
 | 
			
		||||
                                            :value="pgsqlType.udtName"
 | 
			
		||||
                                            :label="pgsqlType.dataType"
 | 
			
		||||
@@ -131,6 +131,7 @@ import { reactive, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import SqlExecBox from '../sqleditor/SqlExecBox';
 | 
			
		||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
 | 
			
		||||
import { DbInst } from '../../db';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -151,6 +152,9 @@ const props = defineProps({
 | 
			
		||||
    dbType: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    flowProcdef: {
 | 
			
		||||
        type: Object,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
@@ -169,7 +173,6 @@ const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    btnloading: false,
 | 
			
		||||
    activeName: '1',
 | 
			
		||||
    columnTypeList: dbDialect.getInfo().columnTypes,
 | 
			
		||||
    tableData: {
 | 
			
		||||
        fields: {
 | 
			
		||||
            colNames: [
 | 
			
		||||
@@ -190,7 +193,7 @@ const state = reactive({
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'numScale',
 | 
			
		||||
                    label: '小数点',
 | 
			
		||||
                    label: '小数精度',
 | 
			
		||||
                    width: 120,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@@ -260,6 +263,8 @@ const state = reactive({
 | 
			
		||||
        },
 | 
			
		||||
        tableName: '',
 | 
			
		||||
        tableComment: '',
 | 
			
		||||
        oldTableName: '',
 | 
			
		||||
        oldTableComment: '',
 | 
			
		||||
        height: 450,
 | 
			
		||||
        db: '',
 | 
			
		||||
    },
 | 
			
		||||
@@ -272,6 +277,18 @@ watch(props, async (newValue) => {
 | 
			
		||||
    dbDialect = getDbDialect(newValue.dbType);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 切换到索引tab时,刷新索引字段下拉选项
 | 
			
		||||
watch(
 | 
			
		||||
    () => state.activeName,
 | 
			
		||||
    (newValue) => {
 | 
			
		||||
        if (newValue === '2') {
 | 
			
		||||
            state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
 | 
			
		||||
                return { name: a.name, remark: a.remark };
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    reset();
 | 
			
		||||
@@ -318,6 +335,7 @@ const submit = async () => {
 | 
			
		||||
        dbId: props.dbId as any,
 | 
			
		||||
        db: props.db as any,
 | 
			
		||||
        dbType: dbDialect.getInfo().formatSqlDialect,
 | 
			
		||||
        flowProcdef: props.flowProcdef,
 | 
			
		||||
        runSuccessCallback: () => {
 | 
			
		||||
            emit('submit-sql', { tableName: state.tableData.tableName });
 | 
			
		||||
            // cancel();
 | 
			
		||||
@@ -331,22 +349,25 @@ const submit = async () => {
 | 
			
		||||
 * @param nowArr 修改后的对象数组
 | 
			
		||||
 * @param key 标志对象唯一属性
 | 
			
		||||
 */
 | 
			
		||||
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[]; add: any[]; upd: any[] } => {
 | 
			
		||||
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[]; add: any[]; upd: any[]; changed: boolean } => {
 | 
			
		||||
    let data = {
 | 
			
		||||
        del: [] as object[], // 删除的数据
 | 
			
		||||
        add: [] as object[], // 新增的数据
 | 
			
		||||
        upd: [] as object[], // 修改的数据
 | 
			
		||||
        changed: false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 旧数据为空
 | 
			
		||||
    if (oldArr && Array.isArray(oldArr) && oldArr.length === 0 && nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
 | 
			
		||||
        data.add = nowArr;
 | 
			
		||||
        data.changed = true;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 新数据为空
 | 
			
		||||
    if (nowArr && Array.isArray(nowArr) && nowArr.length === 0 && oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
 | 
			
		||||
        data.del = oldArr;
 | 
			
		||||
        data.changed = true;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -362,6 +383,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
        oldName && (newMap[oldName] = a);
 | 
			
		||||
        if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
 | 
			
		||||
            // 新增
 | 
			
		||||
            data.changed = true;
 | 
			
		||||
            data.add.push(a);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
@@ -371,6 +393,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
        let newData = newMap[k];
 | 
			
		||||
        if (!newData) {
 | 
			
		||||
            // 删除
 | 
			
		||||
            data.changed = true;
 | 
			
		||||
            data.del.push(a);
 | 
			
		||||
        } else {
 | 
			
		||||
            // 判断每个字段是否相等,否则为修改
 | 
			
		||||
@@ -378,6 +401,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
                let oldV = a[f];
 | 
			
		||||
                let newV = newData[f];
 | 
			
		||||
                if (oldV?.toString() !== newV?.toString()) {
 | 
			
		||||
                    data.changed = true;
 | 
			
		||||
                    data.upd.push(newData);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
@@ -391,22 +415,28 @@ const genSql = () => {
 | 
			
		||||
    let data = state.tableData;
 | 
			
		||||
    // 创建表
 | 
			
		||||
    if (!props.data?.edit) {
 | 
			
		||||
        if (state.activeName === '1') {
 | 
			
		||||
            return dbDialect.getCreateTableSql(data);
 | 
			
		||||
        } else if (state.activeName === '2' && data.indexs.res.length > 0) {
 | 
			
		||||
            return dbDialect.getCreateIndexSql(data);
 | 
			
		||||
        let createTable = dbDialect.getCreateTableSql(data);
 | 
			
		||||
        let createIndex = '';
 | 
			
		||||
        if (data.indexs.res.length > 0) {
 | 
			
		||||
            createIndex = dbDialect.getCreateIndexSql(data);
 | 
			
		||||
        }
 | 
			
		||||
        return createTable + ';' + createIndex;
 | 
			
		||||
    } else {
 | 
			
		||||
        // 修改
 | 
			
		||||
        if (state.activeName === '1') {
 | 
			
		||||
            // 修改列
 | 
			
		||||
            let changeData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
 | 
			
		||||
            return dbDialect.getModifyColumnSql(data, data.tableName, changeData);
 | 
			
		||||
        } else if (state.activeName === '2') {
 | 
			
		||||
            // 修改索引
 | 
			
		||||
            let changeData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
 | 
			
		||||
            return dbDialect.getModifyIndexSql(data, data.tableName, changeData);
 | 
			
		||||
        }
 | 
			
		||||
        // 修改列
 | 
			
		||||
        let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
 | 
			
		||||
        let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
 | 
			
		||||
        // 修改索引
 | 
			
		||||
        let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
 | 
			
		||||
        let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
 | 
			
		||||
        // 修改表名,表注释
 | 
			
		||||
        let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
 | 
			
		||||
 | 
			
		||||
        let sqlArr = [];
 | 
			
		||||
        colSql && sqlArr.push(colSql);
 | 
			
		||||
        idxSql && sqlArr.push(idxSql);
 | 
			
		||||
        tableInfoSql && sqlArr.push(tableInfoSql);
 | 
			
		||||
 | 
			
		||||
        return sqlArr.join(';');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -416,7 +446,9 @@ const reset = () => {
 | 
			
		||||
    state.tableData.tableName = '';
 | 
			
		||||
    state.tableData.tableComment = '';
 | 
			
		||||
    state.tableData.fields.res = [];
 | 
			
		||||
    state.tableData.fields.oldFields = [];
 | 
			
		||||
    state.tableData.indexs.res = [];
 | 
			
		||||
    state.tableData.indexs.oldIndexs = [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const indexChanges = (row: any) => {
 | 
			
		||||
@@ -459,17 +491,20 @@ watch(
 | 
			
		||||
        // 回显表名表注释
 | 
			
		||||
        state.tableData.tableName = row.tableName;
 | 
			
		||||
        state.tableData.tableComment = row.tableComment;
 | 
			
		||||
        state.tableData.oldTableName = row.tableName;
 | 
			
		||||
        state.tableData.oldTableComment = row.tableComment;
 | 
			
		||||
        state.tableData.db = props.db!;
 | 
			
		||||
 | 
			
		||||
        state.tableData.fields.oldFields = [];
 | 
			
		||||
        state.tableData.fields.res = [];
 | 
			
		||||
        state.tableData.indexs.oldIndexs = [];
 | 
			
		||||
        state.tableData.indexs.res = [];
 | 
			
		||||
        // 索引列下拉选
 | 
			
		||||
        state.tableData.indexs.columns = [];
 | 
			
		||||
        DbInst.initColumns(columns);
 | 
			
		||||
        // 回显列
 | 
			
		||||
        if (columns && Array.isArray(columns) && columns.length > 0) {
 | 
			
		||||
            state.tableData.fields.oldFields = [];
 | 
			
		||||
            state.tableData.fields.res = [];
 | 
			
		||||
            // 索引列下拉选
 | 
			
		||||
            state.tableData.indexs.columns = [];
 | 
			
		||||
            columns.forEach((a) => {
 | 
			
		||||
                let typeObj = a.columnType.replace(')', '').split('(');
 | 
			
		||||
                let type = typeObj[0];
 | 
			
		||||
                let length = (typeObj.length > 1 && typeObj[1]) || '';
 | 
			
		||||
                let defaultValue = '';
 | 
			
		||||
                if (a.columnDefault) {
 | 
			
		||||
                    defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
 | 
			
		||||
@@ -479,11 +514,11 @@ watch(
 | 
			
		||||
                let data = {
 | 
			
		||||
                    name: a.columnName,
 | 
			
		||||
                    oldName: a.columnName,
 | 
			
		||||
                    type,
 | 
			
		||||
                    type: a.dataType,
 | 
			
		||||
                    value: defaultValue,
 | 
			
		||||
                    length,
 | 
			
		||||
                    numScale: a.numScale,
 | 
			
		||||
                    notNull: a.nullable !== 'YES',
 | 
			
		||||
                    length: a.showLength,
 | 
			
		||||
                    numScale: a.showScale,
 | 
			
		||||
                    notNull: !a.nullable,
 | 
			
		||||
                    pri: a.isPrimaryKey,
 | 
			
		||||
                    auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
 | 
			
		||||
                    remark: a.columnComment,
 | 
			
		||||
@@ -494,10 +529,9 @@ watch(
 | 
			
		||||
                state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 回显索引
 | 
			
		||||
        if (indexs && Array.isArray(indexs) && indexs.length > 0) {
 | 
			
		||||
            state.tableData.indexs.oldIndexs = [];
 | 
			
		||||
            state.tableData.indexs.res = [];
 | 
			
		||||
            // 索引过滤掉主键
 | 
			
		||||
            indexs
 | 
			
		||||
                .filter((a) => a.indexName !== 'PRIMARY')
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,9 @@
 | 
			
		||||
                </template>
 | 
			
		||||
                <el-form-item label="导出内容: ">
 | 
			
		||||
                    <el-radio-group v-model="dumpInfo.type">
 | 
			
		||||
                        <el-radio :label="1" size="small">结构</el-radio>
 | 
			
		||||
                        <el-radio :label="2" size="small">数据</el-radio>
 | 
			
		||||
                        <el-radio :label="3" size="small">结构+数据</el-radio>
 | 
			
		||||
                        <el-radio :value="1" size="small">结构</el-radio>
 | 
			
		||||
                        <el-radio :value="2" size="small">数据</el-radio>
 | 
			
		||||
                        <el-radio :value="3" size="small">结构+数据</el-radio>
 | 
			
		||||
                    </el-radio-group>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
@@ -98,8 +98,8 @@
 | 
			
		||||
            </el-table>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
 | 
			
		||||
            <el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
 | 
			
		||||
        <el-dialog width="55%" :title="`'${chooseTableName}' DDL`" v-model="ddlDialog.visible">
 | 
			
		||||
            <monaco-editor height="400px" language="sql" v-model="ddlDialog.ddl" :options="{ readOnly: true }" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <db-table-op
 | 
			
		||||
@@ -108,6 +108,7 @@
 | 
			
		||||
            :dbId="dbId"
 | 
			
		||||
            :db="db"
 | 
			
		||||
            :dbType="dbType"
 | 
			
		||||
            :flow-procdef="props.flowProcdef"
 | 
			
		||||
            :data="tableCreateDialog.data"
 | 
			
		||||
            v-model:visible="tableCreateDialog.visible"
 | 
			
		||||
            @submit-sql="onSubmitSql"
 | 
			
		||||
@@ -125,7 +126,10 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
 | 
			
		||||
import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index';
 | 
			
		||||
import { DbInst } from '../../db';
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { format as sqlFormatter } from 'sql-formatter';
 | 
			
		||||
 | 
			
		||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -146,6 +150,9 @@ const props = defineProps({
 | 
			
		||||
        type: [String],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    flowProcdef: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -271,11 +278,13 @@ const dump = (db: string) => {
 | 
			
		||||
 | 
			
		||||
const showColumns = async (row: any) => {
 | 
			
		||||
    state.chooseTableName = row.tableName;
 | 
			
		||||
    state.columnDialog.columns = await dbApi.columnMetadata.request({
 | 
			
		||||
    const columns = await dbApi.columnMetadata.request({
 | 
			
		||||
        id: props.dbId,
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        tableName: row.tableName,
 | 
			
		||||
    });
 | 
			
		||||
    DbInst.initColumns(columns);
 | 
			
		||||
    state.columnDialog.columns = columns;
 | 
			
		||||
 | 
			
		||||
    state.columnDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
@@ -298,7 +307,8 @@ const showCreateDdl = async (row: any) => {
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        tableName: row.tableName,
 | 
			
		||||
    });
 | 
			
		||||
    state.ddlDialog.ddl = res;
 | 
			
		||||
 | 
			
		||||
    state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(props.dbType).getInfo().formatSqlDialect as any });
 | 
			
		||||
    state.ddlDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -317,6 +327,7 @@ const dropTable = async (row: any) => {
 | 
			
		||||
            sql: `DROP TABLE ${tableName}`,
 | 
			
		||||
            dbId: props.dbId as any,
 | 
			
		||||
            db: props.db as any,
 | 
			
		||||
            flowProcdef: props.flowProcdef,
 | 
			
		||||
            runSuccessCallback: async () => {
 | 
			
		||||
                await getTables();
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,11 @@ export class DbInst {
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程定义,若存在则需要审批执行
 | 
			
		||||
     */
 | 
			
		||||
    flowProcdef: any;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * dbName -> db
 | 
			
		||||
     */
 | 
			
		||||
@@ -74,6 +79,11 @@ export class DbInst {
 | 
			
		||||
        return db;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 获取数据库实例方言
 | 
			
		||||
    getDialect(): DbDialect {
 | 
			
		||||
        return getDbDialect(this.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 加载数据库表信息
 | 
			
		||||
     * @param dbName 数据库名
 | 
			
		||||
@@ -156,7 +166,7 @@ export class DbInst {
 | 
			
		||||
        const db = this.getDb(dbName);
 | 
			
		||||
        // 优先从 table map中获取
 | 
			
		||||
        let columns = db.getColumns(table);
 | 
			
		||||
        if (columns) {
 | 
			
		||||
        if (columns && columns.length > 0) {
 | 
			
		||||
            return columns;
 | 
			
		||||
        }
 | 
			
		||||
        console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
 | 
			
		||||
@@ -165,6 +175,9 @@ export class DbInst {
 | 
			
		||||
            db: dbName,
 | 
			
		||||
            tableName: table,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        DbInst.initColumns(columns);
 | 
			
		||||
 | 
			
		||||
        db.columnsMap.set(table, columns);
 | 
			
		||||
        return columns;
 | 
			
		||||
    }
 | 
			
		||||
@@ -248,7 +261,7 @@ export class DbInst {
 | 
			
		||||
 | 
			
		||||
    // 获取指定表的默认查询sql
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
 | 
			
		||||
        return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
 | 
			
		||||
        return this.getDialect().getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -256,12 +269,22 @@ export class DbInst {
 | 
			
		||||
     * @param dbName 数据库名
 | 
			
		||||
     * @param table 表名
 | 
			
		||||
     * @param datas 要生成的数据
 | 
			
		||||
     * @param dbDialect db方言
 | 
			
		||||
     * @param skipNull 是否跳过空字段
 | 
			
		||||
     */
 | 
			
		||||
    async genInsertSql(dbName: string, table: string, datas: any[]) {
 | 
			
		||||
    async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
 | 
			
		||||
        if (!datas) {
 | 
			
		||||
            return '';
 | 
			
		||||
        }
 | 
			
		||||
        let schema = '';
 | 
			
		||||
        let arr = dbName.split('/');
 | 
			
		||||
        if (arr.length == 1) {
 | 
			
		||||
            schema = this.wrapName(dbName) + '.';
 | 
			
		||||
        } else if (arr.length == 2) {
 | 
			
		||||
            schema = this.wrapName(arr[1]) + '.';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let dbDialect = this.getDialect();
 | 
			
		||||
        const columns = await this.loadColumns(dbName, table);
 | 
			
		||||
        const sqls = [];
 | 
			
		||||
        for (let data of datas) {
 | 
			
		||||
@@ -269,23 +292,59 @@ export class DbInst {
 | 
			
		||||
            let values = [];
 | 
			
		||||
            for (let column of columns) {
 | 
			
		||||
                const colName = column.columnName;
 | 
			
		||||
                if (skipNull && data[colName] == null) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                colNames.push(this.wrapName(colName));
 | 
			
		||||
                values.push(DbInst.wrapValueByType(data[colName]));
 | 
			
		||||
                values.push(dbDialect.wrapValue(column.dataType, data[colName]));
 | 
			
		||||
            }
 | 
			
		||||
            sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
 | 
			
		||||
            sqls.push(`INSERT INTO ${schema}${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
 | 
			
		||||
        }
 | 
			
		||||
        return sqls.join(';\n') + ';';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成根据主键更新语句
 | 
			
		||||
     * @param dbName 数据库名
 | 
			
		||||
     * @param table 表名
 | 
			
		||||
     * @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
 | 
			
		||||
     * @param rowData 表的一行完整数据(需要获取主键信息)
 | 
			
		||||
     */
 | 
			
		||||
    async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
 | 
			
		||||
        let schema = '';
 | 
			
		||||
        let dbArr = dbName.split('/');
 | 
			
		||||
        if (dbArr.length == 2) {
 | 
			
		||||
            schema = this.wrapName(dbArr[1]) + '.';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
 | 
			
		||||
        // 主键列信息
 | 
			
		||||
        const primaryKey = await this.loadTableColumn(dbName, table);
 | 
			
		||||
        let primaryKeyType = primaryKey.dataType;
 | 
			
		||||
        let primaryKeyName = primaryKey.columnName;
 | 
			
		||||
        let primaryKeyValue = rowData[primaryKeyName];
 | 
			
		||||
        const dialect = this.getDialect();
 | 
			
		||||
        for (let k of Object.keys(columnValue)) {
 | 
			
		||||
            const v = columnValue[k];
 | 
			
		||||
            // 更新字段列信息
 | 
			
		||||
            const updateColumn = await this.loadTableColumn(dbName, table, k);
 | 
			
		||||
            sql += ` ${this.wrapName(k)} = ${dialect.wrapValue(updateColumn.dataType, v)},`;
 | 
			
		||||
        }
 | 
			
		||||
        sql = sql.substring(0, sql.length - 1);
 | 
			
		||||
 | 
			
		||||
        return sql + ` WHERE ${this.wrapName(primaryKeyName)} = ${this.getDialect().wrapValue(primaryKeyType, primaryKeyValue)} ;`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成根据主键删除的sql语句
 | 
			
		||||
     * @param db 数据库名
 | 
			
		||||
     * @param table 表名
 | 
			
		||||
     * @param datas 要删除的记录
 | 
			
		||||
     */
 | 
			
		||||
    async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
 | 
			
		||||
        const primaryKey = await this.loadTableColumn(db, table);
 | 
			
		||||
        const primaryKeyColumnName = primaryKey.columnName;
 | 
			
		||||
        const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
 | 
			
		||||
        const ids = datas.map((d: any) => `${this.getDialect().wrapValue(primaryKey.dataType, d[primaryKeyColumnName])}`).join(',');
 | 
			
		||||
        return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -293,24 +352,25 @@ export class DbInst {
 | 
			
		||||
     * 弹框提示是否执行sql
 | 
			
		||||
     */
 | 
			
		||||
    promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
 | 
			
		||||
        console.log(this);
 | 
			
		||||
        SqlExecBox({
 | 
			
		||||
            sql,
 | 
			
		||||
            dbId: this.id,
 | 
			
		||||
            db,
 | 
			
		||||
            dbType: getDbDialect(this.type).getInfo().formatSqlDialect,
 | 
			
		||||
            dbType: this.getDialect().getInfo().formatSqlDialect,
 | 
			
		||||
            runSuccessCallback: successFunc,
 | 
			
		||||
            cancelCallback: cancelFunc,
 | 
			
		||||
            flowProcdef: this.flowProcdef,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
 | 
			
		||||
     * @param table
 | 
			
		||||
     * @param condition
 | 
			
		||||
     * @returns
 | 
			
		||||
     * @param name 表名、字段名、schema名
 | 
			
		||||
     * @returns 包裹后的字符串
 | 
			
		||||
     */
 | 
			
		||||
    wrapName = (name: string) => {
 | 
			
		||||
        return getDbDialect(this.type).quoteIdentifier(name);
 | 
			
		||||
        return this.getDialect().quoteIdentifier(name);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -324,6 +384,11 @@ export class DbInst {
 | 
			
		||||
        }
 | 
			
		||||
        let dbInst = dbInstCache.get(inst.id);
 | 
			
		||||
        if (dbInst) {
 | 
			
		||||
            // 更新可能更改的流程定义
 | 
			
		||||
            if (inst.flowProcdef !== undefined) {
 | 
			
		||||
                dbInst.flowProcdef = inst.flowProcdef;
 | 
			
		||||
                dbInstCache.set(dbInst.id, dbInst);
 | 
			
		||||
            }
 | 
			
		||||
            return dbInst;
 | 
			
		||||
        }
 | 
			
		||||
        console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
 | 
			
		||||
@@ -334,6 +399,7 @@ export class DbInst {
 | 
			
		||||
        dbInst.name = inst.name;
 | 
			
		||||
        dbInst.type = inst.type;
 | 
			
		||||
        dbInst.databases = inst.databases;
 | 
			
		||||
        dbInst.flowProcdef = inst.flowProcdef;
 | 
			
		||||
 | 
			
		||||
        dbInstCache.set(dbInst.id, dbInst);
 | 
			
		||||
        return dbInst;
 | 
			
		||||
@@ -363,41 +429,13 @@ export class DbInst {
 | 
			
		||||
        dbInstCache.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据返回值包装值,若值为字符串类型则添加''
 | 
			
		||||
     * @param val 值
 | 
			
		||||
     * @returns 包装后的值
 | 
			
		||||
     */
 | 
			
		||||
    static wrapValueByType = (val: any) => {
 | 
			
		||||
        if (val == null) {
 | 
			
		||||
            return 'NULL';
 | 
			
		||||
        }
 | 
			
		||||
        if (typeof val == 'number') {
 | 
			
		||||
            return val;
 | 
			
		||||
        }
 | 
			
		||||
        return `'${val}'`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可
 | 
			
		||||
     */
 | 
			
		||||
    static wrapColumnValue(columnType: string, value: any, dbDialect?: DbDialect) {
 | 
			
		||||
        if (this.isNumber(columnType)) {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dbDialect) {
 | 
			
		||||
            return `'${value}'`;
 | 
			
		||||
        }
 | 
			
		||||
        return dbDialect.wrapStrValue(columnType, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断字段类型是否为数字类型
 | 
			
		||||
     * @param columnType 字段类型
 | 
			
		||||
     * @returns
 | 
			
		||||
     */
 | 
			
		||||
    static isNumber(columnType: string) {
 | 
			
		||||
        return columnType.match(/int|double|float|number|decimal|byte|bit/gi);
 | 
			
		||||
        return columnType && columnType.match(/int|double|float|number|decimal|byte|bit/gi);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -437,6 +475,35 @@ export class DbInst {
 | 
			
		||||
        const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
 | 
			
		||||
        return flexWidth > 500 ? 500 : flexWidth;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 初始化所有列信息,完善需要显示的列类型,包含长度等,如varchar(20)
 | 
			
		||||
    static initColumns(columns: any[]) {
 | 
			
		||||
        if (!columns) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        for (let col of columns) {
 | 
			
		||||
            if (col.charMaxLength > 0) {
 | 
			
		||||
                col.columnType = `${col.dataType}(${col.charMaxLength})`;
 | 
			
		||||
                col.showLength = col.charMaxLength;
 | 
			
		||||
                col.showScale = null;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (col.numPrecision > 0) {
 | 
			
		||||
                if (col.numScale > 0) {
 | 
			
		||||
                    col.columnType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
 | 
			
		||||
                    col.showScale = col.numScale;
 | 
			
		||||
                } else {
 | 
			
		||||
                    col.columnType = `${col.dataType}(${col.numPrecision})`;
 | 
			
		||||
                    col.showScale = null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                col.showLength = col.numPrecision;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            col.columnType = col.dataType;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,10 @@ import {
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    QuoteEscape,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
    RowDefinition,
 | 
			
		||||
    sqlColumnType,
 | 
			
		||||
@@ -34,7 +36,6 @@ const DM_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
    // 位串数据类型 BIT 用于存储整数数据 1、0 或 NULL,只有 0 才转换为假,其他非空、非 0 值都会自动转换为真
 | 
			
		||||
    { udtName: 'BIT', dataType: 'BIT', desc: '用于存储整数数据 1、0 或 NULL', space: '1', range: '1' },
 | 
			
		||||
    // 一般日期时间数据类型 DATE TIME TIMESTAMP 默认精度 6
 | 
			
		||||
    // 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型  IMAGE/LONGVARBINARY 类型  BLOB CLOB BFILE  100G-1
 | 
			
		||||
    { udtName: 'DATE', dataType: 'DATE', desc: '年、月、日', space: '', range: '' },
 | 
			
		||||
    { udtName: 'TIME', dataType: 'TIME', desc: '时、分、秒', space: '', range: '' },
 | 
			
		||||
    {
 | 
			
		||||
@@ -44,6 +45,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
        space: '',
 | 
			
		||||
        range: '-4712-01-01 00:00:00.000000000 ~ 9999-12-31 23:59:59.999999999',
 | 
			
		||||
    },
 | 
			
		||||
    // 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型  IMAGE/LONGVARBINARY 类型  BLOB CLOB BFILE  100G-1
 | 
			
		||||
    { udtName: 'TEXT', dataType: 'TEXT', desc: '变长字符串', space: '', range: '100G-1' },
 | 
			
		||||
    { udtName: 'LONG', dataType: 'LONG', desc: '同TEXT', space: '', range: '100G-1' },
 | 
			
		||||
    { udtName: 'LONGVARCHAR', dataType: 'LONGVARCHAR', desc: '同TEXT', space: '', range: '100G-1' },
 | 
			
		||||
@@ -522,7 +524,7 @@ class DMDialect implements DbDialect {
 | 
			
		||||
            }
 | 
			
		||||
            // 列注释
 | 
			
		||||
            if (item.remark) {
 | 
			
		||||
                columCommentSql += ` comment on column "${data.tableName}"."${item.name}" is '${item.remark}'; `;
 | 
			
		||||
                columCommentSql += ` comment on column "${data.tableName}"."${item.name}" is '${QuoteEscape(item.remark)}'; `;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        // 建表
 | 
			
		||||
@@ -533,7 +535,7 @@ class DMDialect implements DbDialect {
 | 
			
		||||
                     );`;
 | 
			
		||||
        // 表注释
 | 
			
		||||
        if (data.tableComment) {
 | 
			
		||||
            tableCommentSql = ` comment on table "${data.tableName}" is '${data.tableComment}'; `;
 | 
			
		||||
            tableCommentSql = ` comment on table "${data.tableName}" is '${QuoteEscape(data.tableComment)}'; `;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return createSql + tableCommentSql + columCommentSql;
 | 
			
		||||
@@ -568,7 +570,7 @@ class DMDialect implements DbDialect {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
 | 
			
		||||
                    commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
 | 
			
		||||
                }
 | 
			
		||||
                if (a.pri) {
 | 
			
		||||
                    priArr.add(`"${a.name}"`);
 | 
			
		||||
@@ -578,7 +580,7 @@ class DMDialect implements DbDialect {
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
 | 
			
		||||
                let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
 | 
			
		||||
                if (a.remark && a.oldName === a.name) {
 | 
			
		||||
                    commentSql += cmtSql;
 | 
			
		||||
                }
 | 
			
		||||
@@ -662,6 +664,22 @@ class DMDialect implements DbDialect {
 | 
			
		||||
        }
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
 | 
			
		||||
        let sql = '';
 | 
			
		||||
 | 
			
		||||
        if (tableData.oldTableName !== tableData.tableName) {
 | 
			
		||||
            let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql += `ALTER TABLE ${baseTable} RENAME TO ${this.quoteIdentifier(tableData.tableName)};`;
 | 
			
		||||
        }
 | 
			
		||||
        if (tableData.oldTableComment !== tableData.tableComment) {
 | 
			
		||||
            let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
            sql += `COMMENT ON TABLE ${baseTable} IS '${QuoteEscape(tableData.tableComment)}';`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
@@ -682,8 +700,54 @@ class DMDialect implements DbDialect {
 | 
			
		||||
        return DataType.String;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
 | 
			
		||||
    wrapStrValue(columnType: string, value: string): string {
 | 
			
		||||
    wrapValue(columnType: string, value: any): any {
 | 
			
		||||
        if (value == null) {
 | 
			
		||||
            return 'NULL';
 | 
			
		||||
        }
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        return `'${value}'`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
 | 
			
		||||
        // 替换
 | 
			
		||||
        //  MERGE INTO t_person T1
 | 
			
		||||
        //   USING (
 | 
			
		||||
        //   <foreach collection="list" item="item" index="index" separator="UNION ALL">
 | 
			
		||||
        //   SELECT
 | 
			
		||||
        //   #{item.id} id,
 | 
			
		||||
        //   #{item.mc} mc,
 | 
			
		||||
        //   #{item.sex} sex,
 | 
			
		||||
        //   #{item.age} age
 | 
			
		||||
        //   FROM dual
 | 
			
		||||
        //   </foreach>
 | 
			
		||||
        // ) T2 ON (T1.id = T2.id )
 | 
			
		||||
        //   WHEN NOT MATCHED THEN INSERT(id, mc, sex,
 | 
			
		||||
        //   age) VALUES
 | 
			
		||||
        //   (T2.id, T2.mc, T2.sex, T2.age)
 | 
			
		||||
        //   WHEN MATCHED THEN UPDATE
 | 
			
		||||
        //   SET T1.mc = T2.mc,T1.sex = T2.sex,T1.age = T2.age
 | 
			
		||||
 | 
			
		||||
        if (duplicateStrategy == DuplicateStrategy.REPLACE || duplicateStrategy == DuplicateStrategy.IGNORE) {
 | 
			
		||||
            // 字段数组生成占位符sql
 | 
			
		||||
            let phs = [];
 | 
			
		||||
            let values = [];
 | 
			
		||||
            for (let i = 0; i < fieldArr.length; i++) {
 | 
			
		||||
                phs.push(`? ${fieldArr[i]}`);
 | 
			
		||||
                values.push(`T2.${fieldArr[i]}`);
 | 
			
		||||
            }
 | 
			
		||||
            let placeholder = phs.join(',');
 | 
			
		||||
            let sql = `MERGE INTO ${tableName} T1 USING 
 | 
			
		||||
        (
 | 
			
		||||
         SELECT ${placeholder} FROM dual
 | 
			
		||||
        ) T2 ON (T1.id = T2.id) 
 | 
			
		||||
        WHEN NOT MATCHED THEN INSERT(${fieldArr.join(',')}) VALUES (${values.join(',')})
 | 
			
		||||
        WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
 | 
			
		||||
            return sql;
 | 
			
		||||
        } else {
 | 
			
		||||
            let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
 | 
			
		||||
            return `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder}), (${placeholder});`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
 | 
			
		||||
import { DialectInfo } from '@/views/ops/db/dialect/index';
 | 
			
		||||
import { DialectInfo, DuplicateStrategy } from '@/views/ops/db/dialect/index';
 | 
			
		||||
 | 
			
		||||
let gsDialectInfo: DialectInfo;
 | 
			
		||||
export class GaussDialect extends PostgresqlDialect {
 | 
			
		||||
@@ -14,4 +14,17 @@ export class GaussDialect extends PostgresqlDialect {
 | 
			
		||||
        gsDialectInfo.name = 'GaussDB';
 | 
			
		||||
        return gsDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
 | 
			
		||||
        // 构建占位符字符串 "($1, $2, $3 ...)"
 | 
			
		||||
        let placeholder = fieldArr.map((_, i) => `$${i + 1}`).join(',');
 | 
			
		||||
        let suffix = '';
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.IGNORE) {
 | 
			
		||||
            suffix = '\nON DUPLICATE KEY UPDATE NOTHING';
 | 
			
		||||
        } else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
 | 
			
		||||
            suffix = '\n-- 执行前会删除唯一键涉及到的字段 \nON DUPLICATE KEY UPDATE ' + fieldArr.map((a) => `${a}=excluded.${a}`).join(',');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder}) ${suffix};`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@ import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
 | 
			
		||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
 | 
			
		||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
 | 
			
		||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
 | 
			
		||||
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
 | 
			
		||||
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
 | 
			
		||||
 | 
			
		||||
export interface sqlColumnType {
 | 
			
		||||
    udtName: string;
 | 
			
		||||
@@ -122,13 +124,15 @@ export const DbType = {
 | 
			
		||||
    oracle: 'oracle',
 | 
			
		||||
    sqlite: 'sqlite',
 | 
			
		||||
    mssql: 'mssql', // ms sqlserver
 | 
			
		||||
    kingbaseEs: 'kingbaseEs', // 人大金仓 pgsql模式 https://help.kingbase.com.cn/v8/index.html
 | 
			
		||||
    vastbase: 'vastbase', // https://docs.vastdata.com.cn/zh/docs/VastbaseG100Ver2.2.5/doc/%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/SQL%E5%8F%82%E8%80%83/SQL%E5%8F%82%E8%80%83.html
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// mysql兼容的数据库
 | 
			
		||||
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
 | 
			
		||||
 | 
			
		||||
// 有schema层的数据库
 | 
			
		||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql];
 | 
			
		||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql, DbType.kingbaseEs, DbType.vastbase];
 | 
			
		||||
 | 
			
		||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
 | 
			
		||||
 | 
			
		||||
@@ -142,6 +146,25 @@ export const compatibleMysql = (dbType: string): boolean => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 哪些数据库支持键冲突策略
 | 
			
		||||
export const compatibleDuplicateStrategy = (dbType: string): boolean => {
 | 
			
		||||
    switch (dbType) {
 | 
			
		||||
        case DbType.mysql:
 | 
			
		||||
        case DbType.mariadb:
 | 
			
		||||
        case DbType.postgresql:
 | 
			
		||||
        case DbType.gauss:
 | 
			
		||||
        case DbType.kingbaseEs:
 | 
			
		||||
        case DbType.vastbase:
 | 
			
		||||
        case DbType.dm:
 | 
			
		||||
        case DbType.oracle:
 | 
			
		||||
        case DbType.sqlite:
 | 
			
		||||
        case DbType.mssql:
 | 
			
		||||
            return true;
 | 
			
		||||
        default:
 | 
			
		||||
            return false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface DbDialect {
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取一些数据库默认信息
 | 
			
		||||
@@ -199,11 +222,28 @@ export interface DbDialect {
 | 
			
		||||
     */
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
 | 
			
		||||
 | 
			
		||||
    /** 生成编辑表信息sql */
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string;
 | 
			
		||||
 | 
			
		||||
    /** 通过数据库字段类型,返回基本数据类型 */
 | 
			
		||||
    getDataType(columnType: string): DataType;
 | 
			
		||||
 | 
			
		||||
    /** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
 | 
			
		||||
    wrapStrValue(columnType: string, value: string): string;
 | 
			
		||||
    /** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') mssql需要把中文字符串数据包装为 N'中文字符串' */
 | 
			
		||||
    wrapValue(columnType: string, value: any): any;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成插入数据预览sql
 | 
			
		||||
     * @param tableName 表名
 | 
			
		||||
     * @param columns 列名
 | 
			
		||||
     * @param duplicateStrategy 重复策略
 | 
			
		||||
     */
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, columns: string[], duplicateStrategy: DuplicateStrategy): string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum DuplicateStrategy {
 | 
			
		||||
    NONE = -1, // 无
 | 
			
		||||
    IGNORE = 1, // 忽略
 | 
			
		||||
    REPLACE = 2, // 覆盖
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let mysqlDialect = new MysqlDialect();
 | 
			
		||||
@@ -218,8 +258,20 @@ export const getDbDialectMap = () => {
 | 
			
		||||
    return dbType2DialectMap;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDbDialect = (dbType: string): DbDialect => {
 | 
			
		||||
    return dbType2DialectMap.get(dbType) || mysqlDialect;
 | 
			
		||||
export const getDbDialect = (dbType?: string): DbDialect => {
 | 
			
		||||
    return dbType2DialectMap.get(dbType!) || mysqlDialect;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *  引号转义,多用于sql注释转义,防止拼接sql报错,如: comment xx is '注''释'   最终注释文本为:  注'释
 | 
			
		||||
 * @author  liuzongyang
 | 
			
		||||
 * @since   2024/3/22 08:23
 | 
			
		||||
 */
 | 
			
		||||
export const QuoteEscape = (str: string): string => {
 | 
			
		||||
    if (!str) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
    return str.replace(/'/g, "''");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
(function () {
 | 
			
		||||
@@ -232,4 +284,6 @@ export const getDbDialect = (dbType: string): DbDialect => {
 | 
			
		||||
    registerDbDialect(DbType.oracle, new OracleDialect());
 | 
			
		||||
    registerDbDialect(DbType.sqlite, new SqliteDialect());
 | 
			
		||||
    registerDbDialect(DbType.mssql, new MssqlDialect());
 | 
			
		||||
    registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
 | 
			
		||||
    registerDbDialect(DbType.vastbase, new VastbaseDialect());
 | 
			
		||||
})();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { DialectInfo } from './index';
 | 
			
		||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
 | 
			
		||||
 | 
			
		||||
let kbpgDialectInfo: DialectInfo;
 | 
			
		||||
 | 
			
		||||
export class KingbaseEsDialect extends PostgresqlDialect {
 | 
			
		||||
    getInfo(): DialectInfo {
 | 
			
		||||
        if (kbpgDialectInfo) {
 | 
			
		||||
            return kbpgDialectInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        kbpgDialectInfo = {} as DialectInfo;
 | 
			
		||||
        Object.assign(kbpgDialectInfo, super.getInfo());
 | 
			
		||||
        kbpgDialectInfo.name = 'KingbaseES';
 | 
			
		||||
        kbpgDialectInfo.icon = 'iconfont icon-kingbase';
 | 
			
		||||
        return kbpgDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,16 @@
 | 
			
		||||
import { DbInst } from '../db';
 | 
			
		||||
import { commonCustomKeywords, DataType, DbDialect, DialectInfo, EditorCompletion, EditorCompletionItem, IndexDefinition, RowDefinition } from './index';
 | 
			
		||||
import {
 | 
			
		||||
    commonCustomKeywords,
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    QuoteEscape,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
    RowDefinition,
 | 
			
		||||
} from './index';
 | 
			
		||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
 | 
			
		||||
 | 
			
		||||
export { MSSQL_TYPE_LIST, MssqlDialect };
 | 
			
		||||
@@ -134,7 +145,7 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
            { name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'creator',
 | 
			
		||||
                type: 'varchar',
 | 
			
		||||
                type: 'nvarchar',
 | 
			
		||||
                length: '100',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
@@ -157,7 +168,7 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
            { name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'updator',
 | 
			
		||||
                type: 'varchar',
 | 
			
		||||
                type: 'nvarchar',
 | 
			
		||||
                length: '100',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
@@ -215,7 +226,7 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
            item.name && fields.push(this.genColumnBasicSql(item));
 | 
			
		||||
            item.remark &&
 | 
			
		||||
                fieldComments.push(
 | 
			
		||||
                    `EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
 | 
			
		||||
                    `EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(item.remark)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
 | 
			
		||||
                );
 | 
			
		||||
            if (item.pri) {
 | 
			
		||||
                pks.push(`${this.quoteIdentifier(item.name)}`);
 | 
			
		||||
@@ -234,7 +245,7 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
 | 
			
		||||
        // 表注释
 | 
			
		||||
        if (data.tableComment) {
 | 
			
		||||
            createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
 | 
			
		||||
            createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(data.tableComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return createTable + createIndexSql + fieldComments.join(';');
 | 
			
		||||
@@ -258,12 +269,15 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
            sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
 | 
			
		||||
            if (a.indexComment) {
 | 
			
		||||
                indexComment.push(
 | 
			
		||||
                    `EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
 | 
			
		||||
                    `EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.indexComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return sql.join(';') + ';' + indexComment.join(';');
 | 
			
		||||
        let arr = [];
 | 
			
		||||
        sql.length > 0 && arr.push(sql.join(';'));
 | 
			
		||||
        indexComment.length > 0 && arr.push(indexComment.join(';'));
 | 
			
		||||
        return arr.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
@@ -290,10 +304,10 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
        }
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
                addArr.push(` ALTER TABLE ${baseTable} ADD ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    addCommentArr.push(
 | 
			
		||||
                        `EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
 | 
			
		||||
                        `EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.remark)}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@@ -302,7 +316,7 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                if (a.oldName && a.name !== a.oldName) {
 | 
			
		||||
                    renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
 | 
			
		||||
                    renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${QuoteEscape(a.name)}', 'COLUMN' `);
 | 
			
		||||
                } else {
 | 
			
		||||
                    updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
 | 
			
		||||
                }
 | 
			
		||||
@@ -312,13 +326,13 @@ class MssqlDialect implements DbDialect {
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN', N'${a.name}')) > 0)
 | 
			
		||||
  EXEC sp_updateextendedproperty
 | 
			
		||||
'MS_Description', N'${a.remark}',
 | 
			
		||||
'MS_Description', N'${QuoteEscape(a.remark)}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN', N'${a.name}'
 | 
			
		||||
ELSE
 | 
			
		||||
  EXEC sp_addextendedproperty
 | 
			
		||||
'MS_Description', N'${a.remark}',
 | 
			
		||||
'MS_Description', N'${QuoteEscape(a.remark)}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN',N'${a.name}'`);
 | 
			
		||||
@@ -326,14 +340,15 @@ ELSE
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            delSql +
 | 
			
		||||
            (addArr.length > 0 ? addArr.join(';') + ';' : '') +
 | 
			
		||||
            (renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
 | 
			
		||||
            (updArr.length > 0 ? updArr.join(';') + ';' : '') +
 | 
			
		||||
            (changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
 | 
			
		||||
            (addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
 | 
			
		||||
        );
 | 
			
		||||
        let arr = [];
 | 
			
		||||
        delSql && arr.push(delSql);
 | 
			
		||||
        addArr.length > 0 && arr.push(addArr.join(';'));
 | 
			
		||||
        renameArr.length > 0 && arr.push(renameArr.join(';'));
 | 
			
		||||
        updArr.length > 0 && arr.push(updArr.join(';'));
 | 
			
		||||
        changeCommentArr.length > 0 && arr.push(changeCommentArr.join(';'));
 | 
			
		||||
        addCommentArr.length > 0 && arr.push(addCommentArr.join(';'));
 | 
			
		||||
 | 
			
		||||
        return arr.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
@@ -353,7 +368,7 @@ ELSE
 | 
			
		||||
            );
 | 
			
		||||
            if (a.indexComment) {
 | 
			
		||||
                commentArr.push(
 | 
			
		||||
                    ` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
 | 
			
		||||
                    ` EXEC sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.indexComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
@@ -377,7 +392,43 @@ ELSE
 | 
			
		||||
        let dropSql = dropArr.join(';');
 | 
			
		||||
        let addSql = addArr.join(';');
 | 
			
		||||
        let commentSql = commentArr.join(';');
 | 
			
		||||
        return dropSql + ';' + addSql + ';' + commentSql + ';';
 | 
			
		||||
 | 
			
		||||
        let arr = [];
 | 
			
		||||
        dropSql && arr.push(dropSql);
 | 
			
		||||
        addSql && arr.push(addSql);
 | 
			
		||||
        commentSql && arr.push(commentSql);
 | 
			
		||||
        return arr.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
 | 
			
		||||
        let sql = '';
 | 
			
		||||
 | 
			
		||||
        if (tableData.oldTableName !== tableData.tableName) {
 | 
			
		||||
            let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            // 查找是否存在注释,存在则修改,不存在则添加
 | 
			
		||||
            sql += `EXEC sp_rename '${baseTable}', '${tableData.tableName}';`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tableData.oldTableComment !== tableData.tableComment) {
 | 
			
		||||
            // 转义注释中的单引号和换行符
 | 
			
		||||
            let tableComment = tableData.tableComment.replaceAll(/'/g, "'").replaceAll(/[\r\n]/g, ' ');
 | 
			
		||||
            sql += `IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableData.tableName}', NULL, NULL)) > 0)
 | 
			
		||||
  EXEC sp_updateextendedproperty
 | 
			
		||||
'MS_Description', N'${tableComment}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableData.tableName}'
 | 
			
		||||
ELSE
 | 
			
		||||
  EXEC sp_addextendedproperty
 | 
			
		||||
'MS_Description', N'${tableComment}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableData.tableName}'`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
@@ -398,8 +449,46 @@ ELSE
 | 
			
		||||
        }
 | 
			
		||||
        return DataType.String;
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
 | 
			
		||||
    wrapStrValue(columnType: string, value: string): string {
 | 
			
		||||
 | 
			
		||||
    wrapValue(columnType: string, value: any): any {
 | 
			
		||||
        if (value == null) {
 | 
			
		||||
            return 'NULL';
 | 
			
		||||
        }
 | 
			
		||||
        if (this.getDataType(columnType) == DataType.Number) {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.getDataType(columnType) == DataType.String) {
 | 
			
		||||
            return `N'${value}'`;
 | 
			
		||||
        }
 | 
			
		||||
        return `'${value}'`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
 | 
			
		||||
        let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
 | 
			
		||||
        let baseSql = `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder});`;
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.IGNORE) {
 | 
			
		||||
            let on = `ALTER TABLE ${tableName} ADD CONSTRAINT uniqueRows UNIQUE (id) WITH (IGNORE_DUP_KEY = ON);`;
 | 
			
		||||
            return on + '\n' + baseSql;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.REPLACE) {
 | 
			
		||||
            // 字段数组生成占位符sql
 | 
			
		||||
            let phs = [];
 | 
			
		||||
            let values = [];
 | 
			
		||||
            for (let i = 0; i < fieldArr.length; i++) {
 | 
			
		||||
                phs.push(`? ${fieldArr[i]}`);
 | 
			
		||||
                values.push(`T2.${fieldArr[i]}`);
 | 
			
		||||
            }
 | 
			
		||||
            let placeholder = phs.join(',');
 | 
			
		||||
            let sql = `MERGE INTO ${tableName} T1 USING 
 | 
			
		||||
        (
 | 
			
		||||
         SELECT ${placeholder}
 | 
			
		||||
        ) T2 ON (T1.id = T2.id) 
 | 
			
		||||
        WHEN NOT MATCHED THEN INSERT(${fieldArr.join(',')}) VALUES (${values.join(',')})
 | 
			
		||||
        WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
 | 
			
		||||
            return sql;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return baseSql;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user