mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	Compare commits
	
		
			43 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					986b187f0a | ||
| 
						 | 
					008d34c453 | ||
| 
						 | 
					49d3f988c9 | ||
| 
						 | 
					76475e807e | ||
| 
						 | 
					f93231da61 | ||
| 
						 | 
					bf75483a3c | ||
| 
						 | 
					b56b0187cf | ||
| 
						 | 
					7e7f02b502 | ||
| 
						 | 
					878985f7c5 | ||
| 
						 | 
					2133d9b737 | ||
| 
						 | 
					d711a36749 | ||
| 
						 | 
					9dbf104ef1 | ||
| 
						 | 
					20eb06fb28 | ||
| 
						 | 
					9c20bdef39 | ||
| 
						 | 
					3fdd98a390 | ||
| 
						 | 
					d4f456c0cf | ||
| 
						 | 
					f2b6e15cf4 | ||
| 
						 | 
					6be0ea6aed | ||
| 
						 | 
					eee08be2cc | ||
| 
						 | 
					252fc553f2 | ||
| 
						 | 
					ac2ceed3f9 | ||
| 
						 | 
					3f828cc5b0 | ||
| 
						 | 
					fc1b9ef35d | ||
| 
						 | 
					d0b71a1c40 | ||
| 
						 | 
					a743a6a05a | ||
| 
						 | 
					0e6b9713ce | ||
| 
						 | 
					b9afbc764d | ||
| 
						 | 
					923e183a67 | ||
| 
						 | 
					7e9a381641 | ||
| 
						 | 
					bed95254d0 | ||
| 
						 | 
					e4d13f3377 | ||
| 
						 | 
					d530365ef9 | ||
| 
						 | 
					070d4ea104 | ||
| 
						 | 
					3fc86f0fae | ||
| 
						 | 
					3b77ab2727 | ||
| 
						 | 
					76cb991282 | ||
| 
						 | 
					9efd20f1b9 | ||
| 
						 | 
					de5b9e46d3 | ||
| 
						 | 
					f27d3d200f | ||
| 
						 | 
					f4a64b96a9 | ||
| 
						 | 
					9a59749763 | ||
| 
						 | 
					b017b902f8 | ||
| 
						 | 
					7c53353c60 | 
@@ -5,12 +5,12 @@ WORKDIR /mayfly
 | 
			
		||||
 | 
			
		||||
COPY mayfly_go_web .
 | 
			
		||||
 | 
			
		||||
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
 | 
			
		||||
RUN yarn config set registry 'https://registry.npmmirror.com' && \
 | 
			
		||||
    yarn install && \
 | 
			
		||||
    yarn build
 | 
			
		||||
 | 
			
		||||
# 构建后端资源
 | 
			
		||||
FROM golang:1.21.5 as be-builder
 | 
			
		||||
FROM golang:1.22 as be-builder
 | 
			
		||||
 | 
			
		||||
ENV GOPROXY https://goproxy.cn
 | 
			
		||||
WORKDIR /mayfly
 | 
			
		||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
 | 
			
		||||
 | 
			
		||||
# Build
 | 
			
		||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
 | 
			
		||||
    go build -a \
 | 
			
		||||
    go build -a -ldflags=-w \
 | 
			
		||||
    -o mayfly-go main.go
 | 
			
		||||
 | 
			
		||||
FROM debian:bookworm-slim
 | 
			
		||||
 
 | 
			
		||||
@@ -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 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
 | 
			
		||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
 | 
			
		||||
 | 
			
		||||
### 开发语言与主要框架
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2021 lyt-Top
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
@@ -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.8.0",
 | 
			
		||||
    "asciinema-player": "^3.7.0",
 | 
			
		||||
    "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.1",
 | 
			
		||||
    "countup.js": "^2.8.0",
 | 
			
		||||
    "cropperjs": "^1.6.1",
 | 
			
		||||
    "echarts": "^5.5.0",
 | 
			
		||||
    "element-plus": "^2.6.0",
 | 
			
		||||
    "js-base64": "^3.7.5",
 | 
			
		||||
    "jsencrypt": "^3.3.2",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "mitt": "^3.0.1",
 | 
			
		||||
    "monaco-editor": "^0.45.0",
 | 
			
		||||
    "monaco-editor": "^0.46.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",
 | 
			
		||||
    "uuid": "^9.0.1",
 | 
			
		||||
    "vue": "^3.4.14",
 | 
			
		||||
    "vue-router": "^4.2.5",
 | 
			
		||||
    "vue": "^3.4.21",
 | 
			
		||||
    "vue-router": "^4.3.0",
 | 
			
		||||
    "xterm": "^5.3.0",
 | 
			
		||||
    "xterm-addon-fit": "^0.8.0",
 | 
			
		||||
    "xterm-addon-search": "^0.13.0",
 | 
			
		||||
@@ -44,19 +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.21",
 | 
			
		||||
    "code-inspector-plugin": "^0.4.5",
 | 
			
		||||
    "dotenv": "^16.3.1",
 | 
			
		||||
    "eslint": "^8.35.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.19.2",
 | 
			
		||||
    "prettier": "^3.1.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.21.1",
 | 
			
		||||
    "prettier": "^3.2.5",
 | 
			
		||||
    "sass": "^1.69.0",
 | 
			
		||||
    "typescript": "^5.3.2",
 | 
			
		||||
    "vite": "^5.0.11",
 | 
			
		||||
    "vue-eslint-parser": "^9.4.0"
 | 
			
		||||
    "vite": "^5.1.4",
 | 
			
		||||
    "vue-eslint-parser": "^9.4.2"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "> 1%",
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -55,11 +55,11 @@
 | 
			
		||||
      "unicode_decimal": 58905
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "11617944",
 | 
			
		||||
      "icon_id": "25271976",
 | 
			
		||||
      "name": "oracle",
 | 
			
		||||
      "font_class": "oracle",
 | 
			
		||||
      "unicode": "e6ea",
 | 
			
		||||
      "unicode_decimal": 59114
 | 
			
		||||
      "unicode": "e507",
 | 
			
		||||
      "unicode_decimal": 58631
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "8105644",
 | 
			
		||||
@@ -67,6 +67,41 @@
 | 
			
		||||
      "font_class": "mariadb",
 | 
			
		||||
      "unicode": "e513",
 | 
			
		||||
      "unicode_decimal": 58643
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "13601813",
 | 
			
		||||
      "name": "sqlite",
 | 
			
		||||
      "font_class": "sqlite",
 | 
			
		||||
      "unicode": "e546",
 | 
			
		||||
      "unicode_decimal": 58694
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "29340317",
 | 
			
		||||
      "name": "temp-mssql",
 | 
			
		||||
      "font_class": "MSSQLNATIVE",
 | 
			
		||||
      "unicode": "e600",
 | 
			
		||||
      "unicode_decimal": 58880
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "7699332",
 | 
			
		||||
      "name": "gaussdb",
 | 
			
		||||
      "font_class": "gauss",
 | 
			
		||||
      "unicode": "e683",
 | 
			
		||||
      "unicode_decimal": 59011
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "34836637",
 | 
			
		||||
      "name": "kingbase",
 | 
			
		||||
      "font_class": "kingbase",
 | 
			
		||||
      "unicode": "e882",
 | 
			
		||||
      "unicode_decimal": 59522
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "33047500",
 | 
			
		||||
      "name": "vastbase",
 | 
			
		||||
      "font_class": "vastbase",
 | 
			
		||||
      "unicode": "e62b",
 | 
			
		||||
      "unicode_decimal": 58923
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ const config = {
 | 
			
		||||
    baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
 | 
			
		||||
 | 
			
		||||
    // 系统版本
 | 
			
		||||
    version: 'v1.7.0',
 | 
			
		||||
    version: 'v1.7.4',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
 
 | 
			
		||||
@@ -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())   // 上午好
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								mayfly_go_web/src/common/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								mayfly_go_web/src/common/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
// 根据对象访问路径,获取对应的值
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
 | 
			
		||||
    <el-input v-model="cron" placeholder="可点击左边按钮配置">
 | 
			
		||||
        <template #prepend>
 | 
			
		||||
            <el-button @click="showCron = true" icon="Pointer"></el-button>
 | 
			
		||||
        </template>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <el-page-header @back="props.back">
 | 
			
		||||
        <template #content>
 | 
			
		||||
            <span>{{ header }}</span>
 | 
			
		||||
            <span v-if="resource && !hideResource">
 | 
			
		||||
                -
 | 
			
		||||
                <el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
 | 
			
		||||
                    <el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
                <el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
 | 
			
		||||
            </span>
 | 
			
		||||
            <el-divider v-if="slots.buttons" direction="vertical" />
 | 
			
		||||
            <slot v-if="slots.buttons" name="buttons"></slot>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template #extra>
 | 
			
		||||
            <slot v-if="slots.extra" name="extra"></slot>
 | 
			
		||||
        </template>
 | 
			
		||||
    </el-page-header>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useSlots } from 'vue';
 | 
			
		||||
const slots = useSlots();
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'DrawerHeader' });
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    header: String,
 | 
			
		||||
    back: Function,
 | 
			
		||||
    resource: String,
 | 
			
		||||
    hideResource: Boolean,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,14 @@ import TerminalSearch from './TerminalSearch.vue';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
import { TerminalStatus } from './common';
 | 
			
		||||
import { useEventListener } from '@vueuse/core';
 | 
			
		||||
import themes from './themes';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    // mounted时,是否执行init方法
 | 
			
		||||
    mountInit: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化执行命令
 | 
			
		||||
     */
 | 
			
		||||
@@ -64,9 +70,9 @@ const state = reactive({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
    if (props.mountInit) {
 | 
			
		||||
        init();
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
@@ -76,6 +82,14 @@ watch(
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// 监听 themeConfig terminalTheme配置的变化
 | 
			
		||||
watch(
 | 
			
		||||
    () => themeConfig.value.terminalTheme,
 | 
			
		||||
    () => {
 | 
			
		||||
        term.options.theme = getTerminalTheme();
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
    close();
 | 
			
		||||
});
 | 
			
		||||
@@ -85,6 +99,13 @@ function init() {
 | 
			
		||||
        console.log('重新连接...');
 | 
			
		||||
        close();
 | 
			
		||||
    }
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
        initTerm();
 | 
			
		||||
        initSocket();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    // 注册自适应组件
 | 
			
		||||
@@ -117,21 +135,6 @@ function init() {
 | 
			
		||||
    state.addon.weblinks = weblinks;
 | 
			
		||||
    term.loadAddon(weblinks);
 | 
			
		||||
 | 
			
		||||
    // 初始化websocket
 | 
			
		||||
    initSocket();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 连接成功
 | 
			
		||||
 */
 | 
			
		||||
const onConnected = () => {
 | 
			
		||||
    // 注册心跳
 | 
			
		||||
    pingInterval = setInterval(sendPing, 15000);
 | 
			
		||||
 | 
			
		||||
    // 注册 terminal 事件
 | 
			
		||||
    term.onResize((event) => sendResize(event.cols, event.rows));
 | 
			
		||||
    term.onData((event) => sendCmd(event));
 | 
			
		||||
 | 
			
		||||
    // 注册自定义快捷键
 | 
			
		||||
    term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
 | 
			
		||||
        // 注册搜索键 ctrl + f
 | 
			
		||||
@@ -142,50 +145,31 @@ const onConnected = () => {
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initSocket() {
 | 
			
		||||
    if (props.socketUrl) {
 | 
			
		||||
        socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 监听socket连接
 | 
			
		||||
    socket.onopen = () => {
 | 
			
		||||
        // 注册心跳
 | 
			
		||||
        pingInterval = setInterval(sendPing, 15000);
 | 
			
		||||
        state.status = TerminalStatus.Connected;
 | 
			
		||||
 | 
			
		||||
    // 注册窗口大小监听器
 | 
			
		||||
    useEventListener('resize', debounce(fitTerminal, 400));
 | 
			
		||||
        // 注册 terminal 事件
 | 
			
		||||
        term.onResize((event) => sendResize(event.cols, event.rows));
 | 
			
		||||
        term.onData((event) => sendCmd(event));
 | 
			
		||||
 | 
			
		||||
        // // 注册窗口大小监听器
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 监听socket连接
 | 
			
		||||
    socket.onopen = () => {
 | 
			
		||||
        onConnected();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 监听socket错误信息
 | 
			
		||||
@@ -197,20 +181,47 @@ 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) {
 | 
			
		||||
    socket.onmessage = (msg: any) => {
 | 
			
		||||
        // msg.data是真正后端返回的数据
 | 
			
		||||
        term.write(msg.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 +229,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,11 +266,10 @@ const getStatus = (): TerminalStatus => {
 | 
			
		||||
    return state.status;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
 | 
			
		||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
#terminal-body {
 | 
			
		||||
    background: #212529;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    .terminal {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)"
 | 
			
		||||
@@ -259,6 +259,10 @@ defineExpose({
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-dialog {
 | 
			
		||||
        padding: 1px 1px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 取消body最大高度,否则全屏有问题
 | 
			
		||||
    .el-dialog__body {
 | 
			
		||||
        max-height: 100% !important;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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,12 +41,6 @@ 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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,22 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="layout-search-dialog">
 | 
			
		||||
        <el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
 | 
			
		||||
            <el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
 | 
			
		||||
                prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
 | 
			
		||||
            <el-autocomplete
 | 
			
		||||
                v-model="state.menuQuery"
 | 
			
		||||
                :fetch-suggestions="menuSearch"
 | 
			
		||||
                placeholder="菜单搜索"
 | 
			
		||||
                prefix-icon="el-icon-search"
 | 
			
		||||
                ref="layoutMenuAutocompleteRef"
 | 
			
		||||
                @select="onHandleSelect"
 | 
			
		||||
                @blur="onSearchBlur"
 | 
			
		||||
            >
 | 
			
		||||
                <template #prefix>
 | 
			
		||||
                    <el-icon class="el-input__icon">
 | 
			
		||||
                        <search />
 | 
			
		||||
                    </el-icon>
 | 
			
		||||
                </template>
 | 
			
		||||
                <template #default="{ item }">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-autocomplete>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { useRoutesList } from '@/store/routesList';
 | 
			
		||||
 | 
			
		||||
const layoutMenuAutocompleteRef: any = ref(null);;
 | 
			
		||||
const layoutMenuAutocompleteRef: any = ref(null);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const state: any = reactive({
 | 
			
		||||
    isShowSearch: false,
 | 
			
		||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
 | 
			
		||||
const createFilter = (queryString: any) => {
 | 
			
		||||
    return (restaurant: any) => {
 | 
			
		||||
        return (
 | 
			
		||||
            restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
 | 
			
		||||
            restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
 | 
			
		||||
            restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
 | 
			
		||||
    closeSearch();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({openSearch})
 | 
			
		||||
defineExpose({ openSearch });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,16 @@
 | 
			
		||||
                <!-- ssh终端主题 -->
 | 
			
		||||
                <el-divider content-position="left">终端主题</el-divider>
 | 
			
		||||
                <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                        <el-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>
 | 
			
		||||
                <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')">
 | 
			
		||||
@@ -21,10 +31,13 @@
 | 
			
		||||
                    <div class="layout-breadcrumb-seting-bar-flex">
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
 | 
			
		||||
                        <div class="layout-breadcrumb-seting-bar-flex-value">
 | 
			
		||||
                        <el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
 | 
			
		||||
                            <el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
 | 
			
		||||
                            </el-color-picker>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                <div class="layout-breadcrumb-seting-bar-flex mt15">
 | 
			
		||||
                </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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import { saveThemeConfig } from '@/common/utils/storage';
 | 
			
		||||
import { isDark } from './user.vue';
 | 
			
		||||
 | 
			
		||||
export const switchDark = () => {
 | 
			
		||||
    themeConfig.value.isDark = isDark.value;
 | 
			
		||||
    if (isDark.value) {
 | 
			
		||||
        themeConfig.value.editorTheme = 'vs-dark';
 | 
			
		||||
    } else {
 | 
			
		||||
        themeConfig.value.editorTheme = 'vs';
 | 
			
		||||
    }
 | 
			
		||||
    // 如果终端主题不是自定义主题,则切换主题
 | 
			
		||||
    if (themeConfig.value.terminalTheme != 'custom') {
 | 
			
		||||
        if (isDark.value) {
 | 
			
		||||
            themeConfig.value.terminalTheme = 'dark';
 | 
			
		||||
        } else {
 | 
			
		||||
            themeConfig.value.terminalTheme = 'solarizedLight';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    saveThemeConfig(themeConfig.value);
 | 
			
		||||
};
 | 
			
		||||
@@ -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 = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -114,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 | 
			
		||||
            // 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
 | 
			
		||||
            layout: 'classic',
 | 
			
		||||
 | 
			
		||||
            terminalTheme: 'solarizedLight',
 | 
			
		||||
            // 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 = 'solarizedLight';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,20 +13,24 @@
 | 
			
		||||
/* 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;
 | 
			
		||||
@@ -38,6 +42,7 @@
 | 
			
		||||
.el-alert {
 | 
			
		||||
    border: 1px solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-alert__title {
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
}
 | 
			
		||||
@@ -56,23 +61,28 @@
 | 
			
		||||
.el-menu-hover-bg-color {
 | 
			
		||||
    background-color: var(--bg-menuBarActiveColor) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 默认样式修改
 | 
			
		||||
.el-menu {
 | 
			
		||||
    border-right: none !important;
 | 
			
		||||
    width: 220px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-menu-item {
 | 
			
		||||
    height: 56px !important;
 | 
			
		||||
    line-height: 56px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-menu-item,
 | 
			
		||||
.el-sub-menu__title {
 | 
			
		||||
    color: var(--bg-menuBarColor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
 | 
			
		||||
.el-menu--collapse {
 | 
			
		||||
    width: 64px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 外部链接时
 | 
			
		||||
.el-menu-item a,
 | 
			
		||||
.el-menu-item a:hover,
 | 
			
		||||
@@ -81,6 +91,7 @@
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 第三方图标字体间距/大小设置
 | 
			
		||||
.el-menu-item .iconfont,
 | 
			
		||||
.el-sub-menu .iconfont,
 | 
			
		||||
@@ -88,18 +99,22 @@
 | 
			
		||||
.el-sub-menu .fa {
 | 
			
		||||
    @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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-menu-item:hover {
 | 
			
		||||
    @extend .el-menu-hover-bg-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
 | 
			
		||||
    background-color: unset !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 子级菜单背景颜色
 | 
			
		||||
// .el-menu--inline {
 | 
			
		||||
// 	background: var(--next-bg-menuBar-light-1);
 | 
			
		||||
@@ -109,79 +124,96 @@
 | 
			
		||||
    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.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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 菜单收起时,图标不居中问题
 | 
			
		||||
.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;
 | 
			
		||||
    }
 | 
			
		||||
@@ -196,30 +228,24 @@
 | 
			
		||||
/* Dropdown 下拉菜单
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-dropdown-menu {
 | 
			
		||||
	list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
 | 
			
		||||
    list-style: none !important;
 | 
			
		||||
    /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-dropdown-menu .el-dropdown-menu__item {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
 | 
			
		||||
    &:not(.is-disabled):hover {
 | 
			
		||||
        background-color: var(--el-dropdown-menuItem-hover-fill);
 | 
			
		||||
        color: var(--el-dropdown-menuItem-hover-color);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Steps 步骤条
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-step__icon-inner {
 | 
			
		||||
	font-size: 30px !important;
 | 
			
		||||
	font-weight: 400 !important;
 | 
			
		||||
}
 | 
			
		||||
.el-step__title {
 | 
			
		||||
	font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dialog 对话框
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-overlay {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    .el-overlay-dialog {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
@@ -227,15 +253,18 @@
 | 
			
		||||
        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;
 | 
			
		||||
@@ -261,25 +290,31 @@
 | 
			
		||||
.el-scrollbar__bar {
 | 
			
		||||
    z-index: 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
 | 
			
		||||
.el-scrollbar__wrap {
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-select-dropdown .el-scrollbar__wrap {
 | 
			
		||||
    overflow-x: scroll !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*修复Select 选择器高度问题*/
 | 
			
		||||
.el-select-dropdown__wrap {
 | 
			
		||||
    max-height: 274px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*修复Cascader 级联选择器高度问题*/
 | 
			
		||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
 | 
			
		||||
    height: 204px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
 | 
			
		||||
.layout-container-view .el-scrollbar__view {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
 | 
			
		||||
.layout-columns-warp .layout-aside .el-scrollbar__view {
 | 
			
		||||
    height: unset !important;
 | 
			
		||||
@@ -290,6 +325,7 @@
 | 
			
		||||
.el-pagination__editor {
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*深色模式时分页高亮问题*/
 | 
			
		||||
.el-pagination.is-background .btn-next.is-active,
 | 
			
		||||
.el-pagination.is-background .btn-prev.is-active,
 | 
			
		||||
@@ -298,32 +334,13 @@
 | 
			
		||||
    color: var(--el-color-white) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Drawer 抽屉
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-drawer {
 | 
			
		||||
	--el-drawer-padding-primary: unset !important;
 | 
			
		||||
	.el-drawer__header {
 | 
			
		||||
		padding: 0 15px !important;
 | 
			
		||||
		height: 50px;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		margin-bottom: 0 !important;
 | 
			
		||||
		border-bottom: 1px solid var(--el-border-color);
 | 
			
		||||
		color: var(--el-text-color-primary);
 | 
			
		||||
	}
 | 
			
		||||
	.el-drawer__body {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Breadcrumb 面包屑
 | 
			
		||||
------------------------------- */
 | 
			
		||||
.el-breadcrumb__inner a:hover,
 | 
			
		||||
.el-breadcrumb__inner.is-link:hover {
 | 
			
		||||
    color: var(--el-color-primary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-breadcrumb__inner a,
 | 
			
		||||
.el-breadcrumb__inner.is-link {
 | 
			
		||||
    color: var(--bg-topBarColor);
 | 
			
		||||
@@ -336,6 +353,7 @@
 | 
			
		||||
    //   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;
 | 
			
		||||
 
 | 
			
		||||
@@ -98,4 +98,3 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@/router/staticRouter
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										211
									
								
								mayfly_go_web/src/views/flow/ProcdefEdit.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										211
									
								
								mayfly_go_web/src/views/flow/ProcdefEdit.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,211 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
 | 
			
		||||
            <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-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';
 | 
			
		||||
 | 
			
		||||
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: '',
 | 
			
		||||
    },
 | 
			
		||||
    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 };
 | 
			
		||||
        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 () => {
 | 
			
		||||
    formRef.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
            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>
 | 
			
		||||
							
								
								
									
										141
									
								
								mayfly_go_web/src/views/flow/ProcdefList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								mayfly_go_web/src/views/flow/ProcdefList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
<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 #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';
 | 
			
		||||
 | 
			
		||||
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('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>
 | 
			
		||||
							
								
								
									
										120
									
								
								mayfly_go_web/src/views/flow/ProcinstList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								mayfly_go_web/src/views/flow/ProcinstList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
			
		||||
<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)];
 | 
			
		||||
const columns = [
 | 
			
		||||
    TableColumn.new('bizType', '业务').typeTag(FlowBizType),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('creator', '发起人'),
 | 
			
		||||
    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>
 | 
			
		||||
							
								
								
									
										110
									
								
								mayfly_go_web/src/views/flow/ProcinstTaskList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								mayfly_go_web/src/views/flow/ProcinstTaskList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
<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.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'),
 | 
			
		||||
    getByKey: Api.newGet('/flow/procdefs/{key}'),
 | 
			
		||||
    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>
 | 
			
		||||
							
								
								
									
										136
									
								
								mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										136
									
								
								mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
<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 { procdefApi } from '../api';
 | 
			
		||||
import { ElSteps, ElStep } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    // 流程定义任务
 | 
			
		||||
    tasks: {
 | 
			
		||||
        type: [String, Object],
 | 
			
		||||
    },
 | 
			
		||||
    procdefKey: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    // 流程实例任务列表
 | 
			
		||||
    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.procdefKey,
 | 
			
		||||
    async (newValue: any) => {
 | 
			
		||||
        if (newValue) {
 | 
			
		||||
            parseTasksByKey(newValue);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    if (props.procdefKey) {
 | 
			
		||||
        parseTasksByKey(props.procdefKey);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    parseTasks(props.tasks);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const parseTasksByKey = async (key: string) => {
 | 
			
		||||
    const procdef = await procdefApi.getByKey.request({ key });
 | 
			
		||||
    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>
 | 
			
		||||
@@ -73,12 +73,28 @@ const currentTime = computed(() => {
 | 
			
		||||
 | 
			
		||||
// 初始化数字滚动
 | 
			
		||||
const initNumCountUp = async () => {
 | 
			
		||||
    const res: any = await indexApi.getIndexCount.request();
 | 
			
		||||
    indexApi.machineDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('machineNum', res.machineNum).start();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.dbDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('dbNum', res.dbNum).start();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.redisDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('redisNum', res.redisNum).start();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    indexApi.mongoDashbord.request().then((res: any) => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
            new CountUp('mongoNum', res.mongoNum).start();
 | 
			
		||||
        new CountUp('machineNum', res.machineNum).start();
 | 
			
		||||
        new CountUp('dbNum', res.dbNum).start();
 | 
			
		||||
        new CountUp('redisNum', res.redisNum).start();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +109,7 @@ const toPage = (item: any) => {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'machineNum': {
 | 
			
		||||
            router.push('/machine/machines');
 | 
			
		||||
            router.push('/machine/machines-op');
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dbNum': {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import Api from '@/common/Api';
 | 
			
		||||
 | 
			
		||||
export const indexApi = {
 | 
			
		||||
    getIndexCount: Api.newGet("/common/index/count"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    machineDashbord: Api.newGet('/machines/dashbord'),
 | 
			
		||||
    dbDashbord: Api.newGet('/dbs/dashbord'),
 | 
			
		||||
    redisDashbord: Api.newGet('/redis/dashbord'),
 | 
			
		||||
    mongoDashbord: Api.newGet('/mongos/dashbord'),
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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.tagPath" />
 | 
			
		||||
            <span class="ml3">{{ tag.tagPath }}</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.tagPath" />
 | 
			
		||||
                    <span class="ml3">{{ i.tagPath }}</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,7 +17,7 @@
 | 
			
		||||
                @node-contextmenu="nodeContextmenu"
 | 
			
		||||
            >
 | 
			
		||||
                <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,7 +25,13 @@
 | 
			
		||||
                        <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>
 | 
			
		||||
@@ -135,15 +141,29 @@ 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) {
 | 
			
		||||
        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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,13 +2,13 @@
 | 
			
		||||
    <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="[state.selectTags]"
 | 
			
		||||
            show-checkbox
 | 
			
		||||
            node-key="id"
 | 
			
		||||
            :props="{
 | 
			
		||||
@@ -40,32 +40,22 @@ import { tagApi } from '../tag/api';
 | 
			
		||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    resourceCode: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    resourceType: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        required: true,
 | 
			
		||||
    selectTags: {
 | 
			
		||||
        type: [Array<any>],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tags: [],
 | 
			
		||||
    // 单选则为id,多选为id数组
 | 
			
		||||
    selectTags: [],
 | 
			
		||||
    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();
 | 
			
		||||
    if (props.selectTags) {
 | 
			
		||||
        state.selectTags = props.selectTags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.tags = await tagApi.getTagTrees.request(null);
 | 
			
		||||
 
 | 
			
		||||
@@ -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 右击菜单按钮选项
 | 
			
		||||
 
 | 
			
		||||
@@ -23,13 +23,16 @@
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="name" label="任务名称">
 | 
			
		||||
                    <el-input v-model.number="state.form.name" type="text" placeholder="任务名称"></el-input>
 | 
			
		||||
                    <el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <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);
 | 
			
		||||
@@ -101,10 +112,11 @@ const state = reactive({
 | 
			
		||||
        id: 0,
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        dbNames: '',
 | 
			
		||||
        name: null as any,
 | 
			
		||||
        intervalDay: null,
 | 
			
		||||
        name: '',
 | 
			
		||||
        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();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										155
									
								
								mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="db-backup-history">
 | 
			
		||||
        <page-table
 | 
			
		||||
            height="100%"
 | 
			
		||||
            ref="pageTableRef"
 | 
			
		||||
            :page-api="dbApi.getDbBackupHistories"
 | 
			
		||||
            :show-selection="true"
 | 
			
		||||
            v-model:selection-data="state.selectedData"
 | 
			
		||||
            :searchItems="searchItems"
 | 
			
		||||
            :before-query-fn="beforeQueryFn"
 | 
			
		||||
            v-model:query-form="query"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
        >
 | 
			
		||||
            <template #dbSelect>
 | 
			
		||||
                <el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
 | 
			
		||||
                    <el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
 | 
			
		||||
                </el-select>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tableHeader>
 | 
			
		||||
                <el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
 | 
			
		||||
                <el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
 | 
			
		||||
                    <el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, Ref, ref } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    dbNames: {
 | 
			
		||||
        type: [Array<String>],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
 | 
			
		||||
 | 
			
		||||
const columns = [
 | 
			
		||||
    TableColumn.new('dbName', '数据库名称'),
 | 
			
		||||
    TableColumn.new('name', '备份名称'),
 | 
			
		||||
    TableColumn.new('createTime', '创建时间').isTime(),
 | 
			
		||||
    TableColumn.new('lastResult', '恢复结果'),
 | 
			
		||||
    TableColumn.new('lastTime', '恢复时间').isTime(),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const emptyQuery = {
 | 
			
		||||
    dbId: 0,
 | 
			
		||||
    dbName: '',
 | 
			
		||||
    pageNum: 1,
 | 
			
		||||
    pageSize: 10,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    data: [],
 | 
			
		||||
    total: 0,
 | 
			
		||||
    query: emptyQuery,
 | 
			
		||||
    /**
 | 
			
		||||
     * 选中的数据
 | 
			
		||||
     */
 | 
			
		||||
    selectedData: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { query } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const beforeQueryFn = (query: any) => {
 | 
			
		||||
    query.dbId = props.dbId;
 | 
			
		||||
    return query;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    await pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDbBackupHistory = async (data: any) => {
 | 
			
		||||
    let backupHistoryId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        backupHistoryId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要删除的数据库备份历史');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
 | 
			
		||||
    await search();
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const restoreDbBackupHistory = async (data: any) => {
 | 
			
		||||
    let backupHistoryId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        backupHistoryId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        const pluralDbNames: string[] = [];
 | 
			
		||||
        const dbNames: Map<string, boolean> = new Map();
 | 
			
		||||
        state.selectedData.forEach((item: any) => {
 | 
			
		||||
            if (!dbNames.has(item.dbName)) {
 | 
			
		||||
                dbNames.set(item.dbName, false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (!dbNames.get(item.dbName)) {
 | 
			
		||||
                dbNames.set(item.dbName, true);
 | 
			
		||||
                pluralDbNames.push(item.dbName);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        if (pluralDbNames.length > 0) {
 | 
			
		||||
            ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要恢复的数据库备份历史');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await dbApi.restoreDbBackupHistory.request({
 | 
			
		||||
        dbId: props.dbId,
 | 
			
		||||
        backupHistoryId: backupHistoryId,
 | 
			
		||||
    });
 | 
			
		||||
    await search();
 | 
			
		||||
    ElMessage.success('成功创建数据库恢复任务');
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
                <el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
 | 
			
		||||
                <el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
 | 
			
		||||
                <el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
 | 
			
		||||
                <el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
@@ -29,6 +30,7 @@
 | 
			
		||||
                    <el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
 | 
			
		||||
                    <el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
 | 
			
		||||
                    <el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
 | 
			
		||||
                    <el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
@@ -72,10 +74,10 @@ const columns = [
 | 
			
		||||
    TableColumn.new('name', '任务名称'),
 | 
			
		||||
    TableColumn.new('startTime', '启动时间').isTime(),
 | 
			
		||||
    TableColumn.new('intervalDay', '备份周期'),
 | 
			
		||||
    TableColumn.new('enabled', '是否启用'),
 | 
			
		||||
    TableColumn.new('enabledDesc', '是否启用'),
 | 
			
		||||
    TableColumn.new('lastResult', '执行结果'),
 | 
			
		||||
    TableColumn.new('lastTime', '执行时间').isTime(),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const emptyQuery = {
 | 
			
		||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
 | 
			
		||||
    await search();
 | 
			
		||||
    ElMessage.success('备份任务启动成功');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDbBackup = async (data: any) => {
 | 
			
		||||
    let backupId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        backupId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        backupId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要删除的数据库备份任务');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
 | 
			
		||||
    await search();
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,7 @@
 | 
			
		||||
                            }
 | 
			
		||||
                        "
 | 
			
		||||
                        multiple
 | 
			
		||||
                        :resource-code="form.code"
 | 
			
		||||
                        :resource-type="TagResourceTypeEnum.Db.value"
 | 
			
		||||
                        :select-tags="form.tagId"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                    />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
@@ -75,6 +74,8 @@
 | 
			
		||||
                <el-form-item prop="remark" label="备注">
 | 
			
		||||
                    <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <procdef-select-form-item v-model="form.flowProcdefKey" />
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
@@ -92,8 +93,9 @@ import { toRefs, reactive, watch, ref } 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 ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
 | 
			
		||||
import { DbType } from '@/views/ops/db/dialect';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -163,6 +165,7 @@ const state = reactive({
 | 
			
		||||
        database: '',
 | 
			
		||||
        remark: '',
 | 
			
		||||
        instanceId: null as any,
 | 
			
		||||
        flowProcdefKey: '',
 | 
			
		||||
    },
 | 
			
		||||
    instances: [] as any,
 | 
			
		||||
});
 | 
			
		||||
@@ -178,7 +181,7 @@ watch(props, async (newValue: any) => {
 | 
			
		||||
    }
 | 
			
		||||
    if (newValue.db) {
 | 
			
		||||
        state.form = { ...newValue.db };
 | 
			
		||||
 | 
			
		||||
        state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
 | 
			
		||||
        // 将数据库名使用空格切割,获取所有数据库列表
 | 
			
		||||
        state.dbNamesSelected = newValue.db.database.split(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -194,7 +197,14 @@ const changeInstance = () => {
 | 
			
		||||
 | 
			
		||||
const getAllDatabase = async () => {
 | 
			
		||||
    if (state.form.instanceId > 0) {
 | 
			
		||||
        state.allDatabases = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
 | 
			
		||||
        let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
 | 
			
		||||
        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];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
 | 
			
		||||
                <ResourceTags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
@@ -62,15 +62,28 @@
 | 
			
		||||
                        <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: 'dbBackup', data }" v-if="supportAction('dbBackup', data.type)"> 备份 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
 | 
			
		||||
                                备份任务
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item
 | 
			
		||||
                                :command="{ type: 'backupHistory', data }"
 | 
			
		||||
                                v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
 | 
			
		||||
                            >
 | 
			
		||||
                                备份历史
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item
 | 
			
		||||
                                :command="{ type: 'restoreDb', data }"
 | 
			
		||||
                                v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
 | 
			
		||||
                            >
 | 
			
		||||
                                恢复任务
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                        </el-dropdown-menu>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-dropdown>
 | 
			
		||||
            </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="导出内容: ">
 | 
			
		||||
@@ -131,6 +144,16 @@
 | 
			
		||||
            <db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            width="80%"
 | 
			
		||||
            :title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            v-model="dbBackupHistoryDialog.visible"
 | 
			
		||||
        >
 | 
			
		||||
            <db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            width="80%"
 | 
			
		||||
            :title="`${dbRestoreDialog.title} - 数据库恢复`"
 | 
			
		||||
@@ -141,23 +164,32 @@
 | 
			
		||||
            <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="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="3" label="工单流程key">{{ infoDialog.data?.flowProcdefKey }}</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>
 | 
			
		||||
 | 
			
		||||
@@ -173,7 +205,6 @@ 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';
 | 
			
		||||
@@ -185,7 +216,9 @@ import { getDbDialect } from './dialect/index';
 | 
			
		||||
import { getTagPathSearchItem } from '../component/tag';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -193,22 +226,26 @@ 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 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('flowProcdefKey', '关联流程'),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
 | 
			
		||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
 | 
			
		||||
const actionBtns = hasPerms(Object.values(perms));
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
@@ -253,6 +290,13 @@ const state = reactive({
 | 
			
		||||
        dbs: [],
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
    },
 | 
			
		||||
    // 数据库备份历史弹框
 | 
			
		||||
    dbBackupHistoryDialog: {
 | 
			
		||||
        title: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
        dbs: [],
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
    },
 | 
			
		||||
    // 数据库恢复弹框
 | 
			
		||||
    dbRestoreDialog: {
 | 
			
		||||
        title: '',
 | 
			
		||||
@@ -285,7 +329,8 @@ const state = reactive({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
 | 
			
		||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
 | 
			
		||||
    toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (Object.keys(actionBtns).length > 0) {
 | 
			
		||||
@@ -345,11 +390,15 @@ const handleMoreActionCommand = (commond: any) => {
 | 
			
		||||
            onDumpDbs(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dbBackup': {
 | 
			
		||||
        case 'backupDb': {
 | 
			
		||||
            onShowDbBackupDialog(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dbRestore': {
 | 
			
		||||
        case 'backupHistory': {
 | 
			
		||||
            onShowDbBackupHistoryDialog(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'restoreDb': {
 | 
			
		||||
            onShowDbRestoreDialog(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -402,6 +451,13 @@ const onShowDbBackupDialog = async (row: any) => {
 | 
			
		||||
    state.dbBackupDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onShowDbBackupHistoryDialog = async (row: any) => {
 | 
			
		||||
    state.dbBackupHistoryDialog.title = `${row.name}`;
 | 
			
		||||
    state.dbBackupHistoryDialog.dbId = row.id;
 | 
			
		||||
    state.dbBackupHistoryDialog.dbs = row.database.split(' ');
 | 
			
		||||
    state.dbBackupHistoryDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onShowDbRestoreDialog = async (row: any) => {
 | 
			
		||||
    state.dbRestoreDialog.title = `${row.name}`;
 | 
			
		||||
    state.dbRestoreDialog.dbId = row.id;
 | 
			
		||||
@@ -455,7 +511,7 @@ const supportAction = (action: string, dbType: string): boolean => {
 | 
			
		||||
    switch (dbType) {
 | 
			
		||||
        case DbType.mysql:
 | 
			
		||||
        case DbType.mariadb:
 | 
			
		||||
            actions = ['dumpDb', 'dbBackup', 'dbRestore'];
 | 
			
		||||
            actions = ['dumpDb', 'backupDb', 'restoreDb'];
 | 
			
		||||
    }
 | 
			
		||||
    return actions.includes(action);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,13 @@
 | 
			
		||||
                        clearable
 | 
			
		||||
                        class="w100"
 | 
			
		||||
                    >
 | 
			
		||||
                        <el-option v-for="item in state.histories" :key="item.id" :label="item.name" :value="item"> </el-option>
 | 
			
		||||
                        <el-option
 | 
			
		||||
                            v-for="item in state.histories"
 | 
			
		||||
                            :key="item.id"
 | 
			
		||||
                            :label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
 | 
			
		||||
                            :value="item"
 | 
			
		||||
                        >
 | 
			
		||||
                        </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="startTime" label="开始时间">
 | 
			
		||||
@@ -56,7 +62,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, reactive, ref, watch } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    data: {
 | 
			
		||||
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
 | 
			
		||||
    if (!state.histories || state.histories.length == 0) {
 | 
			
		||||
        callback(new Error('数据库没有备份记录'));
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const history = state.histories[state.histories.length - 1];
 | 
			
		||||
    if (value < new Date(history.createTime)) {
 | 
			
		||||
        callback(new Error('在此之前数据库没有备份记录'));
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (value > new Date()) {
 | 
			
		||||
        callback(new Error('恢复时间点晚于当前时间'));
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!state.histories || state.histories.length == 0) {
 | 
			
		||||
        callback(new Error('数据库没有备份记录'));
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    let last = null;
 | 
			
		||||
    for (const history of state.histories) {
 | 
			
		||||
        if (!history.binlogFileName || history.binlogFileName.length === 0) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        if (new Date(history.createTime) < value) {
 | 
			
		||||
            callback();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        last = history;
 | 
			
		||||
    }
 | 
			
		||||
    if (!last) {
 | 
			
		||||
        callback(new Error('现有数据库备份不支持指定时间恢复'));
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
@@ -110,7 +126,6 @@ const rules = {
 | 
			
		||||
    pointInTime: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            // message: '请选择恢复时间点',
 | 
			
		||||
            validator: validatePointInTime,
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
@@ -146,7 +161,7 @@ const state = reactive({
 | 
			
		||||
        id: 0,
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        dbName: null as any,
 | 
			
		||||
        intervalDay: 1,
 | 
			
		||||
        intervalDay: 0,
 | 
			
		||||
        startTime: null as any,
 | 
			
		||||
        repeated: null as any,
 | 
			
		||||
        dbBackupId: null as any,
 | 
			
		||||
@@ -218,7 +233,8 @@ const init = async (data: any) => {
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form.dbName = '';
 | 
			
		||||
        state.editOrCreate = false;
 | 
			
		||||
        state.form.intervalDay = 1;
 | 
			
		||||
        state.form.intervalDay = 0;
 | 
			
		||||
        state.form.repeated = false;
 | 
			
		||||
        state.form.pointInTime = new Date();
 | 
			
		||||
        state.form.startTime = new Date();
 | 
			
		||||
        state.histories = [];
 | 
			
		||||
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
 | 
			
		||||
const btnOk = async () => {
 | 
			
		||||
    restoreForm.value.validate(async (valid: any) => {
 | 
			
		||||
        if (valid) {
 | 
			
		||||
            await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
 | 
			
		||||
                confirmButtonText: '确定',
 | 
			
		||||
                cancelButtonText: '取消',
 | 
			
		||||
                type: 'warning',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (state.restoreMode == 'point-in-time') {
 | 
			
		||||
                state.form.dbBackupId = 0;
 | 
			
		||||
                state.form.dbBackupHistoryId = 0;
 | 
			
		||||
@@ -245,13 +267,14 @@ const btnOk = async () => {
 | 
			
		||||
                state.form.pointInTime = null;
 | 
			
		||||
            }
 | 
			
		||||
            state.form.repeated = false;
 | 
			
		||||
            state.form.intervalDay = 0;
 | 
			
		||||
            const reqForm = { ...state.form };
 | 
			
		||||
            let api = dbApi.createDbRestore;
 | 
			
		||||
            if (props.data) {
 | 
			
		||||
                api = dbApi.saveDbRestore;
 | 
			
		||||
            }
 | 
			
		||||
            api.request(reqForm).then(() => {
 | 
			
		||||
                ElMessage.success('保存成功');
 | 
			
		||||
                ElMessage.success('成功创建数据库恢复任务');
 | 
			
		||||
                emit('val-change', state.form);
 | 
			
		||||
                state.btnLoading = true;
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,14 @@
 | 
			
		||||
                <el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
 | 
			
		||||
                <el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
 | 
			
		||||
                <el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
 | 
			
		||||
                <el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
 | 
			
		||||
                <el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
 | 
			
		||||
                <el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
 | 
			
		||||
                <el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +51,7 @@
 | 
			
		||||
                    infoDialog.data.dbBackupHistoryName
 | 
			
		||||
                }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
 | 
			
		||||
            </el-descriptions>
 | 
			
		||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
 | 
			
		||||
const columns = [
 | 
			
		||||
    TableColumn.new('dbName', '数据库名称'),
 | 
			
		||||
    TableColumn.new('startTime', '启动时间').isTime(),
 | 
			
		||||
    TableColumn.new('enabled', '是否启用'),
 | 
			
		||||
    TableColumn.new('enabledDesc', '是否启用'),
 | 
			
		||||
    TableColumn.new('lastTime', '执行时间').isTime(),
 | 
			
		||||
    TableColumn.new('lastResult', '执行结果'),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
 | 
			
		||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
 | 
			
		||||
    state.dbRestoreEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDbRestore = async (data: any) => {
 | 
			
		||||
    let restoreId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        restoreId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        restoreId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要删除的数据库恢复任务');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
 | 
			
		||||
    await search();
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showDbRestore = async (data: any) => {
 | 
			
		||||
    state.infoDialog.data = data;
 | 
			
		||||
    state.infoDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const enableDbRestore = async (data: any) => {
 | 
			
		||||
    let restoreId: String;
 | 
			
		||||
    let restoreId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        restoreId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        restoreId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要启用的恢复任务');
 | 
			
		||||
        ElMessage.error('请选择需要启用的数据库恢复任务');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
 | 
			
		||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const disableDbRestore = async (data: any) => {
 | 
			
		||||
    let restoreId: String;
 | 
			
		||||
    let restoreId: string;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        restoreId = data.id;
 | 
			
		||||
    } else if (state.selectedData.length > 0) {
 | 
			
		||||
        restoreId = state.selectedData.map((x: any) => x.id).join(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        ElMessage.error('请选择需要禁用的恢复任务');
 | 
			
		||||
        ElMessage.error('请选择需要禁用的数据库恢复任务');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -36,9 +39,9 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, watch, reactive, onMounted, Ref, ref } from 'vue';
 | 
			
		||||
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,
 | 
			
		||||
@@ -120,6 +126,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
 | 
			
		||||
    const primaryKey = getPrimaryKey(columns);
 | 
			
		||||
    const oldValue = JSON.parse(sqlExecLog.oldValue);
 | 
			
		||||
 | 
			
		||||
    let schema = '';
 | 
			
		||||
    let dbArr = sqlExecLog.db.split('/');
 | 
			
		||||
    if (dbArr.length == 2) {
 | 
			
		||||
        schema = dbArr[1] + '.';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rollbackSqls = [];
 | 
			
		||||
    if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
 | 
			
		||||
        for (let ov of oldValue) {
 | 
			
		||||
@@ -130,7 +142,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
 | 
			
		||||
                }
 | 
			
		||||
                setItems.push(`${key} = ${wrapValue(ov[key])}`);
 | 
			
		||||
            }
 | 
			
		||||
            rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
 | 
			
		||||
            rollbackSqls.push(`UPDATE ${schema}${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
 | 
			
		||||
        }
 | 
			
		||||
    } else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
 | 
			
		||||
        const columnNames = columns.map((c: any) => c.columnName);
 | 
			
		||||
@@ -139,7 +151,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
 | 
			
		||||
            for (let column of columnNames) {
 | 
			
		||||
                values.push(wrapValue(ov[column]));
 | 
			
		||||
            }
 | 
			
		||||
            rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
 | 
			
		||||
            rollbackSqls.push(`INSERT INTO ${schema}${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -148,7 +160,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getPrimaryKey = (columns: any) => {
 | 
			
		||||
    const col = columns.find((c: any) => c.columnKey == 'PRI');
 | 
			
		||||
    const col = columns.find((c: any) => c.isPrimaryKey);
 | 
			
		||||
    if (col) {
 | 
			
		||||
        return col.columnName;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,22 @@
 | 
			
		||||
                        </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="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
 | 
			
		||||
                                    <SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
 | 
			
		||||
                                    {{ dt.label }}
 | 
			
		||||
                                <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>
 | 
			
		||||
 | 
			
		||||
                                <template #prefix>
 | 
			
		||||
                                    <SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-select>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="host" label="host" required>
 | 
			
		||||
                        <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>
 | 
			
		||||
@@ -24,13 +33,18 @@
 | 
			
		||||
                                <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" prop="sid" label="SID">
 | 
			
		||||
                            <el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
                        <el-form-item prop="username" label="用户名" required>
 | 
			
		||||
                        <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 prop="password" label="密码">
 | 
			
		||||
                        <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">
 | 
			
		||||
@@ -90,7 +104,7 @@ import { ElMessage } from 'element-plus';
 | 
			
		||||
import { notBlank } from '@/common/assert';
 | 
			
		||||
import { RsaEncrypt } from '@/common/rsa';
 | 
			
		||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
 | 
			
		||||
import { DbType, getDbDialect } from './dialect';
 | 
			
		||||
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
@@ -148,35 +162,12 @@ const rules = {
 | 
			
		||||
 | 
			
		||||
const dbForm: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const dbTypes = [
 | 
			
		||||
    {
 | 
			
		||||
        type: 'mysql',
 | 
			
		||||
        label: 'mysql',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        type: 'mariadb',
 | 
			
		||||
        label: 'mariadb',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        type: 'postgres',
 | 
			
		||||
        label: 'postgres',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        type: 'dm',
 | 
			
		||||
        label: '达梦',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        type: 'oracle',
 | 
			
		||||
        label: 'oracle',
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    tabActiveName: 'basic',
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        type: null,
 | 
			
		||||
        type: '',
 | 
			
		||||
        name: null,
 | 
			
		||||
        host: '',
 | 
			
		||||
        port: null,
 | 
			
		||||
@@ -187,17 +178,17 @@ const state = reactive({
 | 
			
		||||
        remark: '',
 | 
			
		||||
        sshTunnelMachineId: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    subimtForm: {},
 | 
			
		||||
    submitForm: {},
 | 
			
		||||
    // 原密码
 | 
			
		||||
    pwd: '',
 | 
			
		||||
    // 原用户名
 | 
			
		||||
    oldUserName: null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
 | 
			
		||||
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
 | 
			
		||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
 | 
			
		||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
 | 
			
		||||
 | 
			
		||||
watch(props, (newValue: any) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
 | 
			
		||||
        state.form = { ...newValue.data };
 | 
			
		||||
        state.oldUserName = state.form.username;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = { port: null } as any;
 | 
			
		||||
        state.form = { port: null, type: DbType.mysql } as any;
 | 
			
		||||
        state.oldUserName = null;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
@@ -240,18 +231,20 @@ const testConn = async () => {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.subimtForm = await getReqForm();
 | 
			
		||||
        state.submitForm = await getReqForm();
 | 
			
		||||
        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, '已修改用户名,请输入密码');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
        if (!valid) {
 | 
			
		||||
@@ -259,7 +252,7 @@ const btnOk = async () => {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.subimtForm = await getReqForm();
 | 
			
		||||
        state.submitForm = await getReqForm();
 | 
			
		||||
        await saveInstanceExec();
 | 
			
		||||
        ElMessage.success('保存成功');
 | 
			
		||||
        emit('val-change', state.form);
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
            </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>
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
</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';
 | 
			
		||||
@@ -91,7 +91,7 @@ const columns = ref([
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
const actionBtns = hasPerms([perms.saveInstance]);
 | 
			
		||||
const actionBtns = hasPerms(Object.values(perms));
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,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 +143,7 @@
 | 
			
		||||
                                    :db-id="dt.params.id"
 | 
			
		||||
                                    :db="dt.params.db"
 | 
			
		||||
                                    :db-type="dt.params.type"
 | 
			
		||||
                                    :flow-procdef-key="dt.params.flowProcdefKey"
 | 
			
		||||
                                    :height="state.tablesOpHeight"
 | 
			
		||||
                                />
 | 
			
		||||
                            </el-tab-pane>
 | 
			
		||||
@@ -151,12 +152,23 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </Pane>
 | 
			
		||||
        </Splitpanes>
 | 
			
		||||
        <db-table-op
 | 
			
		||||
            :title="tableCreateDialog.title"
 | 
			
		||||
            :active-name="tableCreateDialog.activeName"
 | 
			
		||||
            :dbId="tableCreateDialog.dbId"
 | 
			
		||||
            :db="tableCreateDialog.db"
 | 
			
		||||
            :dbType="tableCreateDialog.dbType"
 | 
			
		||||
            :flow-procdef-key="tableCreateDialog.flowProcdefKey"
 | 
			
		||||
            :data="tableCreateDialog.data"
 | 
			
		||||
            v-model:visible="tableCreateDialog.visible"
 | 
			
		||||
            @submit-sql="onSubmitEditTableSql"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } 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';
 | 
			
		||||
@@ -165,12 +177,14 @@ import { dbApi } from './api';
 | 
			
		||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
import { DbType, getDbDialect } from './dialect/index';
 | 
			
		||||
import { getDbDialect, schemaDbTypes } from './dialect/index';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
 | 
			
		||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
 | 
			
		||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
 | 
			
		||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
 | 
			
		||||
@@ -214,12 +228,26 @@ 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,
 | 
			
		||||
                flowProcdefKey: params.flowProcdefKey,
 | 
			
		||||
            },
 | 
			
		||||
            params.db
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
 | 
			
		||||
 | 
			
		||||
// tagpath 节点类型
 | 
			
		||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
 | 
			
		||||
        const dbInfos = dbInfoRes.list;
 | 
			
		||||
        if (!dbInfos) {
 | 
			
		||||
@@ -232,7 +260,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
 | 
			
		||||
            x.tagPath = parentNode.key;
 | 
			
		||||
            return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
    })
 | 
			
		||||
    .withContextMenuItems([ContextmenuItemRefresh]);
 | 
			
		||||
 | 
			
		||||
// 数据库实例节点类型
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
 | 
			
		||||
@@ -248,6 +277,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
 | 
			
		||||
                host: `${params.host}:${params.port}`,
 | 
			
		||||
                dbs: dbs,
 | 
			
		||||
                db: x,
 | 
			
		||||
                flowProcdefKey: params.flowProcdefKey,
 | 
			
		||||
            })
 | 
			
		||||
            .withIcon(DbIcon);
 | 
			
		||||
    });
 | 
			
		||||
@@ -255,12 +285,12 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
 | 
			
		||||
 | 
			
		||||
// 数据库节点
 | 
			
		||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
 | 
			
		||||
    .withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
 | 
			
		||||
    .withContextMenuItems([ContextmenuItemRefresh])
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const params = parentNode.params;
 | 
			
		||||
        params.parentKey = parentNode.key;
 | 
			
		||||
        // pg类数据库会多一层schema
 | 
			
		||||
        if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
 | 
			
		||||
            const params = parentNode.params;
 | 
			
		||||
        if (schemaDbTypes.includes(params.type)) {
 | 
			
		||||
            const { id, db } = params;
 | 
			
		||||
            const schemaNames = await dbApi.pgSchemas.request({ id, db });
 | 
			
		||||
            return schemaNames.map((sn: any) => {
 | 
			
		||||
@@ -269,33 +299,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
 | 
			
		||||
                nParams.schema = sn;
 | 
			
		||||
                nParams.db = nParams.db + '/' + sn;
 | 
			
		||||
                nParams.dbs = schemaNames;
 | 
			
		||||
                return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
 | 
			
		||||
                return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return [
 | 
			
		||||
            new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
 | 
			
		||||
            new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
 | 
			
		||||
        ];
 | 
			
		||||
        return NodeTypeTables(params);
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
 | 
			
		||||
const NodeTypeTables = (params: any) => {
 | 
			
		||||
    let tableKey = `${params.id}.${params.db}.table-menu`;
 | 
			
		||||
    let sqlKey = getSqlMenuNodeKey(params.id, params.db);
 | 
			
		||||
    return [
 | 
			
		||||
        new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
 | 
			
		||||
        new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
 | 
			
		||||
    ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// postgres schema模式
 | 
			
		||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
 | 
			
		||||
    .withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
 | 
			
		||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
 | 
			
		||||
    .withContextMenuItems([ContextmenuItemRefresh])
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const params = parentNode.params;
 | 
			
		||||
        return [
 | 
			
		||||
            new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
 | 
			
		||||
            new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
 | 
			
		||||
        ];
 | 
			
		||||
        params.parentKey = parentNode.key;
 | 
			
		||||
        return NodeTypeTables(params);
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
 | 
			
		||||
// 数据库表菜单节点
 | 
			
		||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
 | 
			
		||||
    .withContextMenuItems([
 | 
			
		||||
        new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
 | 
			
		||||
 | 
			
		||||
        ContextmenuItemRefresh,
 | 
			
		||||
        new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
 | 
			
		||||
        new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
 | 
			
		||||
            const params = data.params;
 | 
			
		||||
            addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
 | 
			
		||||
@@ -303,27 +337,33 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
 | 
			
		||||
    ])
 | 
			
		||||
    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
        const params = parentNode.params;
 | 
			
		||||
        let { id, db } = params;
 | 
			
		||||
        let { id, db, type, flowProcdefKey } = params;
 | 
			
		||||
        // 获取当前库的所有表信息
 | 
			
		||||
        let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
 | 
			
		||||
        state.reloadStatus = false;
 | 
			
		||||
        let dbTableSize = 0;
 | 
			
		||||
        const tablesNode = tables.map((x: any) => {
 | 
			
		||||
            dbTableSize += x.dataLength + x.indexLength;
 | 
			
		||||
            return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
 | 
			
		||||
            const tableSize = x.dataLength + x.indexLength;
 | 
			
		||||
            dbTableSize += tableSize;
 | 
			
		||||
            const key = `${id}.${db}.${x.tableName}`;
 | 
			
		||||
            return new TagTreeNode(key, x.tableName, NodeTypeTable)
 | 
			
		||||
                .withIsLeaf(true)
 | 
			
		||||
                .withParams({
 | 
			
		||||
                    id,
 | 
			
		||||
                    db,
 | 
			
		||||
                    type,
 | 
			
		||||
                    flowProcdefKey: flowProcdefKey,
 | 
			
		||||
                    key: key,
 | 
			
		||||
                    parentKey: parentNode.key,
 | 
			
		||||
                    tableName: x.tableName,
 | 
			
		||||
                    tableComment: x.tableComment,
 | 
			
		||||
                    size: formatByteSize(x.dataLength + x.indexLength, 1),
 | 
			
		||||
                    size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
 | 
			
		||||
                })
 | 
			
		||||
                .withIcon(TableIcon)
 | 
			
		||||
                .withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
 | 
			
		||||
        });
 | 
			
		||||
        // 设置父节点参数的表大小
 | 
			
		||||
        parentNode.params.dbTableSize = formatByteSize(dbTableSize);
 | 
			
		||||
        parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
 | 
			
		||||
        return tablesNode;
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
@@ -340,22 +380,24 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
 | 
			
		||||
        return sqls.map((x: any) => {
 | 
			
		||||
            return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
 | 
			
		||||
                .withIsLeaf(true)
 | 
			
		||||
                .withParams({
 | 
			
		||||
                    id,
 | 
			
		||||
                    db,
 | 
			
		||||
                    dbs,
 | 
			
		||||
                    sqlName: x.name,
 | 
			
		||||
                })
 | 
			
		||||
                .withParams({ id, db, dbs, sqlName: x.name })
 | 
			
		||||
                .withIcon(SqlIcon);
 | 
			
		||||
        });
 | 
			
		||||
    })
 | 
			
		||||
    .withNodeClickFunc(nodeClickChangeDb);
 | 
			
		||||
 | 
			
		||||
// 表节点类型
 | 
			
		||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
 | 
			
		||||
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)),
 | 
			
		||||
    ])
 | 
			
		||||
    .withNodeClickFunc((nodeData: TagTreeNode) => {
 | 
			
		||||
        const params = nodeData.params;
 | 
			
		||||
        loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
 | 
			
		||||
});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
// sql模板节点类型
 | 
			
		||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
 | 
			
		||||
@@ -385,9 +427,20 @@ const state = reactive({
 | 
			
		||||
        loading: true,
 | 
			
		||||
        version: '',
 | 
			
		||||
    },
 | 
			
		||||
    tableCreateDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        title: '',
 | 
			
		||||
        activeName: '',
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        db: '',
 | 
			
		||||
        dbType: '',
 | 
			
		||||
        flowProcdefKey: '',
 | 
			
		||||
        data: {},
 | 
			
		||||
        parentKey: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { nowDbInst } = toRefs(state);
 | 
			
		||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const serverInfoReqParam = ref({
 | 
			
		||||
    instanceId: 0,
 | 
			
		||||
@@ -408,7 +461,7 @@ onBeforeUnmount(() => {
 | 
			
		||||
 * 设置editor高度和数据表高度
 | 
			
		||||
 */
 | 
			
		||||
const setHeight = () => {
 | 
			
		||||
    state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
 | 
			
		||||
    state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
 | 
			
		||||
    state.tablesOpHeight = window.innerHeight - 225 + 'px';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -603,6 +656,124 @@ const reloadNode = (nodeKey: string) => {
 | 
			
		||||
    tagTreeRef.value.reloadNode(nodeKey);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onEditTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
 | 
			
		||||
    // data.label就是表名
 | 
			
		||||
    if (tableName) {
 | 
			
		||||
        state.tableCreateDialog.title = '修改表';
 | 
			
		||||
        let indexs = await dbApi.tableIndex.request({ id, db, tableName });
 | 
			
		||||
        let columns = await dbApi.columnMetadata.request({ id, db, tableName });
 | 
			
		||||
        let row = { tableName, tableComment };
 | 
			
		||||
        state.tableCreateDialog.data = { edit: true, row, indexs, columns };
 | 
			
		||||
        state.tableCreateDialog.parentKey = parentKey;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.tableCreateDialog.title = '创建表';
 | 
			
		||||
        state.tableCreateDialog.data = { edit: false, row: {} };
 | 
			
		||||
        state.tableCreateDialog.parentKey = key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.tableCreateDialog.activeName = '1';
 | 
			
		||||
    state.tableCreateDialog.dbId = id;
 | 
			
		||||
    state.tableCreateDialog.db = db;
 | 
			
		||||
    state.tableCreateDialog.dbType = type;
 | 
			
		||||
    state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
 | 
			
		||||
    state.tableCreateDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onDeleteTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
 | 
			
		||||
    await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 执行sql
 | 
			
		||||
    dbApi.sqlExec.request({ id, db, sql: `drop table ${getDbDialect(state.nowDbInst.type).quoteIdentifier(tableName)}` }).then(() => {
 | 
			
		||||
        if (flowProcdefKey) {
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            parentKey && reloadNode(parentKey);
 | 
			
		||||
        }, 1000);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onRenameTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey, flowProcdefKey } = 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,
 | 
			
		||||
        flowProcdefKey: flowProcdefKey,
 | 
			
		||||
        runSuccessCallback: () => {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                parentKey && reloadNode(parentKey);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onCopyTable = async (data: any) => {
 | 
			
		||||
    let { db, id, tableName, parentKey } = data.params;
 | 
			
		||||
 | 
			
		||||
    let checked = ref(false);
 | 
			
		||||
 | 
			
		||||
    // 弹出确认框,并选择是否复制数据
 | 
			
		||||
    await ElMessageBox({
 | 
			
		||||
        title: `复制表【${tableName}】`,
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
        //  icon: markRaw(Delete),
 | 
			
		||||
        message: () =>
 | 
			
		||||
            h(ElCheckbox, {
 | 
			
		||||
                label: '是否复制数据?',
 | 
			
		||||
                modelValue: checked.value,
 | 
			
		||||
                'onUpdate:modelValue': (val: boolean | string | number) => {
 | 
			
		||||
                    if (typeof val === 'boolean') {
 | 
			
		||||
                        checked.value = val;
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            }),
 | 
			
		||||
        callback: (action: string) => {
 | 
			
		||||
            if (action === 'confirm') {
 | 
			
		||||
                // 执行sql
 | 
			
		||||
                dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
 | 
			
		||||
                    ElMessage.success('复制成功');
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        parentKey && reloadNode(parentKey);
 | 
			
		||||
                    }, 1000);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onSubmitEditTableSql = () => {
 | 
			
		||||
    state.tableCreateDialog.visible = false;
 | 
			
		||||
    state.tableCreateDialog.data = { edit: false, row: {} };
 | 
			
		||||
    reloadNode(state.tableCreateDialog.parentKey);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取当前操作的数据库信息
 | 
			
		||||
 */
 | 
			
		||||
@@ -614,6 +785,7 @@ const getNowDbInfo = () => {
 | 
			
		||||
        name: di.name,
 | 
			
		||||
        type: di.type,
 | 
			
		||||
        host: di.host,
 | 
			
		||||
        flowProcdefKey: di.flowProcdefKey,
 | 
			
		||||
        dbName: state.db,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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,8 +45,10 @@
 | 
			
		||||
                            <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>
 | 
			
		||||
@@ -55,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>
 | 
			
		||||
@@ -85,9 +89,11 @@
 | 
			
		||||
                                </el-col>
 | 
			
		||||
 | 
			
		||||
                                <el-col :span="8">
 | 
			
		||||
                                    <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">
 | 
			
		||||
@@ -121,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>
 | 
			
		||||
@@ -181,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 { 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({
 | 
			
		||||
@@ -226,18 +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 = {
 | 
			
		||||
@@ -245,10 +263,11 @@ const basicFormData = {
 | 
			
		||||
    targetDbId: -1,
 | 
			
		||||
    dataSql: 'select * from',
 | 
			
		||||
    pageSize: 1000,
 | 
			
		||||
    updField: 'id',
 | 
			
		||||
    updField: '',
 | 
			
		||||
    updFieldVal: '0',
 | 
			
		||||
    fieldMap: [{ src: 'a', target: 'b' }],
 | 
			
		||||
    status: 1,
 | 
			
		||||
    duplicateStrategy: -1,
 | 
			
		||||
} as FormData;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -263,6 +282,7 @@ const state = reactive({
 | 
			
		||||
    previewRes: {} as any,
 | 
			
		||||
    previewDataSql: '',
 | 
			
		||||
    previewInsertSql: '',
 | 
			
		||||
    previewFieldArr: [] as string[],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { tabActiveName, form, submitForm } = toRefs(state);
 | 
			
		||||
@@ -287,6 +307,9 @@ watch(dialogVisible, async (newValue: boolean) => {
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
@@ -302,6 +325,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.srcInstName = db.instanceName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //  初始化target数据源
 | 
			
		||||
@@ -312,6 +337,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) {
 | 
			
		||||
@@ -331,13 +358,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();
 | 
			
		||||
@@ -353,17 +379,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; // 数据源里需要这个值
 | 
			
		||||
@@ -396,8 +424,8 @@ const handleGetSrcFields = async () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 判断sql是否是查询语句
 | 
			
		||||
    if (!/^select/i.test(state.form.dataSql!)) {
 | 
			
		||||
        let msg = 'sql语句错误,请输入查询语句';
 | 
			
		||||
    if (!/^select/i.test(state.form.dataSql.trim()!)) {
 | 
			
		||||
        let msg = 'sql语句错误,请输入select语句';
 | 
			
		||||
        ElMessage.warning(msg);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -410,10 +438,24 @@ const handleGetSrcFields = async () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 执行sql
 | 
			
		||||
    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: state.form.dataSql.trim() + ' limit 1',
 | 
			
		||||
        sql,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!res.columns) {
 | 
			
		||||
@@ -487,6 +529,11 @@ const btnOk = async () => {
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.form = basicFormData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDuplicateStrategy = () => {
 | 
			
		||||
    refreshPreviewInsertSql();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ export const dbApi = {
 | 
			
		||||
    tableInfos: Api.newGet('/dbs/{id}/t-infos'),
 | 
			
		||||
    tableIndex: Api.newGet('/dbs/{id}/t-index'),
 | 
			
		||||
    tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
 | 
			
		||||
    copyTable: Api.newPost('/dbs/{id}/copy-table'),
 | 
			
		||||
    columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
 | 
			
		||||
    pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
 | 
			
		||||
    // 获取表即列提示
 | 
			
		||||
@@ -34,7 +35,7 @@ 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}'),
 | 
			
		||||
@@ -48,16 +49,20 @@ export const dbApi = {
 | 
			
		||||
    // 获取数据库备份列表
 | 
			
		||||
    getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
 | 
			
		||||
    createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
 | 
			
		||||
    deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
 | 
			
		||||
    getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
 | 
			
		||||
    enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
 | 
			
		||||
    disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
 | 
			
		||||
    startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
 | 
			
		||||
    saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
 | 
			
		||||
    getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
 | 
			
		||||
    restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
 | 
			
		||||
    deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
 | 
			
		||||
 | 
			
		||||
    // 获取数据库备份列表
 | 
			
		||||
    // 获取数据库恢复列表
 | 
			
		||||
    getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
 | 
			
		||||
    createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
 | 
			
		||||
    deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
 | 
			
		||||
    getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
 | 
			
		||||
    enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
 | 
			
		||||
    disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
 | 
			
		||||
@@ -79,3 +84,8 @@ export const dbApi = {
 | 
			
		||||
    stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
 | 
			
		||||
    datasyncLogs: Api.newGet('/datasync/tasks/{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" />
 | 
			
		||||
@@ -19,7 +22,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
 | 
			
		||||
import { dbApi } from '@/views/ops/db/api';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
 | 
			
		||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
@@ -27,15 +30,21 @@ const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    },
 | 
			
		||||
    instName: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    dbName: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    tagPath: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    dbType: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'selectDb']);
 | 
			
		||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 树节点类型
 | 
			
		||||
@@ -53,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: () => {
 | 
			
		||||
        //
 | 
			
		||||
@@ -87,8 +96,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**  mysql类型的数据库,没有schema层 */
 | 
			
		||||
const mysqlType = (type: string) => {
 | 
			
		||||
    return type === DbType.mysql;
 | 
			
		||||
const noSchemaType = (type: string) => {
 | 
			
		||||
    return noSchemaTypes.includes(type);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 数据库实例节点类型
 | 
			
		||||
@@ -96,7 +105,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
 | 
			
		||||
    const params = parentNode.params;
 | 
			
		||||
    const dbs = params.database.split(' ')?.sort();
 | 
			
		||||
    let fn: NodeType;
 | 
			
		||||
    if (mysqlType(params.type)) {
 | 
			
		||||
    if (noSchemaType(params.type)) {
 | 
			
		||||
        fn = MysqlNodeTypes;
 | 
			
		||||
    } else {
 | 
			
		||||
        fn = PgNodeTypes;
 | 
			
		||||
@@ -114,7 +123,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
 | 
			
		||||
                db: x,
 | 
			
		||||
            })
 | 
			
		||||
            .withIcon(DbIcon);
 | 
			
		||||
        if (mysqlType(params.type)) {
 | 
			
		||||
        if (noSchemaType(params.type)) {
 | 
			
		||||
            tagTreeNode.isLeaf = true;
 | 
			
		||||
        }
 | 
			
		||||
        return tagTreeNode;
 | 
			
		||||
@@ -148,8 +157,10 @@ 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);
 | 
			
		||||
    emits('selectDb', params);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -128,12 +128,12 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { h, nextTick, onMounted, reactive, toRefs, ref, unref } from 'vue';
 | 
			
		||||
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
 | 
			
		||||
import { getToken } from '@/common/utils/storage';
 | 
			
		||||
import { notBlank } from '@/common/assert';
 | 
			
		||||
import { format as sqlFormatter } from 'sql-formatter';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
 | 
			
		||||
 | 
			
		||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
 | 
			
		||||
import { editor } from 'monaco-editor';
 | 
			
		||||
@@ -146,11 +146,9 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
 | 
			
		||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
 | 
			
		||||
import { ElNotification } from 'element-plus';
 | 
			
		||||
import syssocket from '@/common/syssocket';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { getDbDialect } from '../../dialect';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['saveSqlSuccess']);
 | 
			
		||||
 | 
			
		||||
@@ -299,13 +297,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: '确定',
 | 
			
		||||
@@ -322,6 +323,18 @@ const onRunSql = async (newTab = false) => {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 启用工单审批
 | 
			
		||||
    if (execRemark && getNowDbInst().flowProcdefKey) {
 | 
			
		||||
        try {
 | 
			
		||||
            await getNowDbInst().runSql(props.dbName, sql, execRemark);
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            ElMessage.success('工单提交失败');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let execRes: ExecResTab;
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    let id;
 | 
			
		||||
@@ -357,6 +370,7 @@ const onRunSql = async (newTab = false) => {
 | 
			
		||||
        const colAndData: any = data.value;
 | 
			
		||||
        if (!colAndData.res || colAndData.res.length === 0) {
 | 
			
		||||
            ElMessage.warning('未查询到结果集');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 要实时响应,故需要用索引改变数据才生效
 | 
			
		||||
@@ -453,7 +467,7 @@ const formatSql = () => {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
 | 
			
		||||
    const formatDialect = 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;
 | 
			
		||||
    flowProcdefKey?: string;
 | 
			
		||||
    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,14 @@
 | 
			
		||||
<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">
 | 
			
		||||
            <monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
 | 
			
		||||
            <el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
 | 
			
		||||
 | 
			
		||||
            <div v-if="props.flowProcdefKey">
 | 
			
		||||
                <el-divider content-position="left">审批节点</el-divider>
 | 
			
		||||
                <procdef-tasks :procdef-key="props.flowProcdefKey" />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <span class="dialog-footer">
 | 
			
		||||
                    <el-button @click="cancel">取 消</el-button>
 | 
			
		||||
@@ -14,47 +20,34 @@
 | 
			
		||||
</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
 | 
			
		||||
 */
 | 
			
		||||
@@ -67,12 +60,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.flowProcdefKey) {
 | 
			
		||||
            runSuccess = false;
 | 
			
		||||
            ElMessage.success('工单提交成功');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let re of res.res) {
 | 
			
		||||
            if (re.result !== 'success') {
 | 
			
		||||
                ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
 | 
			
		||||
@@ -84,45 +84,33 @@ const runSql = async () => {
 | 
			
		||||
        ElMessage.success('执行成功');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        runSuccess = false;
 | 
			
		||||
    }
 | 
			
		||||
    } finally {
 | 
			
		||||
        if (runSuccess) {
 | 
			
		||||
        if (runSuccessCallback) {
 | 
			
		||||
            runSuccessCallback();
 | 
			
		||||
            if (props.runSuccessCallback) {
 | 
			
		||||
                props.runSuccessCallback();
 | 
			
		||||
            }
 | 
			
		||||
        cancel();
 | 
			
		||||
        }
 | 
			
		||||
        state.btnLoading = false;
 | 
			
		||||
        cancel();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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' });
 | 
			
		||||
    state.dialogVisible = true;
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        remarkInputRef.value?.focus();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    }, 200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ open });
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
        <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"
 | 
			
		||||
@@ -16,9 +16,9 @@
 | 
			
		||||
    <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"
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
    <el-date-picker
 | 
			
		||||
        v-else-if="dataType == DataType.Date"
 | 
			
		||||
        :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        @change="emit('blur')"
 | 
			
		||||
        @blur="handleBlur"
 | 
			
		||||
        class="edit-time-picker mb4"
 | 
			
		||||
@@ -43,6 +44,7 @@
 | 
			
		||||
    <el-date-picker
 | 
			
		||||
        v-else-if="dataType == DataType.DateTime"
 | 
			
		||||
        :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        @change="handleBlur"
 | 
			
		||||
        @blur="handleBlur"
 | 
			
		||||
        class="edit-time-picker mb4"
 | 
			
		||||
@@ -58,6 +60,7 @@
 | 
			
		||||
    <el-time-picker
 | 
			
		||||
        v-else-if="dataType == DataType.Time"
 | 
			
		||||
        :ref="(el: any) => focus && el?.focus()"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        @change="handleBlur"
 | 
			
		||||
        @blur="handleBlur"
 | 
			
		||||
        class="edit-time-picker mb4"
 | 
			
		||||
@@ -71,7 +74,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Ref, ref, computed } from 'vue';
 | 
			
		||||
import { computed, ref, Ref } from 'vue';
 | 
			
		||||
import { ElInput } from 'element-plus';
 | 
			
		||||
import { DataType } from '../../dialect/index';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
@@ -83,11 +86,13 @@ export interface ColumnFormItemProps {
 | 
			
		||||
    focus?: boolean; // 是否获取焦点
 | 
			
		||||
    placeholder?: string;
 | 
			
		||||
    columnName?: string;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
 | 
			
		||||
    focus: false,
 | 
			
		||||
    dataType: DataType.String,
 | 
			
		||||
    disabled: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:modelValue', 'blur']);
 | 
			
		||||
@@ -178,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
 | 
			
		||||
    .el-input__prefix {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
    .el-input__inner {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.edit-time-picker-popper {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,14 +46,6 @@
 | 
			
		||||
                                            <b :title="column.remark" class="el-text" style="cursor: pointer">
 | 
			
		||||
                                                {{ column.title }}
 | 
			
		||||
                                            </b>
 | 
			
		||||
 | 
			
		||||
                                            <span>
 | 
			
		||||
                                                <SvgIcon
 | 
			
		||||
                                                    color="var(--el-color-primary)"
 | 
			
		||||
                                                    v-if="column.title == nowSortColumn?.columnName"
 | 
			
		||||
                                                    :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
 | 
			
		||||
                                                ></SvgIcon>
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
 | 
			
		||||
                                        <!-- 字段备注信息 -->
 | 
			
		||||
@@ -71,6 +63,13 @@
 | 
			
		||||
                                            {{ column.title }}
 | 
			
		||||
                                        </b>
 | 
			
		||||
                                    </div>
 | 
			
		||||
 | 
			
		||||
                                    <!-- 字段列右部分内容 -->
 | 
			
		||||
                                    <div class="column-right">
 | 
			
		||||
                                        <span v-if="column.title == nowSortColumn?.columnName">
 | 
			
		||||
                                            <SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
@@ -138,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';
 | 
			
		||||
@@ -154,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']);
 | 
			
		||||
 | 
			
		||||
@@ -247,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())
 | 
			
		||||
@@ -333,7 +352,11 @@ const state = reactive({
 | 
			
		||||
        },
 | 
			
		||||
        items: [] as ContextmenuItem[],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    tableDataFormDialog: {
 | 
			
		||||
        data: {},
 | 
			
		||||
        title: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
    },
 | 
			
		||||
    genTxtDialog: {
 | 
			
		||||
        title: 'SQL',
 | 
			
		||||
        visible: false,
 | 
			
		||||
@@ -444,7 +467,7 @@ const formatDataValues = (datas: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setTableData = (datas: any) => {
 | 
			
		||||
    tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
 | 
			
		||||
    tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
 | 
			
		||||
    selectionRowsMap.clear();
 | 
			
		||||
    cellUpdateMap.clear();
 | 
			
		||||
    formatDataValues(datas);
 | 
			
		||||
@@ -576,7 +599,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 });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -601,6 +624,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);
 | 
			
		||||
@@ -714,48 +749,28 @@ const submitUpdateFields = async () => {
 | 
			
		||||
 | 
			
		||||
    const db = state.db;
 | 
			
		||||
    let res = '';
 | 
			
		||||
    const dbDialect = getDbDialect(dbInst.type);
 | 
			
		||||
 | 
			
		||||
    for (let updateRow of cellUpdateMap.values()) {
 | 
			
		||||
        let sql = `UPDATE ${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;
 | 
			
		||||
        }
 | 
			
		||||
        res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        sql = sql.substring(0, sql.length - 1);
 | 
			
		||||
        sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
 | 
			
		||||
        res += sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbInst.promptExeSql(
 | 
			
		||||
        db,
 | 
			
		||||
        res,
 | 
			
		||||
        () => {},
 | 
			
		||||
        () => {
 | 
			
		||||
    dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
 | 
			
		||||
        triggerRefresh();
 | 
			
		||||
        cellUpdateMap.clear();
 | 
			
		||||
        changeUpdatedField();
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelUpdateFields = () => {
 | 
			
		||||
@@ -868,9 +883,15 @@ defineExpose({
 | 
			
		||||
        color: var(--el-color-info-light-3);
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: -7px;
 | 
			
		||||
        top: -5px;
 | 
			
		||||
        padding: 2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .column-right {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 2px;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        padding: 2px;
 | 
			
		||||
        height: 12px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,122 @@
 | 
			
		||||
<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.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="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';
 | 
			
		||||
import { getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
@@ -17,8 +17,8 @@
 | 
			
		||||
                            <el-checkbox
 | 
			
		||||
                                v-model="item.show"
 | 
			
		||||
                                :label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
 | 
			
		||||
                                :true-label="true"
 | 
			
		||||
                                :false-label="false"
 | 
			
		||||
                                :true-value="true"
 | 
			
		||||
                                :false-value="false"
 | 
			
		||||
                                size="small"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
@@ -55,7 +55,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>
 | 
			
		||||
@@ -158,21 +158,52 @@
 | 
			
		||||
            @data-delete="onRefresh"
 | 
			
		||||
        ></db-table-data>
 | 
			
		||||
 | 
			
		||||
        <el-row type="flex" class="mt5" justify="center">
 | 
			
		||||
            <el-pagination
 | 
			
		||||
                small
 | 
			
		||||
                :total="count"
 | 
			
		||||
                @size-change="handleSizeChange"
 | 
			
		||||
                @current-change="pageChange()"
 | 
			
		||||
                layout="prev, pager, next, total, sizes, jumper"
 | 
			
		||||
                v-model:current-page="pageNum"
 | 
			
		||||
                v-model:page-size="pageSize"
 | 
			
		||||
                :page-sizes="pageSizes"
 | 
			
		||||
            ></el-pagination>
 | 
			
		||||
        </el-row>
 | 
			
		||||
        <div style="font-size: 12px; padding: 0 10px; color: #606266">
 | 
			
		||||
            <span>{{ state.sql }}</span>
 | 
			
		||||
        <el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
 | 
			
		||||
            <el-col :span="12">
 | 
			
		||||
                <el-text
 | 
			
		||||
                    id="copyValue"
 | 
			
		||||
                    style="color: var(--el-color-info-light-3)"
 | 
			
		||||
                    class="is-truncated font12 mt5"
 | 
			
		||||
                    @click="copyToClipboard(sql)"
 | 
			
		||||
                    :title="sql"
 | 
			
		||||
                    >{{ sql }}</el-text
 | 
			
		||||
                >
 | 
			
		||||
            </el-col>
 | 
			
		||||
            <el-col :span="12">
 | 
			
		||||
                <el-row :gutter="10" justify="left">
 | 
			
		||||
                    <el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" title="首页" />
 | 
			
		||||
                    <el-link class="op-page" :underline="false" @click="pageNum = --pageNum || 1" :disabled="pageNum == 1" icon="Back" title="上一页" />
 | 
			
		||||
                    <div class="op-page">
 | 
			
		||||
                        <el-input-number
 | 
			
		||||
                            style="width: 50px"
 | 
			
		||||
                            :controls="false"
 | 
			
		||||
                            :min="1"
 | 
			
		||||
                            v-model="state.setPageNum"
 | 
			
		||||
                            size="small"
 | 
			
		||||
                            @blur="handleSetPageNum"
 | 
			
		||||
                            @keydown.enter="handleSetPageNum"
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
 | 
			
		||||
                    <el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
 | 
			
		||||
                    <div style="width: 90px" class="op-page ml10">
 | 
			
		||||
                        <el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
 | 
			
		||||
                            <el-option
 | 
			
		||||
                                style="font-size: 12px; height: 24px; line-height: 24px"
 | 
			
		||||
                                v-for="(op, i) in pageSizes"
 | 
			
		||||
                                :key="i"
 | 
			
		||||
                                :label="op + '条/页'"
 | 
			
		||||
                                :value="op"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <el-button @click="handleCount" :loading="state.counting" class="ml10" text bg size="small">
 | 
			
		||||
                        {{ state.showTotal ? `${state.total} 条` : 'count' }}
 | 
			
		||||
                    </el-button>
 | 
			
		||||
                </el-row>
 | 
			
		||||
            </el-col>
 | 
			
		||||
        </el-row>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
 | 
			
		||||
            <el-row>
 | 
			
		||||
@@ -203,31 +234,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.columnKey != 'PRI'"
 | 
			
		||||
                >
 | 
			
		||||
                    <ColumnFormItem
 | 
			
		||||
                        v-model="addDataDialog.data[`${column.columnName}`]"
 | 
			
		||||
                        :data-type="dbDialect.getDataType(column.columnType)"
 | 
			
		||||
                        :placeholder="`${column.columnType}  ${column.columnComment}`"
 | 
			
		||||
                        :column-name="column.columnName"
 | 
			
		||||
        <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"
 | 
			
		||||
        />
 | 
			
		||||
                </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>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -237,10 +253,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: {
 | 
			
		||||
@@ -261,7 +278,6 @@ const props = defineProps({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const dataForm: any = ref(null);
 | 
			
		||||
const dbTableRef: Ref = ref(null);
 | 
			
		||||
const condInputRef: Ref = ref(null);
 | 
			
		||||
const columnNameSearchInputRef: Ref = ref(null);
 | 
			
		||||
@@ -289,7 +305,10 @@ const state = reactive({
 | 
			
		||||
        defaultPageSize * 40,
 | 
			
		||||
        defaultPageSize * 80,
 | 
			
		||||
    ],
 | 
			
		||||
    count: 0,
 | 
			
		||||
    setPageNum: 0,
 | 
			
		||||
    total: 0,
 | 
			
		||||
    showTotal: false,
 | 
			
		||||
    counting: false,
 | 
			
		||||
    selectionDatas: [] as any,
 | 
			
		||||
    condPopVisible: false,
 | 
			
		||||
    columnNameSearch: '',
 | 
			
		||||
@@ -305,7 +324,6 @@ const state = reactive({
 | 
			
		||||
    addDataDialog: {
 | 
			
		||||
        data: {},
 | 
			
		||||
        title: '',
 | 
			
		||||
        placeholder: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
    },
 | 
			
		||||
    tableHeight: '600px',
 | 
			
		||||
@@ -313,7 +331,7 @@ const state = reactive({
 | 
			
		||||
    dbDialect: {} as DbDialect,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
 | 
			
		||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.tableHeight,
 | 
			
		||||
@@ -331,7 +349,7 @@ onMounted(async () => {
 | 
			
		||||
    state.tableHeight = props.tableHeight;
 | 
			
		||||
    await onRefresh();
 | 
			
		||||
 | 
			
		||||
    state.dbDialect = getDbDialect(getNowDbInst().type);
 | 
			
		||||
    state.dbDialect = getNowDbInst().getDialect();
 | 
			
		||||
    useEventListener('click', handlerWindowClick);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -346,18 +364,19 @@ const onRefresh = async () => {
 | 
			
		||||
    await selectData();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据tab修改页数
 | 
			
		||||
 */
 | 
			
		||||
const pageChange = async () => {
 | 
			
		||||
watch(
 | 
			
		||||
    () => state.pageNum,
 | 
			
		||||
    async () => {
 | 
			
		||||
        await selectData();
 | 
			
		||||
};
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 单表数据信息查询数据
 | 
			
		||||
 */
 | 
			
		||||
const selectData = async () => {
 | 
			
		||||
    state.loading = true;
 | 
			
		||||
    state.setPageNum = state.pageNum;
 | 
			
		||||
    const dbInst = getNowDbInst();
 | 
			
		||||
    const db = props.dbName;
 | 
			
		||||
    const table = props.tableName;
 | 
			
		||||
@@ -370,16 +389,10 @@ const selectData = async () => {
 | 
			
		||||
            state.columns = columns;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
 | 
			
		||||
        state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
 | 
			
		||||
        let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
 | 
			
		||||
        let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
 | 
			
		||||
        state.sql = sql;
 | 
			
		||||
        if (state.count > 0) {
 | 
			
		||||
        const colAndData: any = await dbInst.runSql(db, sql);
 | 
			
		||||
        state.datas = colAndData.res;
 | 
			
		||||
        } else {
 | 
			
		||||
            state.datas = [];
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
@@ -391,6 +404,33 @@ const handleSizeChange = async (size: any) => {
 | 
			
		||||
    await selectData();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleEndPage = async () => {
 | 
			
		||||
    await handleCount();
 | 
			
		||||
    state.pageNum = Math.ceil(state.total / state.pageSize);
 | 
			
		||||
    await selectData();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleSetPageNum = async () => {
 | 
			
		||||
    state.pageNum = state.setPageNum;
 | 
			
		||||
    await selectData();
 | 
			
		||||
};
 | 
			
		||||
const handleCount = async () => {
 | 
			
		||||
    state.counting = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const db = props.dbName;
 | 
			
		||||
        const table = props.tableName;
 | 
			
		||||
        const dbInst = getNowDbInst();
 | 
			
		||||
        const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
 | 
			
		||||
        state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
 | 
			
		||||
        state.showTotal = true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        /* empty */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.counting = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
 | 
			
		||||
let completeCond = '';
 | 
			
		||||
// 是否存在列建议
 | 
			
		||||
@@ -484,7 +524,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.columnType, conditionDialog.value!);
 | 
			
		||||
    onCancelCondition();
 | 
			
		||||
    condInputRef.value.focus();
 | 
			
		||||
};
 | 
			
		||||
@@ -543,40 +583,10 @@ 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(',');
 | 
			
		||||
            let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
 | 
			
		||||
            dbInst.promptExeSql(props.dbName, sql, null, () => {
 | 
			
		||||
                closeAddDataDialog();
 | 
			
		||||
                onRefresh();
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            ElMessage.error('请正确填写数据信息');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.op-page {
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%" :close-on-press-escape="false" :close-on-click-modal="false">
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="70%" :close-on-press-escape="false" :close-on-click-modal="false">
 | 
			
		||||
            <el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
 | 
			
		||||
                <el-row>
 | 
			
		||||
                    <el-col :span="12">
 | 
			
		||||
@@ -26,11 +26,11 @@
 | 
			
		||||
                                :width="item.width"
 | 
			
		||||
                            >
 | 
			
		||||
                                <template #default="scope">
 | 
			
		||||
                                    <el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
 | 
			
		||||
                                    <el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
 | 
			
		||||
 | 
			
		||||
                                    <el-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"
 | 
			
		||||
@@ -42,35 +42,30 @@
 | 
			
		||||
                                        </el-option>
 | 
			
		||||
                                    </el-select>
 | 
			
		||||
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value" />
 | 
			
		||||
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'length'" type="number" size="small" v-model.number="scope.row.length" />
 | 
			
		||||
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'numScale'" type="number" size="small" v-model.number="scope.row.numScale" />
 | 
			
		||||
 | 
			
		||||
                                    <el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
 | 
			
		||||
                                    <el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull" />
 | 
			
		||||
 | 
			
		||||
                                    <el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
 | 
			
		||||
                                    <el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri" />
 | 
			
		||||
 | 
			
		||||
                                    <el-checkbox
 | 
			
		||||
                                        v-else-if="item.prop === 'auto_increment'"
 | 
			
		||||
                                        size="small"
 | 
			
		||||
                                        v-model="scope.row.auto_increment"
 | 
			
		||||
                                        :disabled="dbType === DbType.postgresql"
 | 
			
		||||
                                    >
 | 
			
		||||
                                    </el-checkbox>
 | 
			
		||||
                                        :disabled="disableEditIncr()"
 | 
			
		||||
                                    />
 | 
			
		||||
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
 | 
			
		||||
                                    <el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
 | 
			
		||||
 | 
			
		||||
                                    <el-link
 | 
			
		||||
                                        v-else-if="item.prop === 'action'"
 | 
			
		||||
                                        type="danger"
 | 
			
		||||
                                        plain
 | 
			
		||||
                                        size="small"
 | 
			
		||||
                                        :underline="false"
 | 
			
		||||
                                        @click.prevent="deleteRow(scope.$index)"
 | 
			
		||||
                                        >删除</el-link
 | 
			
		||||
                                    >
 | 
			
		||||
                                    <el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <el-link type="danger" plain size="small" :underline="false">删除</el-link>
 | 
			
		||||
                                        </template>
 | 
			
		||||
                                    </el-popconfirm>
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-table-column>
 | 
			
		||||
                        </el-table>
 | 
			
		||||
@@ -104,21 +99,15 @@
 | 
			
		||||
                                    <el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
 | 
			
		||||
                                    </el-checkbox>
 | 
			
		||||
 | 
			
		||||
                                    <el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
 | 
			
		||||
                                        <el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
 | 
			
		||||
                                    </el-select>
 | 
			
		||||
                                    <el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
 | 
			
		||||
 | 
			
		||||
                                    <el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
 | 
			
		||||
 | 
			
		||||
                                    <el-link
 | 
			
		||||
                                        v-if="item.prop === 'action'"
 | 
			
		||||
                                        type="danger"
 | 
			
		||||
                                        plain
 | 
			
		||||
                                        size="small"
 | 
			
		||||
                                        :underline="false"
 | 
			
		||||
                                        @click.prevent="deleteIndex(scope.$index)"
 | 
			
		||||
                                        >删除</el-link
 | 
			
		||||
                                    >
 | 
			
		||||
                                    <el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <el-link type="danger" plain size="small" :underline="false">删除</el-link>
 | 
			
		||||
                                        </template>
 | 
			
		||||
                                    </el-popconfirm>
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-table-column>
 | 
			
		||||
                        </el-table>
 | 
			
		||||
@@ -130,6 +119,7 @@
 | 
			
		||||
                </el-tabs>
 | 
			
		||||
            </el-form>
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <el-button @click="cancel()">取消</el-button>
 | 
			
		||||
                <el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
@@ -161,12 +151,15 @@ const props = defineProps({
 | 
			
		||||
    dbType: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
    flowProcdefKey: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
 | 
			
		||||
 | 
			
		||||
const dbDialect = getDbDialect(props.dbType);
 | 
			
		||||
let dbDialect = getDbDialect(props.dbType);
 | 
			
		||||
 | 
			
		||||
type ColName = {
 | 
			
		||||
    prop: string;
 | 
			
		||||
@@ -179,30 +172,33 @@ const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    btnloading: false,
 | 
			
		||||
    activeName: '1',
 | 
			
		||||
    columnTypeList: dbDialect.getInfo().columnTypes,
 | 
			
		||||
    indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
 | 
			
		||||
    tableData: {
 | 
			
		||||
        fields: {
 | 
			
		||||
            colNames: [
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'name',
 | 
			
		||||
                    label: '字段名称',
 | 
			
		||||
                    width: 200,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'type',
 | 
			
		||||
                    label: '字段类型',
 | 
			
		||||
                    width: 120,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'length',
 | 
			
		||||
                    label: '长度',
 | 
			
		||||
                    width: 120,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'numScale',
 | 
			
		||||
                    label: '小数点',
 | 
			
		||||
                    width: 120,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    prop: 'value',
 | 
			
		||||
                    label: '默认值',
 | 
			
		||||
                    width: 120,
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                {
 | 
			
		||||
@@ -231,6 +227,7 @@ const state = reactive({
 | 
			
		||||
                },
 | 
			
		||||
            ] as ColName[],
 | 
			
		||||
            res: [] as RowDefinition[],
 | 
			
		||||
            oldFields: [] as RowDefinition[],
 | 
			
		||||
        },
 | 
			
		||||
        indexs: {
 | 
			
		||||
            colNames: [
 | 
			
		||||
@@ -261,19 +258,36 @@ const state = reactive({
 | 
			
		||||
            ],
 | 
			
		||||
            columns: [{ name: '', remark: '' }],
 | 
			
		||||
            res: [] as IndexDefinition[],
 | 
			
		||||
            oldIndexs: [] as IndexDefinition[],
 | 
			
		||||
        },
 | 
			
		||||
        tableName: '',
 | 
			
		||||
        tableComment: '',
 | 
			
		||||
        oldTableName: '',
 | 
			
		||||
        oldTableComment: '',
 | 
			
		||||
        height: 450,
 | 
			
		||||
        db: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
 | 
			
		||||
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
    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();
 | 
			
		||||
@@ -320,6 +334,7 @@ const submit = async () => {
 | 
			
		||||
        dbId: props.dbId as any,
 | 
			
		||||
        db: props.db as any,
 | 
			
		||||
        dbType: dbDialect.getInfo().formatSqlDialect,
 | 
			
		||||
        flowProcdefKey: props.flowProcdefKey,
 | 
			
		||||
        runSuccessCallback: () => {
 | 
			
		||||
            emit('submit-sql', { tableName: state.tableData.tableName });
 | 
			
		||||
            // cancel();
 | 
			
		||||
@@ -333,22 +348,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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -359,8 +377,12 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
    nowArr.forEach((a) => {
 | 
			
		||||
        let k = a[key];
 | 
			
		||||
        newMap[k] = a;
 | 
			
		||||
        if (!oldMap.hasOwnProperty(k)) {
 | 
			
		||||
        // 取oldName,因为修改了name,但是oldName不会变
 | 
			
		||||
        let oldName = a['oldName'];
 | 
			
		||||
        oldName && (newMap[oldName] = a);
 | 
			
		||||
        if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
 | 
			
		||||
            // 新增
 | 
			
		||||
            data.changed = true;
 | 
			
		||||
            data.add.push(a);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
@@ -370,13 +392,15 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
        let newData = newMap[k];
 | 
			
		||||
        if (!newData) {
 | 
			
		||||
            // 删除
 | 
			
		||||
            data.changed = true;
 | 
			
		||||
            data.del.push(a);
 | 
			
		||||
        } else {
 | 
			
		||||
            // 判断每个字段是否相等,否则为修改
 | 
			
		||||
            for (let f in a) {
 | 
			
		||||
                let oldV = a[f];
 | 
			
		||||
                let newV = newData[f];
 | 
			
		||||
                if (oldV.toString() !== newV.toString()) {
 | 
			
		||||
                if (oldV?.toString() !== newV?.toString()) {
 | 
			
		||||
                    data.changed = true;
 | 
			
		||||
                    data.upd.push(newData);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
@@ -388,24 +412,31 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
 | 
			
		||||
 | 
			
		||||
const genSql = () => {
 | 
			
		||||
    let data = state.tableData;
 | 
			
		||||
    console.log(data);
 | 
			
		||||
    // 创建表
 | 
			
		||||
    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(oldData.fields, state.tableData.fields.res, 'name');
 | 
			
		||||
            return dbDialect.getModifyColumnSql(data.tableName, changeData);
 | 
			
		||||
        } else if (state.activeName === '2') {
 | 
			
		||||
        let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
 | 
			
		||||
        let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
 | 
			
		||||
        // 修改索引
 | 
			
		||||
            let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
 | 
			
		||||
            return dbDialect.getModifyIndexSql(data.tableName, changeData);
 | 
			
		||||
        }
 | 
			
		||||
        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(';');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -414,28 +445,10 @@ const reset = () => {
 | 
			
		||||
    formRef.value.resetFields();
 | 
			
		||||
    state.tableData.tableName = '';
 | 
			
		||||
    state.tableData.tableComment = '';
 | 
			
		||||
    state.tableData.fields.res = [
 | 
			
		||||
        {
 | 
			
		||||
            name: '',
 | 
			
		||||
            type: '',
 | 
			
		||||
            value: '',
 | 
			
		||||
            length: '',
 | 
			
		||||
            numScale: '',
 | 
			
		||||
            notNull: false,
 | 
			
		||||
            pri: false,
 | 
			
		||||
            auto_increment: false,
 | 
			
		||||
            remark: '',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    state.tableData.indexs.res = [
 | 
			
		||||
        {
 | 
			
		||||
            indexName: '',
 | 
			
		||||
            columnNames: [],
 | 
			
		||||
            unique: false,
 | 
			
		||||
            indexType: 'BTREE',
 | 
			
		||||
            indexComment: '',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    state.tableData.fields.res = [];
 | 
			
		||||
    state.tableData.fields.oldFields = [];
 | 
			
		||||
    state.tableData.indexs.res = [];
 | 
			
		||||
    state.tableData.indexs.oldIndexs = [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const indexChanges = (row: any) => {
 | 
			
		||||
@@ -456,7 +469,21 @@ const indexChanges = (row: any) => {
 | 
			
		||||
    row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const oldData = { indexs: [] as any[], fields: [] as RowDefinition[] };
 | 
			
		||||
const disableEditIncr = () => {
 | 
			
		||||
    if (DbType.postgresql === props.dbType) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 如果是mssql则不能修改自增
 | 
			
		||||
    if (props.data?.edit) {
 | 
			
		||||
        if (DbType.mssql === props.dbType) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.data,
 | 
			
		||||
    (newValue: any) => {
 | 
			
		||||
@@ -464,37 +491,49 @@ watch(
 | 
			
		||||
        // 回显表名表注释
 | 
			
		||||
        state.tableData.tableName = row.tableName;
 | 
			
		||||
        state.tableData.tableComment = row.tableComment;
 | 
			
		||||
        // 回显列
 | 
			
		||||
        if (columns && Array.isArray(columns) && columns.length > 0) {
 | 
			
		||||
            oldData.fields = [];
 | 
			
		||||
        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 = [];
 | 
			
		||||
        // 回显列
 | 
			
		||||
        if (columns && Array.isArray(columns) && columns.length > 0) {
 | 
			
		||||
            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, '');
 | 
			
		||||
                    // 解决高斯的默认值问题
 | 
			
		||||
                    defaultValue = defaultValue.replace("'::character varying", '');
 | 
			
		||||
                }
 | 
			
		||||
                let data = {
 | 
			
		||||
                    name: a.columnName,
 | 
			
		||||
                    oldName: a.columnName,
 | 
			
		||||
                    type,
 | 
			
		||||
                    value: a.columnDefault || '',
 | 
			
		||||
                    value: defaultValue,
 | 
			
		||||
                    length,
 | 
			
		||||
                    numScale: a.numScale,
 | 
			
		||||
                    notNull: a.nullable !== 'YES',
 | 
			
		||||
                    pri: a.columnKey === 'PRI',
 | 
			
		||||
                    auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
 | 
			
		||||
                    pri: a.isPrimaryKey,
 | 
			
		||||
                    auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
 | 
			
		||||
                    remark: a.columnComment,
 | 
			
		||||
                };
 | 
			
		||||
                state.tableData.fields.res.push(data);
 | 
			
		||||
                oldData.fields.push(JSON.parse(JSON.stringify(data)));
 | 
			
		||||
                state.tableData.fields.oldFields.push(JSON.parse(JSON.stringify(data)));
 | 
			
		||||
                // 索引字段下拉选项
 | 
			
		||||
                state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 回显索引
 | 
			
		||||
        if (indexs && Array.isArray(indexs) && indexs.length > 0) {
 | 
			
		||||
            oldData.indexs = [];
 | 
			
		||||
            state.tableData.indexs.res = [];
 | 
			
		||||
            // 索引过滤掉主键
 | 
			
		||||
            indexs
 | 
			
		||||
                .filter((a) => a.indexName !== 'PRIMARY')
 | 
			
		||||
@@ -502,12 +541,12 @@ watch(
 | 
			
		||||
                    let data = {
 | 
			
		||||
                        indexName: a.indexName,
 | 
			
		||||
                        columnNames: a.columnName?.split(','),
 | 
			
		||||
                        unique: a.nonUnique === 0 || false,
 | 
			
		||||
                        unique: a.isUnique || false,
 | 
			
		||||
                        indexType: a.indexType,
 | 
			
		||||
                        indexComment: a.indexComment,
 | 
			
		||||
                    };
 | 
			
		||||
                    state.tableData.indexs.res.push(data);
 | 
			
		||||
                    oldData.indexs.push(JSON.parse(JSON.stringify(data)));
 | 
			
		||||
                    state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -68,9 +68,7 @@
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
 | 
			
		||||
                    <el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
 | 
			
		||||
                    <el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
 | 
			
		||||
                        >编辑表</el-link
 | 
			
		||||
                    >
 | 
			
		||||
                    <el-link class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
 | 
			
		||||
                    <el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
@@ -110,6 +108,7 @@
 | 
			
		||||
            :dbId="dbId"
 | 
			
		||||
            :db="db"
 | 
			
		||||
            :dbType="dbType"
 | 
			
		||||
            :flow-procdef-key="props.flowProcdefKey"
 | 
			
		||||
            :data="tableCreateDialog.data"
 | 
			
		||||
            v-model:visible="tableCreateDialog.visible"
 | 
			
		||||
            @submit-sql="onSubmitSql"
 | 
			
		||||
@@ -127,7 +126,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
import { compatibleMysql, DbType } from '../../dialect/index';
 | 
			
		||||
import { compatibleMysql, editDbTypes } from '../../dialect/index';
 | 
			
		||||
 | 
			
		||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -148,6 +147,9 @@ const props = defineProps({
 | 
			
		||||
        type: [String],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    flowProcdefKey: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -181,7 +183,6 @@ const state = reactive({
 | 
			
		||||
        visible: false,
 | 
			
		||||
        activeName: '1',
 | 
			
		||||
        type: '',
 | 
			
		||||
        enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
 | 
			
		||||
        data: {
 | 
			
		||||
            // 修改表时,传递修改数据
 | 
			
		||||
            edit: false,
 | 
			
		||||
@@ -320,6 +321,7 @@ const dropTable = async (row: any) => {
 | 
			
		||||
            sql: `DROP TABLE ${tableName}`,
 | 
			
		||||
            dbId: props.dbId as any,
 | 
			
		||||
            db: props.db as any,
 | 
			
		||||
            flowProcdefKey: props.flowProcdefKey,
 | 
			
		||||
            runSuccessCallback: async () => {
 | 
			
		||||
                await getTables();
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
 | 
			
		||||
 | 
			
		||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
 | 
			
		||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
 | 
			
		||||
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
 | 
			
		||||
 | 
			
		||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
 | 
			
		||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
 | 
			
		||||
 | 
			
		||||
const dbInstCache: Map<number, DbInst> = new Map();
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +40,11 @@ export class DbInst {
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程定义key,若存在则需要审批执行
 | 
			
		||||
     */
 | 
			
		||||
    flowProcdefKey: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * dbName -> db
 | 
			
		||||
     */
 | 
			
		||||
@@ -58,17 +67,23 @@ export class DbInst {
 | 
			
		||||
        if (!dbName) {
 | 
			
		||||
            throw new Error('dbName不能为空');
 | 
			
		||||
        }
 | 
			
		||||
        let db = this.dbs.get(dbName);
 | 
			
		||||
        let key = `${this.id}_${dbName}`;
 | 
			
		||||
        let db = this.dbs.get(key);
 | 
			
		||||
        if (db) {
 | 
			
		||||
            return db;
 | 
			
		||||
        }
 | 
			
		||||
        console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
 | 
			
		||||
        db = new Db();
 | 
			
		||||
        db.name = dbName;
 | 
			
		||||
        this.dbs.set(dbName, db);
 | 
			
		||||
        this.dbs.set(key, db);
 | 
			
		||||
        return db;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 获取数据库实例方言
 | 
			
		||||
    getDialect(): DbDialect {
 | 
			
		||||
        return getDbDialect(this.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 加载数据库表信息
 | 
			
		||||
     * @param dbName 数据库名
 | 
			
		||||
@@ -77,17 +92,22 @@ export class DbInst {
 | 
			
		||||
     */
 | 
			
		||||
    async loadTables(dbName: string, reload?: boolean) {
 | 
			
		||||
        const db = this.getDb(dbName);
 | 
			
		||||
        // 优先从 table map中获取
 | 
			
		||||
        let tables = db.tables;
 | 
			
		||||
        let key = this.dbTablesKey(dbName);
 | 
			
		||||
        let tables = tableStorage.value.get(key);
 | 
			
		||||
        // 优先从 table 缓存中获取
 | 
			
		||||
        if (!reload && tables) {
 | 
			
		||||
            db.tables = tables;
 | 
			
		||||
            return tables;
 | 
			
		||||
        }
 | 
			
		||||
        // 重置列信息缓存与表提示信息
 | 
			
		||||
        db.columnsMap?.clear();
 | 
			
		||||
        db.tableHints = null;
 | 
			
		||||
        console.log(`load tables -> dbName: ${dbName}`);
 | 
			
		||||
        tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
 | 
			
		||||
        tableStorage.value.set(key, tables);
 | 
			
		||||
        db.tables = tables;
 | 
			
		||||
 | 
			
		||||
        // 异步加载表提示信息
 | 
			
		||||
        this.loadDbHints(dbName, true).then(() => {});
 | 
			
		||||
        return tables;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -146,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}`);
 | 
			
		||||
@@ -169,18 +189,30 @@ export class DbInst {
 | 
			
		||||
        return this.getDb(dbName).getColumn(table, columnName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbTableHintsKey(dbName: string) {
 | 
			
		||||
        return `db-table-hints_${this.id}_${dbName}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbTablesKey(dbName: string) {
 | 
			
		||||
        return `db-tables_${this.id}_${dbName}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取库信息提示
 | 
			
		||||
     */
 | 
			
		||||
    async loadDbHints(dbName: string) {
 | 
			
		||||
    async loadDbHints(dbName: string, reload?: boolean) {
 | 
			
		||||
        const db = this.getDb(dbName);
 | 
			
		||||
        if (db.tableHints) {
 | 
			
		||||
            return db.tableHints;
 | 
			
		||||
        let key = this.dbTableHintsKey(dbName);
 | 
			
		||||
        let hints = hintsStorage.value.get(key);
 | 
			
		||||
        if (!reload && hints) {
 | 
			
		||||
            db.tableHints = hints;
 | 
			
		||||
            return hints;
 | 
			
		||||
        }
 | 
			
		||||
        console.log(`load db-hits -> dbName: ${dbName}`);
 | 
			
		||||
        const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
 | 
			
		||||
        db.tableHints = hits;
 | 
			
		||||
        return hits;
 | 
			
		||||
        hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
 | 
			
		||||
        db.tableHints = hints;
 | 
			
		||||
        hintsStorage.value.set(key, hints);
 | 
			
		||||
        return hints;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -225,8 +257,8 @@ export class DbInst {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 获取指定表的默认查询sql
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
 | 
			
		||||
        return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
 | 
			
		||||
        return this.getDialect().getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -234,12 +266,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) {
 | 
			
		||||
@@ -247,23 +289,59 @@ export class DbInst {
 | 
			
		||||
            let values = [];
 | 
			
		||||
            for (let column of columns) {
 | 
			
		||||
                const colName = column.columnName;
 | 
			
		||||
                colNames.push(this.wrapName(colName));
 | 
			
		||||
                values.push(DbInst.wrapValueByType(data[colName]));
 | 
			
		||||
                if (skipNull && data[colName] == null) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
 | 
			
		||||
                colNames.push(this.wrapName(colName));
 | 
			
		||||
                values.push(dbDialect.wrapValue(column.dataType, data[colName]));
 | 
			
		||||
            }
 | 
			
		||||
            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.columnType;
 | 
			
		||||
        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.columnType, 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.columnType, d[primaryKeyColumnName])}`).join(',');
 | 
			
		||||
        return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -275,19 +353,20 @@ export class DbInst {
 | 
			
		||||
            sql,
 | 
			
		||||
            dbId: this.id,
 | 
			
		||||
            db,
 | 
			
		||||
            dbType: this.getDialect().getInfo().formatSqlDialect,
 | 
			
		||||
            runSuccessCallback: successFunc,
 | 
			
		||||
            cancelCallback: cancelFunc,
 | 
			
		||||
            flowProcdefKey: this.flowProcdefKey,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
 | 
			
		||||
     * @param table
 | 
			
		||||
     * @param condition
 | 
			
		||||
     * @returns
 | 
			
		||||
     * @param name 表名、字段名、schema名
 | 
			
		||||
     * @returns 包裹后的字符串
 | 
			
		||||
     */
 | 
			
		||||
    wrapName = (name: string) => {
 | 
			
		||||
        return getDbDialect(this.type).quoteIdentifier(name);
 | 
			
		||||
        return this.getDialect().quoteIdentifier(name);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -311,6 +390,7 @@ export class DbInst {
 | 
			
		||||
        dbInst.name = inst.name;
 | 
			
		||||
        dbInst.type = inst.type;
 | 
			
		||||
        dbInst.databases = inst.databases;
 | 
			
		||||
        dbInst.flowProcdefKey = inst.flowProcdefKey;
 | 
			
		||||
 | 
			
		||||
        dbInstCache.set(dbInst.id, dbInst);
 | 
			
		||||
        return dbInst;
 | 
			
		||||
@@ -340,34 +420,6 @@ 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 字段类型
 | 
			
		||||
@@ -441,7 +493,7 @@ class Db {
 | 
			
		||||
    getColumn(table: string, columnName: string = '') {
 | 
			
		||||
        const cols = this.getColumns(table);
 | 
			
		||||
        if (!columnName) {
 | 
			
		||||
            const col = cols.find((c: any) => c.columnKey == 'PRI');
 | 
			
		||||
            const col = cols.find((c: any) => c.isPrimaryKey);
 | 
			
		||||
            return col || cols[0];
 | 
			
		||||
        }
 | 
			
		||||
        return cols.find((c: any) => c.columnName == columnName);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import {
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
@@ -54,6 +55,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
    { udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://eco.dameng.com/document/dm/zh-cn/pm/function.html
 | 
			
		||||
const replaceFunctions: EditorCompletionItem[] = [
 | 
			
		||||
    //  数值函数
 | 
			
		||||
    { label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
 | 
			
		||||
@@ -365,21 +367,22 @@ class DMDialect implements DbDialect {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        dmDialectInfo = {
 | 
			
		||||
            name: 'DM',
 | 
			
		||||
            icon: 'iconfont icon-db-dm',
 | 
			
		||||
            defaultPort: 5236,
 | 
			
		||||
            formatSqlDialect: 'postgresql',
 | 
			
		||||
            formatSqlDialect: 'plsql',
 | 
			
		||||
            columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
 | 
			
		||||
            editorCompletions,
 | 
			
		||||
        };
 | 
			
		||||
        return dmDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getPageSql(pageNum: number, limit: number) {
 | 
			
		||||
        return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
 | 
			
		||||
        return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultRows(): RowDefinition[] {
 | 
			
		||||
@@ -500,7 +503,9 @@ class DMDialect implements DbDialect {
 | 
			
		||||
        // 默认值
 | 
			
		||||
        let defVal = this.getDefaultValueSql(cl);
 | 
			
		||||
        let incr = cl.auto_increment ? 'IDENTITY' : '';
 | 
			
		||||
        return ` "${cl.name}" ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
 | 
			
		||||
        // 如果有原名以原名为准
 | 
			
		||||
        let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
 | 
			
		||||
        return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCreateTableSql(data: any): string {
 | 
			
		||||
@@ -546,35 +551,78 @@ class DMDialect implements DbDialect {
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let modifySql = '';
 | 
			
		||||
        let dropSql = '';
 | 
			
		||||
        let renameSql = '';
 | 
			
		||||
        let commentSql = '';
 | 
			
		||||
 | 
			
		||||
        // 主键字段
 | 
			
		||||
        let priArr = new Set();
 | 
			
		||||
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
                modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                    commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
 | 
			
		||||
                }
 | 
			
		||||
                if (a.pri) {
 | 
			
		||||
                    priArr.add(`"${a.name}"`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
                let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
 | 
			
		||||
                if (a.remark && a.oldName === a.name) {
 | 
			
		||||
                    commentSql += cmtSql;
 | 
			
		||||
                }
 | 
			
		||||
                // 修改了字段名
 | 
			
		||||
                if (a.oldName !== a.name) {
 | 
			
		||||
                    renameSql += `ALTER TABLE ${dbTable} RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)};`;
 | 
			
		||||
                    if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                        commentSql += cmtSql;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                modifySql += `ALTER TABLE ${dbTable} MODIFY ${this.genColumnBasicSql(a)};`;
 | 
			
		||||
                if (a.pri) {
 | 
			
		||||
                    priArr.add(`${this.quoteIdentifier(a.name)}`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
 | 
			
		||||
                dropSql += `ALTER TABLE ${dbTable} DROP COLUMN ${a.name};`;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
 | 
			
		||||
        // 编辑主键
 | 
			
		||||
        let dropPkSql = '';
 | 
			
		||||
        if (priArr.size > 0) {
 | 
			
		||||
            let resPri = tableData.fields.res.filter((a: RowDefinition) => a.pri);
 | 
			
		||||
            if (resPri) {
 | 
			
		||||
                priArr.add(`${this.quoteIdentifier(resPri.name)}`);
 | 
			
		||||
            }
 | 
			
		||||
            // 如果有编辑主键字段,则删除主键,再添加主键
 | 
			
		||||
            // 解析表字段中是否含有主键,有的话就删除主键
 | 
			
		||||
            if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
 | 
			
		||||
                dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
 | 
			
		||||
 | 
			
		||||
        return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        // 不能直接修改索引名或字段、需要先删后加
 | 
			
		||||
        let dropIndexNames: string[] = [];
 | 
			
		||||
        let addIndexs: any[] = [];
 | 
			
		||||
@@ -615,6 +663,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 '${tableData.tableComment}';`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
@@ -635,8 +699,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});`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
 | 
			
		||||
import { DialectInfo, DuplicateStrategy } from '@/views/ops/db/dialect/index';
 | 
			
		||||
 | 
			
		||||
let gsDialectInfo: DialectInfo;
 | 
			
		||||
export class GaussDialect extends PostgresqlDialect {
 | 
			
		||||
    getInfo(): DialectInfo {
 | 
			
		||||
        if (gsDialectInfo) {
 | 
			
		||||
            return gsDialectInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        gsDialectInfo = {} as DialectInfo;
 | 
			
		||||
        Object.assign(gsDialectInfo, super.getInfo());
 | 
			
		||||
        gsDialectInfo.icon = 'iconfont icon-gauss';
 | 
			
		||||
        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};`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,11 @@ import { PostgresqlDialect } from './postgres_dialect';
 | 
			
		||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
 | 
			
		||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
 | 
			
		||||
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;
 | 
			
		||||
@@ -14,6 +19,7 @@ export interface sqlColumnType {
 | 
			
		||||
 | 
			
		||||
export interface RowDefinition {
 | 
			
		||||
    name: string;
 | 
			
		||||
    oldName?: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
    value: string;
 | 
			
		||||
    length: string;
 | 
			
		||||
@@ -78,6 +84,11 @@ export const ColumnTypeSubscript = {
 | 
			
		||||
 | 
			
		||||
// 数据库基础信息
 | 
			
		||||
export interface DialectInfo {
 | 
			
		||||
    /**
 | 
			
		||||
     * 数据库类型label
 | 
			
		||||
     */
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 图标
 | 
			
		||||
     */
 | 
			
		||||
@@ -108,10 +119,23 @@ export const DbType = {
 | 
			
		||||
    mysql: 'mysql',
 | 
			
		||||
    mariadb: 'mariadb',
 | 
			
		||||
    postgresql: 'postgres',
 | 
			
		||||
    gauss: 'gauss',
 | 
			
		||||
    dm: 'dm', // 达梦
 | 
			
		||||
    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, DbType.kingbaseEs, DbType.vastbase];
 | 
			
		||||
 | 
			
		||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
 | 
			
		||||
 | 
			
		||||
export const compatibleMysql = (dbType: string): boolean => {
 | 
			
		||||
    switch (dbType) {
 | 
			
		||||
        case DbType.mysql:
 | 
			
		||||
@@ -122,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 {
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取一些数据库默认信息
 | 
			
		||||
@@ -130,13 +173,14 @@ export interface DbDialect {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取默认查询sql
 | 
			
		||||
     * @param db  数据库信息
 | 
			
		||||
     * @param table  表名
 | 
			
		||||
     * @param condition 条件
 | 
			
		||||
     * @param orderBy 排序
 | 
			
		||||
     * @param pageNum  页数
 | 
			
		||||
     * @param limit  条数
 | 
			
		||||
     */
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
 | 
			
		||||
 | 
			
		||||
    getPageSql(pageNum: number, limit: number): string;
 | 
			
		||||
 | 
			
		||||
@@ -164,47 +208,70 @@ export interface DbDialect {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成编辑列sql
 | 
			
		||||
     * @param tableData 表数据,包含表名、列数据、索引数据
 | 
			
		||||
     * @param tableName 表名
 | 
			
		||||
     * @param changeData 改变信息
 | 
			
		||||
     */
 | 
			
		||||
    getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成编辑索引sql
 | 
			
		||||
     * @param tableData 表数据,包含表名、列数据、索引数据
 | 
			
		||||
     * @param tableName   表名
 | 
			
		||||
     * @param changeData  改变数据
 | 
			
		||||
     */
 | 
			
		||||
    getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
 | 
			
		||||
 | 
			
		||||
    /** 生成编辑表信息sql */
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string;
 | 
			
		||||
 | 
			
		||||
    /** 通过数据库字段类型,返回基本数据类型 */
 | 
			
		||||
    getDataType: (columnType: string) => DataType;
 | 
			
		||||
    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();
 | 
			
		||||
let mariadbDialect = new MariadbDialect();
 | 
			
		||||
let postgresDialect = new PostgresqlDialect();
 | 
			
		||||
let dmDialect = new DMDialect();
 | 
			
		||||
let oracleDialect = new OracleDialect();
 | 
			
		||||
 | 
			
		||||
export const getDbDialect = (dbType: string | undefined): DbDialect => {
 | 
			
		||||
    if (!dbType) {
 | 
			
		||||
        return mysqlDialect;
 | 
			
		||||
    }
 | 
			
		||||
    switch (dbType) {
 | 
			
		||||
        case DbType.mysql:
 | 
			
		||||
            return mysqlDialect;
 | 
			
		||||
        case DbType.mariadb:
 | 
			
		||||
            return mariadbDialect;
 | 
			
		||||
        case DbType.postgresql:
 | 
			
		||||
            return postgresDialect;
 | 
			
		||||
        case DbType.dm:
 | 
			
		||||
            return dmDialect;
 | 
			
		||||
        case DbType.oracle:
 | 
			
		||||
            return oracleDialect;
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error('不支持的数据库');
 | 
			
		||||
    }
 | 
			
		||||
let dbType2DialectMap: Map<string, DbDialect> = new Map();
 | 
			
		||||
 | 
			
		||||
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
 | 
			
		||||
    dbType2DialectMap.set(dbType, dd);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDbDialectMap = () => {
 | 
			
		||||
    return dbType2DialectMap;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDbDialect = (dbType?: string): DbDialect => {
 | 
			
		||||
    return dbType2DialectMap.get(dbType!) || mysqlDialect;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
(function () {
 | 
			
		||||
    console.log('init register db dialect');
 | 
			
		||||
    registerDbDialect(DbType.mysql, mysqlDialect);
 | 
			
		||||
    registerDbDialect(DbType.mariadb, new MariadbDialect());
 | 
			
		||||
    registerDbDialect(DbType.postgresql, new PostgresqlDialect());
 | 
			
		||||
    registerDbDialect(DbType.gauss, new GaussDialect());
 | 
			
		||||
    registerDbDialect(DbType.dm, new DMDialect());
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ class MariadbDialect extends MysqlDialect implements DbDialect {
 | 
			
		||||
 | 
			
		||||
        mariadbDialectInfo = {} as DialectInfo;
 | 
			
		||||
        Object.assign(mariadbDialectInfo, super.getInfo());
 | 
			
		||||
        mariadbDialectInfo.name = 'MariaDB';
 | 
			
		||||
        mariadbDialectInfo.icon = 'iconfont icon-mariadb';
 | 
			
		||||
        return mariadbDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										493
									
								
								mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										493
									
								
								mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,493 @@
 | 
			
		||||
import { DbInst } from '../db';
 | 
			
		||||
import {
 | 
			
		||||
    commonCustomKeywords,
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
    RowDefinition,
 | 
			
		||||
} from './index';
 | 
			
		||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
 | 
			
		||||
 | 
			
		||||
export { MSSQL_TYPE_LIST, MssqlDialect };
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://docs.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15
 | 
			
		||||
const MSSQL_TYPE_LIST = [
 | 
			
		||||
    //精确数字
 | 
			
		||||
    'bigint',
 | 
			
		||||
    'numeric',
 | 
			
		||||
    'bit',
 | 
			
		||||
    'smallint',
 | 
			
		||||
    'decimal',
 | 
			
		||||
    'smallmoney',
 | 
			
		||||
    'int',
 | 
			
		||||
    'tinyint',
 | 
			
		||||
    'money',
 | 
			
		||||
    // 近似数字
 | 
			
		||||
    'float',
 | 
			
		||||
    'real',
 | 
			
		||||
    // 日期和时间
 | 
			
		||||
    'date',
 | 
			
		||||
    'datetimeoffset',
 | 
			
		||||
    'datetime2',
 | 
			
		||||
    'smalldatetime',
 | 
			
		||||
    'datetime',
 | 
			
		||||
    'time',
 | 
			
		||||
    // 字符串
 | 
			
		||||
    'char',
 | 
			
		||||
    'varchar',
 | 
			
		||||
    'text',
 | 
			
		||||
    'nchar',
 | 
			
		||||
    'nvarchar',
 | 
			
		||||
    'ntext',
 | 
			
		||||
    'binary',
 | 
			
		||||
    'varbinary',
 | 
			
		||||
 | 
			
		||||
    // 其他
 | 
			
		||||
    'cursor',
 | 
			
		||||
    'rowversion',
 | 
			
		||||
    'hierarchyid',
 | 
			
		||||
    'uniqueidentifier',
 | 
			
		||||
    'sql_variant',
 | 
			
		||||
    'xml',
 | 
			
		||||
    'table',
 | 
			
		||||
    // 空间几何类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geometry/spatial-types-geometry-transact-sql?view=sql-server-ver15
 | 
			
		||||
    'geometry',
 | 
			
		||||
    // 空间地理类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geography/spatial-types-geography?view=sql-server-ver15
 | 
			
		||||
    'geography',
 | 
			
		||||
];
 | 
			
		||||
// 函数参考官方文档 https://learn.microsoft.com/zh-cn/sql/t-sql/functions/functions?view=sql-server-ver15
 | 
			
		||||
 | 
			
		||||
let mssqlDialectInfo: DialectInfo;
 | 
			
		||||
 | 
			
		||||
const customKeywords: EditorCompletionItem[] = [
 | 
			
		||||
    {
 | 
			
		||||
        label: 'select top ',
 | 
			
		||||
        description: 'keyword',
 | 
			
		||||
        insertText: 'select top 100 * from',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: 'select page ',
 | 
			
		||||
        description: 'keyword',
 | 
			
		||||
        insertText: 'SELECT *, 0 AS _ORDER_F_ FROM table_name \n ORDER BY _ORDER_F_ \n OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY;',
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const fixedLengthTypes = [
 | 
			
		||||
    'int',
 | 
			
		||||
    'bigint',
 | 
			
		||||
    'smallint',
 | 
			
		||||
    'tinyint',
 | 
			
		||||
    'float',
 | 
			
		||||
    'real',
 | 
			
		||||
    'datetime',
 | 
			
		||||
    'smalldatetime',
 | 
			
		||||
    'date',
 | 
			
		||||
    'time',
 | 
			
		||||
    'datetime2',
 | 
			
		||||
    'datetimeoffset',
 | 
			
		||||
    'bit',
 | 
			
		||||
    'uniqueidentifier',
 | 
			
		||||
    'geometry',
 | 
			
		||||
    'geography',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
class MssqlDialect implements DbDialect {
 | 
			
		||||
    getInfo(): DialectInfo {
 | 
			
		||||
        if (mssqlDialectInfo) {
 | 
			
		||||
            return mssqlDialectInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let { keywords, operators, builtinVariables, builtinFunctions } = sqlLanguage;
 | 
			
		||||
        let functions = builtinFunctions.map((a: string): EditorCompletionItem => ({ label: a, insertText: `${a}()`, description: 'func' }));
 | 
			
		||||
 | 
			
		||||
        let excludeKeywords = new Set(operators);
 | 
			
		||||
        let editorCompletions: EditorCompletion = {
 | 
			
		||||
            keywords: keywords
 | 
			
		||||
                .filter((a: string) => !excludeKeywords.has(a)) // 移除已存在的operator、function
 | 
			
		||||
                .map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
 | 
			
		||||
                .concat(customKeywords)
 | 
			
		||||
                .concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
 | 
			
		||||
            operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
 | 
			
		||||
            functions,
 | 
			
		||||
            variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        mssqlDialectInfo = {
 | 
			
		||||
            name: 'MSSQL',
 | 
			
		||||
            icon: 'iconfont icon-MSSQLNATIVE',
 | 
			
		||||
            defaultPort: 1433,
 | 
			
		||||
            formatSqlDialect: 'transactsql',
 | 
			
		||||
            columnTypes: MSSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' })),
 | 
			
		||||
            editorCompletions,
 | 
			
		||||
        };
 | 
			
		||||
        return mssqlDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        let schema = db.split('/')[1];
 | 
			
		||||
        return `SELECT *, 0 AS _MAY_ORDER_F_ FROM ${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${
 | 
			
		||||
            orderBy ? orderBy + ', _MAY_ORDER_F_' : 'order by _MAY_ORDER_F_'
 | 
			
		||||
        } ${this.getPageSql(pageNum, limit)};`.toUpperCase();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getPageSql(pageNum: number, limit: number) {
 | 
			
		||||
        return ` offset ${(pageNum - 1) * limit} rows fetch next ${limit} rows only`.toUpperCase();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultRows(): RowDefinition[] {
 | 
			
		||||
        return [
 | 
			
		||||
            { name: 'id', type: 'bigint', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
 | 
			
		||||
            { name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'creator',
 | 
			
		||||
                type: 'nvarchar',
 | 
			
		||||
                length: '100',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '创建人姓名',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'create_time',
 | 
			
		||||
                type: 'datetime2',
 | 
			
		||||
                length: '',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: 'CURRENT_TIMESTAMP',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '创建时间',
 | 
			
		||||
            },
 | 
			
		||||
            { name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'updator',
 | 
			
		||||
                type: 'nvarchar',
 | 
			
		||||
                length: '100',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '修改人姓名',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'update_time',
 | 
			
		||||
                type: 'datetime2',
 | 
			
		||||
                length: '',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: 'CURRENT_TIMESTAMP',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '修改时间',
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultIndex(): IndexDefinition {
 | 
			
		||||
        return {
 | 
			
		||||
            indexName: '',
 | 
			
		||||
            columnNames: [],
 | 
			
		||||
            unique: false,
 | 
			
		||||
            indexType: 'NONCLUSTERED',
 | 
			
		||||
            indexComment: '',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    quoteIdentifier = (name: string) => {
 | 
			
		||||
        return `[${name}]`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    genColumnBasicSql(cl: any): string {
 | 
			
		||||
        let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
 | 
			
		||||
        let defVal = val ? `DEFAULT ${val}` : '';
 | 
			
		||||
        // mssql哪些字段允许有长度
 | 
			
		||||
        let length = '';
 | 
			
		||||
        if (!fixedLengthTypes.includes(cl.type)) {
 | 
			
		||||
            length = cl.length ? `(${cl.length})` : '';
 | 
			
		||||
        }
 | 
			
		||||
        return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.auto_increment ? 'IDENTITY(1,1)' : ''} ${defVal} ${cl.notNull ? 'NOT NULL' : 'NULL'} `;
 | 
			
		||||
    }
 | 
			
		||||
    getCreateTableSql(data: any): string {
 | 
			
		||||
        let schema = data.db.split('/')[1];
 | 
			
		||||
 | 
			
		||||
        // 创建表结构
 | 
			
		||||
        let pks = [] as string[];
 | 
			
		||||
        let fields: string[] = [];
 | 
			
		||||
        let fieldComments: string[] = [];
 | 
			
		||||
        data.fields.res.forEach((item: any) => {
 | 
			
		||||
            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}'`
 | 
			
		||||
                );
 | 
			
		||||
            if (item.pri) {
 | 
			
		||||
                pks.push(`${this.quoteIdentifier(item.name)}`);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
 | 
			
		||||
 | 
			
		||||
        // 建表语句
 | 
			
		||||
        let createTable = `CREATE TABLE ${baseTable}
 | 
			
		||||
                ( ${fields.join(',')}
 | 
			
		||||
                  ${pks.length > 0 ? `, PRIMARY KEY CLUSTERED (${pks.join(',')})` : ''}
 | 
			
		||||
                );`;
 | 
			
		||||
 | 
			
		||||
        let createIndexSql = this.getCreateIndexSql(data);
 | 
			
		||||
 | 
			
		||||
        // 表注释
 | 
			
		||||
        if (data.tableComment) {
 | 
			
		||||
            createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return createTable + createIndexSql + fieldComments.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCreateIndexSql(data: any): string {
 | 
			
		||||
        // CREATE UNIQUE NONCLUSTERED INDEX [aaa]
 | 
			
		||||
        // ON [dbo].[无标题] (
 | 
			
		||||
        //   [id],
 | 
			
		||||
        //   [name]
 | 
			
		||||
        // )
 | 
			
		||||
        let schema = data.db.split('/')[1];
 | 
			
		||||
        let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let indexComment = [] as string[];
 | 
			
		||||
 | 
			
		||||
        // 创建索引
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
        data.indexs.res.forEach((a: any) => {
 | 
			
		||||
            let columnNames = a.columnNames.map((b: string) => `${this.quoteIdentifier(b)}`);
 | 
			
		||||
            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}'`
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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 {
 | 
			
		||||
        // sql执行顺序
 | 
			
		||||
        // 1. 删除字段
 | 
			
		||||
        // 2. 添加字段
 | 
			
		||||
        // 3. 修改字段名字
 | 
			
		||||
        // 4. 修改字段类型
 | 
			
		||||
        // 5. 修改字段注释
 | 
			
		||||
        // 6. 添加字段注释
 | 
			
		||||
 | 
			
		||||
        let schema = tableData.db.split('/')[1];
 | 
			
		||||
        let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let delSql = '';
 | 
			
		||||
        let addArr = [] as string[];
 | 
			
		||||
        let renameArr = [] as string[];
 | 
			
		||||
        let updArr = [] as string[];
 | 
			
		||||
        let changeCommentArr = [] as string[];
 | 
			
		||||
        let addCommentArr = [] as string[];
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            delSql = `ALTER TABLE ${baseTable} DROP ${changeData.del.map((a) => 'COLUMN ' + this.quoteIdentifier(a.name)).join(',')};`;
 | 
			
		||||
        }
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${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}'`
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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' `);
 | 
			
		||||
                } else {
 | 
			
		||||
                    updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
 | 
			
		||||
                }
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    changeCommentArr.push(`IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN', N'${a.name}')) > 0)
 | 
			
		||||
  EXEC sp_updateextendedproperty
 | 
			
		||||
'MS_Description', N'${a.remark}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN', N'${a.name}'
 | 
			
		||||
ELSE
 | 
			
		||||
  EXEC sp_addextendedproperty
 | 
			
		||||
'MS_Description', N'${a.remark}',
 | 
			
		||||
'SCHEMA', N'${schema}',
 | 
			
		||||
'TABLE', N'${tableName}',
 | 
			
		||||
'COLUMN',N'${a.name}'`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 {
 | 
			
		||||
        let schema = tableData.db.split('/')[1];
 | 
			
		||||
        let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let dropArr = [] as string[];
 | 
			
		||||
        let addArr = [] as string[];
 | 
			
		||||
        let commentArr = [] as string[];
 | 
			
		||||
 | 
			
		||||
        const pushDrop = (a: any) => {
 | 
			
		||||
            dropArr.push(` DROP INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} `);
 | 
			
		||||
        };
 | 
			
		||||
        const pushAdd = (a: any) => {
 | 
			
		||||
            addArr.push(
 | 
			
		||||
                ` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} (${a.columnNames.map((b: string) => this.quoteIdentifier(b)).join(',')}) `
 | 
			
		||||
            );
 | 
			
		||||
            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}' `
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                pushDrop(a);
 | 
			
		||||
                pushAdd(a);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                pushDrop(a);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => pushAdd(a));
 | 
			
		||||
        }
 | 
			
		||||
        let dropSql = dropArr.join(';');
 | 
			
		||||
        let addSql = addArr.join(';');
 | 
			
		||||
        let commentSql = commentArr.join(';');
 | 
			
		||||
 | 
			
		||||
        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 {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return DataType.Number;
 | 
			
		||||
        }
 | 
			
		||||
        // 日期时间类型
 | 
			
		||||
        if (/datetime|timestamp/gi.test(columnType)) {
 | 
			
		||||
            return DataType.DateTime;
 | 
			
		||||
        }
 | 
			
		||||
        // 日期类型
 | 
			
		||||
        if (/date/gi.test(columnType)) {
 | 
			
		||||
            return DataType.Date;
 | 
			
		||||
        }
 | 
			
		||||
        // 时间类型
 | 
			
		||||
        if (/time/gi.test(columnType)) {
 | 
			
		||||
            return DataType.Time;
 | 
			
		||||
        }
 | 
			
		||||
        return DataType.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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,20 @@
 | 
			
		||||
import { DbInst } from '../db';
 | 
			
		||||
import { commonCustomKeywords, DataType, DbDialect, DialectInfo, EditorCompletion, EditorCompletionItem, IndexDefinition, RowDefinition } from './index';
 | 
			
		||||
import {
 | 
			
		||||
    commonCustomKeywords,
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
    RowDefinition,
 | 
			
		||||
} from './index';
 | 
			
		||||
import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
 | 
			
		||||
 | 
			
		||||
export { MYSQL_TYPE_LIST, MysqlDialect };
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.0/en/data-types.html
 | 
			
		||||
const MYSQL_TYPE_LIST = [
 | 
			
		||||
    'bigint',
 | 
			
		||||
    'binary',
 | 
			
		||||
@@ -31,6 +42,7 @@ const MYSQL_TYPE_LIST = [
 | 
			
		||||
    'varchar',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.3/en/functions.html
 | 
			
		||||
const replaceFunctions: EditorCompletionItem[] = [
 | 
			
		||||
    /**  字符串相关函数  */
 | 
			
		||||
    { label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
 | 
			
		||||
@@ -102,6 +114,7 @@ class MysqlDialect implements DbDialect {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        mysqlDialectInfo = {
 | 
			
		||||
            name: 'MySQL',
 | 
			
		||||
            icon: 'iconfont icon-op-mysql',
 | 
			
		||||
            defaultPort: 3306,
 | 
			
		||||
            formatSqlDialect: 'mysql',
 | 
			
		||||
@@ -111,7 +124,7 @@ class MysqlDialect implements DbDialect {
 | 
			
		||||
        return mysqlDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
 | 
			
		||||
            pageNum,
 | 
			
		||||
            limit
 | 
			
		||||
@@ -193,7 +206,7 @@ class MysqlDialect implements DbDialect {
 | 
			
		||||
        let defVal = val ? `DEFAULT ${val}` : '';
 | 
			
		||||
        let length = cl.length ? `(${cl.length})` : '';
 | 
			
		||||
        let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
 | 
			
		||||
        return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
 | 
			
		||||
        return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
 | 
			
		||||
            cl.auto_increment ? 'AUTO_INCREMENT' : ''
 | 
			
		||||
        } ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
 | 
			
		||||
    }
 | 
			
		||||
@@ -223,38 +236,38 @@ class MysqlDialect implements DbDialect {
 | 
			
		||||
        return sql.substring(0, sql.length - 1) + ';';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let addSql = '',
 | 
			
		||||
            updSql = '',
 | 
			
		||||
            delSql = '';
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            addSql = `ALTER TABLE ${tableName}`;
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                addSql += ` ADD ${this.genColumnBasicSql(a)},`;
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let arr = [] as string[];
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                arr.push(` DROP COLUMN  ${this.quoteIdentifier(a.name)} `);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                arr.push(` ADD COLUMN ${this.genColumnBasicSql(a)} `);
 | 
			
		||||
            });
 | 
			
		||||
            addSql = addSql.substring(0, addSql.length - 1);
 | 
			
		||||
            addSql += ';';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            updSql = `ALTER TABLE ${tableName}`;
 | 
			
		||||
            let arr = [] as string[];
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                arr.push(` MODIFY ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
            });
 | 
			
		||||
            updSql += arr.join(',');
 | 
			
		||||
            updSql += ';';
 | 
			
		||||
                if (a.name === a.oldName) {
 | 
			
		||||
                    arr.push(` MODIFY COLUMN ${this.genColumnBasicSql(a)} `);
 | 
			
		||||
                } else {
 | 
			
		||||
                    arr.push(` CHANGE COLUMN ${this.quoteIdentifier(a.oldName!)} ${this.genColumnBasicSql(a)} `);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return addSql + updSql + delSql;
 | 
			
		||||
 | 
			
		||||
        if (arr.length > 0) {
 | 
			
		||||
            let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
            return sql + arr.join(',') + ';';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        // 搜集修改和删除的索引,添加到drop index xx
 | 
			
		||||
        // 收集新增和修改的索引,添加到ADD xx
 | 
			
		||||
        // ALTER TABLE `test1`
 | 
			
		||||
@@ -310,6 +323,18 @@ class MysqlDialect implements DbDialect {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string {
 | 
			
		||||
        let sql = '';
 | 
			
		||||
        if (tableData.tableComment !== tableData.oldTableComment) {
 | 
			
		||||
            sql += `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableData.oldTableName)} COMMENT '${tableData.tableComment}';`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tableData.tableName !== tableData.oldTableName) {
 | 
			
		||||
            sql += `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableData.oldTableName)} RENAME TO ${this.quoteIdentifier(tableData.tableName)};`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return DataType.Number;
 | 
			
		||||
@@ -328,8 +353,27 @@ class MysqlDialect 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;
 | 
			
		||||
        }
 | 
			
		||||
        // 转义所有的换行符
 | 
			
		||||
        value = value.replace(/[\r\n]/g, '\\n');
 | 
			
		||||
        return `'${value}'`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: number): string {
 | 
			
		||||
        let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
 | 
			
		||||
        let prefix = 'insert into';
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.IGNORE) {
 | 
			
		||||
            prefix = 'insert ignore into';
 | 
			
		||||
        } else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
 | 
			
		||||
            prefix = 'replace into';
 | 
			
		||||
        }
 | 
			
		||||
        return `${prefix} ${this.quoteIdentifier(tableName)}(${fieldArr.join(',')}) values (${placeholder});`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import {
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
@@ -14,7 +15,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
 | 
			
		||||
 | 
			
		||||
export { OracleDialect, ORACLE_TYPE_LIST };
 | 
			
		||||
 | 
			
		||||
// 参考文档:https://eco.dameng.com/document/dm/zh-cn/sql-dev/dmpl-sql-datatype.html#%E5%AD%97%E7%AC%A6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B
 | 
			
		||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements001.htm
 | 
			
		||||
const ORACLE_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
    // 字符数据类型
 | 
			
		||||
    { udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' },
 | 
			
		||||
@@ -50,6 +51,7 @@ const ORACLE_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
    { udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
 | 
			
		||||
const replaceFunctions: EditorCompletionItem[] = [
 | 
			
		||||
    //  字符函数
 | 
			
		||||
    { label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' },
 | 
			
		||||
@@ -83,7 +85,7 @@ const replaceFunctions: EditorCompletionItem[] = [
 | 
			
		||||
    { label: 'CURRENT_TIMESTAMP', insertText: 'TIMESTAMP', description: '获取当前时间' },
 | 
			
		||||
    // 转换函数
 | 
			
		||||
    { label: 'TO_CHAR', insertText: 'TO_CHAR(d|n[,fmt])', description: '把日期和数字转换为制定格式的字符串' },
 | 
			
		||||
    { label: 'TO_DATE', insertText: 'TO_DATE(X,[,fmt])', description: '把一个字符串以fmt格式转换成一个日期类型' },
 | 
			
		||||
    { label: 'TO_DATE', insertText: `TO_DATE(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换成一个日期类型' },
 | 
			
		||||
    { label: 'TO_NUMBER', insertText: 'TO_NUMBER(X,[,fmt])', description: '把一个字符串以fmt格式转换为一个数字' },
 | 
			
		||||
    { label: 'TO_TIMESTAMP', insertText: 'TO_TIMESTAMP(X,[,fmt])', description: '把一个字符串以fmt格式转换为日期类型' },
 | 
			
		||||
    // 其他
 | 
			
		||||
@@ -91,7 +93,35 @@ const replaceFunctions: EditorCompletionItem[] = [
 | 
			
		||||
    { label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const addCustomKeywords = ['ROWNUM', 'DUAL'];
 | 
			
		||||
const addCustomKeywords: EditorCompletionItem[] = [
 | 
			
		||||
    {
 | 
			
		||||
        label: 'ROWNUM',
 | 
			
		||||
        description: 'keyword',
 | 
			
		||||
        insertText: 'ROWNUM',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: 'DUAL',
 | 
			
		||||
        description: 'keyword',
 | 
			
		||||
        insertText: 'DUAL',
 | 
			
		||||
    },
 | 
			
		||||
    // 分页代码块
 | 
			
		||||
    {
 | 
			
		||||
        label: 'SELECT ROWNUM',
 | 
			
		||||
        description: 'code block',
 | 
			
		||||
        insertText: 'SELECT * from table_name where rownum <= 10',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: 'SELECT PAGE',
 | 
			
		||||
        description: 'code block',
 | 
			
		||||
        insertText: ` SELECT * FROM
 | 
			
		||||
    (
 | 
			
		||||
      SELECT t.*, ROWNUM AS rn
 | 
			
		||||
      FROM table_name t
 | 
			
		||||
      WHERE ROWNUM <= 25
 | 
			
		||||
    )
 | 
			
		||||
  WHERE rn > 0 \n`,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
let oracleDialectInfo: DialectInfo;
 | 
			
		||||
class OracleDialect implements DbDialect {
 | 
			
		||||
@@ -103,6 +133,7 @@ class OracleDialect implements DbDialect {
 | 
			
		||||
        let { keywords, operators, builtinVariables } = sqlLanguage;
 | 
			
		||||
        let functionNames = replaceFunctions.map((a) => a.label);
 | 
			
		||||
        let excludeKeywords = new Set(functionNames.concat(operators));
 | 
			
		||||
        excludeKeywords.add('SELECT');
 | 
			
		||||
 | 
			
		||||
        let editorCompletions: EditorCompletion = {
 | 
			
		||||
            keywords: keywords
 | 
			
		||||
@@ -117,21 +148,14 @@ class OracleDialect implements DbDialect {
 | 
			
		||||
                        })
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                .concat(
 | 
			
		||||
                    // 加上自定义的关键字
 | 
			
		||||
                    addCustomKeywords.map(
 | 
			
		||||
                        (a): EditorCompletionItem => ({
 | 
			
		||||
                            label: a,
 | 
			
		||||
                            description: 'keyword',
 | 
			
		||||
                        })
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
                .concat(addCustomKeywords),
 | 
			
		||||
            operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
 | 
			
		||||
            functions: replaceFunctions,
 | 
			
		||||
            variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        oracleDialectInfo = {
 | 
			
		||||
            name: 'Oracle',
 | 
			
		||||
            icon: 'iconfont icon-oracle',
 | 
			
		||||
            defaultPort: 1521,
 | 
			
		||||
            formatSqlDialect: 'plsql',
 | 
			
		||||
@@ -141,7 +165,7 @@ class OracleDialect implements DbDialect {
 | 
			
		||||
        return oracleDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        return `
 | 
			
		||||
        SELECT *
 | 
			
		||||
        FROM (
 | 
			
		||||
@@ -268,34 +292,50 @@ class OracleDialect implements DbDialect {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    genColumnBasicSql(cl: RowDefinition): string {
 | 
			
		||||
    genColumnBasicSql(cl: RowDefinition, create: boolean): string {
 | 
			
		||||
        let length = this.getTypeLengthSql(cl);
 | 
			
		||||
        // 默认值
 | 
			
		||||
        let defVal = this.getDefaultValueSql(cl);
 | 
			
		||||
        let incr = cl.auto_increment ? 'generated by default as IDENTITY' : '';
 | 
			
		||||
        let pri = cl.pri ? 'PRIMARY KEY' : '';
 | 
			
		||||
        return ` ${cl.name.toUpperCase()} ${cl.type}${length} ${incr} ${pri} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
 | 
			
		||||
        let incr = cl.auto_increment && create ? 'generated by default as IDENTITY' : '';
 | 
			
		||||
        // 如果有原名以原名为准
 | 
			
		||||
        let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
 | 
			
		||||
        let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`;
 | 
			
		||||
        return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCreateTableSql(data: any): string {
 | 
			
		||||
        let schemaArr = data.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let createSql = '';
 | 
			
		||||
        let tableCommentSql = '';
 | 
			
		||||
        let columCommentSql = '';
 | 
			
		||||
        let pris = [] as string[];
 | 
			
		||||
 | 
			
		||||
        // 创建表结构
 | 
			
		||||
        let fields: string[] = [];
 | 
			
		||||
        data.fields.res.forEach((item: any) => {
 | 
			
		||||
            item.name && fields.push(this.genColumnBasicSql(item));
 | 
			
		||||
            item.name && fields.push(this.genColumnBasicSql(item, true));
 | 
			
		||||
            // 列注释
 | 
			
		||||
            if (item.remark) {
 | 
			
		||||
                columCommentSql += ` comment on column ${data.tableName?.toUpperCase()}.${item.name?.toUpperCase()} is '${item.remark}'; `;
 | 
			
		||||
                columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${item.remark}'; `;
 | 
			
		||||
            }
 | 
			
		||||
            // 主键
 | 
			
		||||
            if (item.pri) {
 | 
			
		||||
                pris.push(this.quoteIdentifier(item.name));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        // 主键语句
 | 
			
		||||
        let prisql = '';
 | 
			
		||||
        if (pris.length > 0) {
 | 
			
		||||
            prisql = ` CONSTRAINT "PK_${data.tableName}" PRIMARY KEY (${pris.join(',')});`;
 | 
			
		||||
        }
 | 
			
		||||
        // 建表
 | 
			
		||||
        createSql = `CREATE TABLE ${data.tableName?.toUpperCase()} ( ${fields.join(',')} );`;
 | 
			
		||||
        createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ) ${prisql ? ',' + prisql : ''};`;
 | 
			
		||||
        // 表注释
 | 
			
		||||
        if (data.tableComment) {
 | 
			
		||||
            tableCommentSql = ` comment on table ${data.tableName?.toUpperCase()} is '${data.tableComment}'; `;
 | 
			
		||||
            tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return createSql + tableCommentSql + columCommentSql;
 | 
			
		||||
@@ -304,43 +344,95 @@ class OracleDialect implements DbDialect {
 | 
			
		||||
    getCreateIndexSql(tableData: any): string {
 | 
			
		||||
        // CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
 | 
			
		||||
        // COMMENT ON INDEX idx_column_name IS 'Your index comment here';
 | 
			
		||||
        // 创建索引
 | 
			
		||||
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
        tableData.indexs.res.forEach((a: any) => {
 | 
			
		||||
            sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`);
 | 
			
		||||
            sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON ${dbTable} ("${a.columnNames.join('","')})"`);
 | 
			
		||||
        });
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let baseSql = `ALTER TABLE ${dbTable} `;
 | 
			
		||||
 | 
			
		||||
        let modifyArr: string[] = [];
 | 
			
		||||
        let dropArr: string[] = [];
 | 
			
		||||
        // 重命名的sql要一条条执行
 | 
			
		||||
        let renameArr: string[] = [];
 | 
			
		||||
        let commentArr: string[] = [];
 | 
			
		||||
 | 
			
		||||
        // 主键字段
 | 
			
		||||
        let priArr = new Set();
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}'`;
 | 
			
		||||
                if (a.remark && a.oldName === a.name) {
 | 
			
		||||
                    commentArr.push(commentSql);
 | 
			
		||||
                }
 | 
			
		||||
                // 修改了字段名
 | 
			
		||||
                if (a.oldName !== a.name) {
 | 
			
		||||
                    renameArr.push(baseSql + ` RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)} ;`);
 | 
			
		||||
                    if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                        commentArr.push(commentSql);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
 | 
			
		||||
                if (a.pri) {
 | 
			
		||||
                    priArr.add(`${this.quoteIdentifier(a.name)}`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                    commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}'`);
 | 
			
		||||
                }
 | 
			
		||||
                if (a.pri) {
 | 
			
		||||
                    priArr.add(`"${a.name}"`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
 | 
			
		||||
                dropArr.push(`${this.quoteIdentifier(a.name)}`);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
 | 
			
		||||
        let dropPkSql = '';
 | 
			
		||||
        if (priArr.size > 0) {
 | 
			
		||||
            let resPri = tableData.fields.res.find((a: RowDefinition) => a.pri);
 | 
			
		||||
            if (resPri) {
 | 
			
		||||
                priArr.add(`"${resPri.name}"`);
 | 
			
		||||
            }
 | 
			
		||||
            // 如果有编辑主键字段,则删除主键,再添加主键
 | 
			
		||||
            // 解析表字段中是否含有主键,有的话就删除主键
 | 
			
		||||
            if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
 | 
			
		||||
                dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        let modifySql = modifyArr.length > 0 ? baseSql + modifyArr.join(' ') + ';' : '';
 | 
			
		||||
        let dropSql = dropArr.length > 0 ? baseSql + ` DROP (${dropArr.join(',')}) ;` : '';
 | 
			
		||||
        let renameSql = renameArr.join('');
 | 
			
		||||
        let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD CONSTRAINT "PK_${tableName}" PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
 | 
			
		||||
        let commentSql = commentArr.join(';');
 | 
			
		||||
 | 
			
		||||
        return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        // 不能直接修改索引名或字段、需要先删后加
 | 
			
		||||
        let dropIndexNames: string[] = [];
 | 
			
		||||
        let addIndexs: any[] = [];
 | 
			
		||||
@@ -382,6 +474,22 @@ class OracleDialect 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.tableComment != tableData.oldTableComment) {
 | 
			
		||||
            let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql = `COMMENT ON TABLE ${dbTable} is '${tableData.tableComment}';`;
 | 
			
		||||
        }
 | 
			
		||||
        if (tableData.tableName != tableData.oldTableName) {
 | 
			
		||||
            let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql += `ALTER TABLE ${dbTable} RENAME TO ${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return DataType.Number;
 | 
			
		||||
@@ -392,11 +500,51 @@ class OracleDialect 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;
 | 
			
		||||
        }
 | 
			
		||||
        if (value && this.getDataType(columnType) === DataType.DateTime) {
 | 
			
		||||
            return `to_timestamp('${value}', 'yyyy-mm-dd hh24:mi:ss')`;
 | 
			
		||||
        }
 | 
			
		||||
        return `'${value}'`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
 | 
			
		||||
        if (duplicateStrategy == DuplicateStrategy.REPLACE) {
 | 
			
		||||
            // 字段数组生成占位符sql
 | 
			
		||||
            let phs = [];
 | 
			
		||||
            let values = [];
 | 
			
		||||
            let insertFields = [];
 | 
			
		||||
            for (let i = 0; i < fieldArr.length; i++) {
 | 
			
		||||
                phs.push(`:${i + 1} ${fieldArr[i]}`);
 | 
			
		||||
                values.push(`T2.${fieldArr[i]}`);
 | 
			
		||||
                insertFields.push(`T1.${fieldArr[i]}`);
 | 
			
		||||
            }
 | 
			
		||||
            let placeholder = phs.join(',');
 | 
			
		||||
            let sql = `MERGE INTO ${this.quoteIdentifier(tableName)} T1 USING 
 | 
			
		||||
        (
 | 
			
		||||
         SELECT ${placeholder} FROM dual
 | 
			
		||||
        ) T2 ON (T1.id = T2.id) 
 | 
			
		||||
        WHEN NOT MATCHED THEN INSERT (${insertFields.join(',')}) VALUES (${values.join(',')})
 | 
			
		||||
        WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
 | 
			
		||||
            return sql;
 | 
			
		||||
        } else {
 | 
			
		||||
            // 字段数组生成占位符sql
 | 
			
		||||
            let phs = [];
 | 
			
		||||
            for (let i = 0; i < fieldArr.length; i++) {
 | 
			
		||||
                phs.push(`:${i + 1} ${fieldArr[i]}`);
 | 
			
		||||
            }
 | 
			
		||||
            let ignore = '';
 | 
			
		||||
            if (duplicateStrategy == DuplicateStrategy.IGNORE) {
 | 
			
		||||
                ignore = `/*+ IGNORE_ROW_ON_DUPKEY_INDEX(${tableName}(id)) */`;
 | 
			
		||||
            }
 | 
			
		||||
            let placeholder = phs.join(',');
 | 
			
		||||
            return `INSERT ${ignore} INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder});`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import {
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
@@ -123,6 +124,7 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        pgDialectInfo = {
 | 
			
		||||
            name: 'PostgreSQL',
 | 
			
		||||
            icon: 'iconfont icon-op-postgres',
 | 
			
		||||
            defaultPort: 5432,
 | 
			
		||||
            formatSqlDialect: 'postgresql',
 | 
			
		||||
@@ -132,7 +134,7 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
        return pgDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
 | 
			
		||||
            pageNum,
 | 
			
		||||
            limit
 | 
			
		||||
@@ -228,7 +230,7 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
            let marks = false;
 | 
			
		||||
            if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
 | 
			
		||||
                // 默认值是now()的time或date不需要加引号
 | 
			
		||||
                if (cl.value.toLowerCase().replace(' ', '') === 'current_timestamp' && this.matchType(cl.type, ['time', 'date'])) {
 | 
			
		||||
                if (['pg_systimestamp()', 'current_timestamp'].includes(cl.value.toLowerCase()) && this.matchType(cl.type, ['time', 'date'])) {
 | 
			
		||||
                    marks = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    marks = true;
 | 
			
		||||
@@ -260,7 +262,10 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
        let length = this.getTypeLengthSql(cl);
 | 
			
		||||
        // 默认值
 | 
			
		||||
        let defVal = this.getDefaultValueSql(cl);
 | 
			
		||||
        return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
 | 
			
		||||
        // 如果有原名以原名为准
 | 
			
		||||
        let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
 | 
			
		||||
 | 
			
		||||
        return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCreateTableSql(data: any): string {
 | 
			
		||||
@@ -299,52 +304,77 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
        // CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
 | 
			
		||||
        // COMMENT ON INDEX idx_column_name IS 'Your index comment here';
 | 
			
		||||
        // 创建索引
 | 
			
		||||
        let schema = tableData.db.split('/')[1];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
        tableData.indexs.res.forEach((a: any) => {
 | 
			
		||||
            sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`);
 | 
			
		||||
            // 字段名用双引号包裹
 | 
			
		||||
            let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
 | 
			
		||||
            sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
 | 
			
		||||
            if (a.indexComment) {
 | 
			
		||||
                sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
 | 
			
		||||
                sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let sql: string[] = [];
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        let dropPkSql = '';
 | 
			
		||||
        let modifySql = '';
 | 
			
		||||
        let dropSql = '';
 | 
			
		||||
        let renameSql = '';
 | 
			
		||||
        let addPkSql = '';
 | 
			
		||||
        let commentSql = '';
 | 
			
		||||
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            changeData.add.forEach((a) => {
 | 
			
		||||
                let typeLength = this.getTypeLengthSql(a);
 | 
			
		||||
                let defaultSql = this.getDefaultValueSql(a);
 | 
			
		||||
                sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
 | 
			
		||||
                modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                    commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            changeData.upd.forEach((a) => {
 | 
			
		||||
                let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
 | 
			
		||||
                if (a.remark && a.oldName === a.name) {
 | 
			
		||||
                    commentSql += cmtSql;
 | 
			
		||||
                }
 | 
			
		||||
                // 修改了字段名
 | 
			
		||||
                if (a.oldName !== a.name) {
 | 
			
		||||
                    renameSql += `alter table ${dbTable} rename column ${this.quoteIdentifier(a.oldName!)} to ${this.quoteIdentifier(a.name)};`;
 | 
			
		||||
                    if (a.remark) {
 | 
			
		||||
                        commentSql += cmtSql;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                let typeLength = this.getTypeLengthSql(a);
 | 
			
		||||
                sql.push(`ALTER TABLE ${tableName} alter column ${a.name} type ${a.type}${typeLength}`);
 | 
			
		||||
                // 如果有原名以原名为准
 | 
			
		||||
                let name = a.oldName && a.name !== a.oldName ? a.oldName : a.name;
 | 
			
		||||
                modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} type ${a.type}${typeLength} ;`;
 | 
			
		||||
                let defaultSql = this.getDefaultValueSql(a);
 | 
			
		||||
                if (defaultSql) {
 | 
			
		||||
                    sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
 | 
			
		||||
                }
 | 
			
		||||
                if (a.remark) {
 | 
			
		||||
                    sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
 | 
			
		||||
                    modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                sql.push(`ALTER TABLE ${tableName} DROP COLUMN ${a.name}`);
 | 
			
		||||
                dropSql += `alter table ${dbTable} drop column ${a.name};`;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
        return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        let schema = tableData.db.split('/')[1];
 | 
			
		||||
        let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
 | 
			
		||||
 | 
			
		||||
        // 不能直接修改索引名或字段、需要先删后加
 | 
			
		||||
        let dropIndexNames: string[] = [];
 | 
			
		||||
        let addIndexs: any[] = [];
 | 
			
		||||
@@ -378,9 +408,11 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
 | 
			
		||||
            if (addIndexs.length > 0) {
 | 
			
		||||
                addIndexs.forEach((a) => {
 | 
			
		||||
                    sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`);
 | 
			
		||||
                    // 字段名用双引号包裹
 | 
			
		||||
                    let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
 | 
			
		||||
                    sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
 | 
			
		||||
                    if (a.indexComment) {
 | 
			
		||||
                        sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
 | 
			
		||||
                        sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
@@ -389,6 +421,22 @@ class PostgresqlDialect 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.tableComment != tableData.oldTableComment) {
 | 
			
		||||
            let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql = `COMMENT ON TABLE ${dbTable} is '${tableData.tableComment}';`;
 | 
			
		||||
        }
 | 
			
		||||
        if (tableData.tableName != tableData.oldTableName) {
 | 
			
		||||
            let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql += `ALTER TABLE ${dbTable} RENAME TO ${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return DataType.Number;
 | 
			
		||||
@@ -408,8 +456,26 @@ class PostgresqlDialect implements DbDialect {
 | 
			
		||||
        return DataType.String;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
 | 
			
		||||
    wrapStrValue(value: string, type: 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 {
 | 
			
		||||
        // 构建占位符字符串 "($1, $2, $3 ...)"
 | 
			
		||||
        let placeholder = fieldArr.map((_, i) => `$${i + 1}`).join(',');
 | 
			
		||||
        let suffix = '';
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.IGNORE) {
 | 
			
		||||
            suffix = ' ON CONFLICT DO NOTHING';
 | 
			
		||||
        } else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
 | 
			
		||||
            suffix = ' ON CONFLICT ON CONSTRAINT {your_constraint_name1} DO UPDATE SET ' + fieldArr.map((a) => `${a}=excluded.${a}`).join(',');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder}) \n ${suffix};`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										369
									
								
								mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,369 @@
 | 
			
		||||
import {
 | 
			
		||||
    commonCustomKeywords,
 | 
			
		||||
    DataType,
 | 
			
		||||
    DbDialect,
 | 
			
		||||
    DialectInfo,
 | 
			
		||||
    DuplicateStrategy,
 | 
			
		||||
    EditorCompletion,
 | 
			
		||||
    EditorCompletionItem,
 | 
			
		||||
    IndexDefinition,
 | 
			
		||||
    RowDefinition,
 | 
			
		||||
    sqlColumnType,
 | 
			
		||||
} from './index';
 | 
			
		||||
import { DbInst } from '@/views/ops/db/db';
 | 
			
		||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
 | 
			
		||||
 | 
			
		||||
export { SqliteDialect };
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://www.sqlite.org/datatype3.html
 | 
			
		||||
const SQLITE_TYPE_LIST: sqlColumnType[] = [
 | 
			
		||||
    // INTEGER
 | 
			
		||||
    { udtName: 'int', dataType: 'int', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'integer', dataType: 'integer', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'int2', dataType: 'int2', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'int8', dataType: 'int8', desc: '', space: '', range: '' },
 | 
			
		||||
    // TEXT
 | 
			
		||||
    { udtName: 'character', dataType: 'character', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'native character', dataType: 'native character', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'text', dataType: 'text', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'clob', dataType: 'clob', desc: '', space: '', range: '' },
 | 
			
		||||
    // blob
 | 
			
		||||
    { udtName: 'blob', dataType: 'blob', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: '' },
 | 
			
		||||
    // REAL
 | 
			
		||||
    { udtName: 'real', dataType: 'real', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'double', dataType: 'double', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'float', dataType: 'float', desc: '', space: '', range: '' },
 | 
			
		||||
    // NUMERIC
 | 
			
		||||
    { udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'date', dataType: 'date', desc: '', space: '', range: '' },
 | 
			
		||||
    { udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: '' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
 | 
			
		||||
 | 
			
		||||
// 参考官方文档:https://www.sqlite.org/lang_corefunc.html
 | 
			
		||||
const functions: EditorCompletionItem[] = [
 | 
			
		||||
    //  字符函数
 | 
			
		||||
    { label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值' },
 | 
			
		||||
    { label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数' },
 | 
			
		||||
    { label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值' },
 | 
			
		||||
    { label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值' },
 | 
			
		||||
    { label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值' },
 | 
			
		||||
    { label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y,否则返回z' },
 | 
			
		||||
    { label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置' },
 | 
			
		||||
    { label: 'length', insertText: 'length(X)', description: '返回给定字符的长度' },
 | 
			
		||||
    { label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块' },
 | 
			
		||||
    { label: 'lower', insertText: 'lower(X)', description: '返回小写字符' },
 | 
			
		||||
    { label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim' },
 | 
			
		||||
    { label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null,否则返回第一个值' },
 | 
			
		||||
    { label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d' },
 | 
			
		||||
    { label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来' },
 | 
			
		||||
    { label: 'random', insertText: 'random()', description: '生成随机数' },
 | 
			
		||||
    { label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB' },
 | 
			
		||||
    { label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串' },
 | 
			
		||||
    { label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数' },
 | 
			
		||||
    { label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim' },
 | 
			
		||||
    { label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
 | 
			
		||||
    { label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
 | 
			
		||||
    { label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
 | 
			
		||||
    { label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
 | 
			
		||||
    { label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
 | 
			
		||||
    { label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
 | 
			
		||||
    { label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
 | 
			
		||||
    { label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串' },
 | 
			
		||||
    { label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格' },
 | 
			
		||||
    { label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型:null,integer,real,text,blob' },
 | 
			
		||||
    { label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点' },
 | 
			
		||||
    { label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符' },
 | 
			
		||||
    { label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB' },
 | 
			
		||||
    { label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值' },
 | 
			
		||||
    { label: 'avg', insertText: 'avg(X)', description: '返回总条数' },
 | 
			
		||||
    { label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值' },
 | 
			
		||||
    { label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值' },
 | 
			
		||||
    { label: 'max', insertText: 'max(X)', description: '返回分组中最小值' },
 | 
			
		||||
    { label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。' },
 | 
			
		||||
    { label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
 | 
			
		||||
    { label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
 | 
			
		||||
    { label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
 | 
			
		||||
    { label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
 | 
			
		||||
    { label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
 | 
			
		||||
    { label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
let sqliteDialectInfo: DialectInfo;
 | 
			
		||||
class SqliteDialect implements DbDialect {
 | 
			
		||||
    getInfo(): DialectInfo {
 | 
			
		||||
        if (sqliteDialectInfo) {
 | 
			
		||||
            return sqliteDialectInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let { keywords, operators, builtinVariables } = sqlLanguage;
 | 
			
		||||
 | 
			
		||||
        let editorCompletions: EditorCompletion = {
 | 
			
		||||
            keywords: keywords
 | 
			
		||||
                .filter((a: string) => addCustomKeywords.indexOf(a) === -1)
 | 
			
		||||
                .map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
 | 
			
		||||
                .concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' })))
 | 
			
		||||
                .concat(addCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
 | 
			
		||||
            operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
 | 
			
		||||
            functions,
 | 
			
		||||
            variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        sqliteDialectInfo = {
 | 
			
		||||
            name: 'Sqlite',
 | 
			
		||||
            icon: 'iconfont icon-sqlite',
 | 
			
		||||
            defaultPort: 0,
 | 
			
		||||
            formatSqlDialect: 'sql',
 | 
			
		||||
            columnTypes: SQLITE_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
 | 
			
		||||
            editorCompletions,
 | 
			
		||||
        };
 | 
			
		||||
        return sqliteDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
 | 
			
		||||
        return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
 | 
			
		||||
            pageNum,
 | 
			
		||||
            limit
 | 
			
		||||
        )};`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getPageSql(pageNum: number, limit: number) {
 | 
			
		||||
        return ` LIMIT ${(pageNum - 1) * limit}, ${limit}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultRows(): RowDefinition[] {
 | 
			
		||||
        return [
 | 
			
		||||
            { name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
 | 
			
		||||
            { name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'creator',
 | 
			
		||||
                type: 'varchar',
 | 
			
		||||
                length: '100',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '创建人姓名',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'create_time',
 | 
			
		||||
                type: 'datetime',
 | 
			
		||||
                length: '',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: 'CURRENT_TIMESTAMP',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '创建时间',
 | 
			
		||||
            },
 | 
			
		||||
            { name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
 | 
			
		||||
            { name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'update_time',
 | 
			
		||||
                type: 'datetime',
 | 
			
		||||
                length: '',
 | 
			
		||||
                numScale: '',
 | 
			
		||||
                value: 'CURRENT_TIMESTAMP',
 | 
			
		||||
                notNull: true,
 | 
			
		||||
                pri: false,
 | 
			
		||||
                auto_increment: false,
 | 
			
		||||
                remark: '修改时间',
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDefaultIndex(): IndexDefinition {
 | 
			
		||||
        return {
 | 
			
		||||
            indexName: '',
 | 
			
		||||
            columnNames: [],
 | 
			
		||||
            unique: false,
 | 
			
		||||
            indexType: 'BTREE',
 | 
			
		||||
            indexComment: '',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    quoteIdentifier = (name: string) => {
 | 
			
		||||
        return `\"${name}\"`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    genColumnBasicSql(cl: any): string {
 | 
			
		||||
        let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
 | 
			
		||||
        let defVal = val ? `DEFAULT ${val}` : '';
 | 
			
		||||
        let length = cl.length ? `(${cl.length})` : '';
 | 
			
		||||
        let nullAble = cl.notNull ? 'NOT NULL' : 'NULL';
 | 
			
		||||
        if (cl.pri) {
 | 
			
		||||
            return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} PRIMARY KEY ${cl.auto_increment ? 'AUTOINCREMENT' : ''} ${nullAble} `;
 | 
			
		||||
        }
 | 
			
		||||
        return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
 | 
			
		||||
    }
 | 
			
		||||
    getCreateTableSql(data: any): string {
 | 
			
		||||
        // 创建表结构
 | 
			
		||||
        let fields: string[] = [];
 | 
			
		||||
        data.fields.res.forEach((item: any) => {
 | 
			
		||||
            item.name && fields.push(this.genColumnBasicSql(item));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
 | 
			
		||||
                  ( ${fields.join(',')} )`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCreateIndexSql(data: any): string {
 | 
			
		||||
        // 创建索引
 | 
			
		||||
        let sql = [] as string[];
 | 
			
		||||
        data.indexs.res.forEach((a: any) => {
 | 
			
		||||
            sql.push(
 | 
			
		||||
                `CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
 | 
			
		||||
        // sqlite修改表结构需要先删除再创建
 | 
			
		||||
 | 
			
		||||
        // 1.删除旧表索引  DROP INDEX "main"."aa";
 | 
			
		||||
        let sql = [] as string[];
 | 
			
		||||
        tableData.indexs.res.forEach((a: any) => {
 | 
			
		||||
            a.indexName && sql.push(`DROP INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)}`);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 2.重命名表,备份旧表  ALTER TABLE "main"."t_sys_resource" RENAME TO "_t_sys_resource_old_20240118162712"; new Date().getTime()
 | 
			
		||||
        let oldTableName = `_${tableName}_old_${new Date().getTime()}`;
 | 
			
		||||
        sql.push(`ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} RENAME TO ${this.quoteIdentifier(oldTableName)}`);
 | 
			
		||||
 | 
			
		||||
        // 3.创建新表
 | 
			
		||||
        sql.push(this.getCreateTableSql(tableData));
 | 
			
		||||
 | 
			
		||||
        // 4.复制数据 INSERT INTO "库名"."新表名" (${insertFields}) SELECT ${queryFields} FROM "库名"."旧表名";
 | 
			
		||||
        // 查询的字段数据类型和数量应与插入的字段一致
 | 
			
		||||
        // 判断哪些字段需要查询旧表,哪些字段需要插入新表
 | 
			
		||||
        // 解析changeData,统计需要查询旧表的字段,统计需要插入新表的字段
 | 
			
		||||
        let delFields = changeData.del.map((a) => a.name);
 | 
			
		||||
        let addFields = changeData.add.map((a) => a.name);
 | 
			
		||||
 | 
			
		||||
        let queryFields = [] as string[];
 | 
			
		||||
        let insertFields = [] as string[];
 | 
			
		||||
        tableData.fields.res.forEach((a: any) => {
 | 
			
		||||
            // 新增、删除的字段不需要查询旧表,不需要插入新表
 | 
			
		||||
            if (addFields.includes(a.name) || delFields.includes(a.name)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            // 修改的字段需要查询和插入,判断是否修改了字段名,如果修改了字段名,需要查询旧表原名,插入新表新名
 | 
			
		||||
            // 其余未删除、未修改的字段,需要查询旧表,插入新表
 | 
			
		||||
            queryFields.push(this.quoteIdentifier(a.name === a.oldName ? a.name : a.oldName));
 | 
			
		||||
            insertFields.push(this.quoteIdentifier(a.name));
 | 
			
		||||
        });
 | 
			
		||||
        // 生成sql
 | 
			
		||||
        sql.push(
 | 
			
		||||
            `INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
 | 
			
		||||
                ','
 | 
			
		||||
            )} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // 5.创建索引
 | 
			
		||||
        tableData.indexs.res.forEach((a: any) => {
 | 
			
		||||
            a.indexName &&
 | 
			
		||||
                sql.push(
 | 
			
		||||
                    `CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
 | 
			
		||||
                );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return sql.join(';') + ';';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
 | 
			
		||||
        // sqlite创建索引需要先删除再创建
 | 
			
		||||
        // CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );
 | 
			
		||||
 | 
			
		||||
        let sql = [] as string[];
 | 
			
		||||
 | 
			
		||||
        if (changeData.del.length > 0) {
 | 
			
		||||
            changeData.del.forEach((a) => {
 | 
			
		||||
                sql.push(`DROP INDEX ${this.quoteIdentifier(a.indexName)}`);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let indexData = [] as any[];
 | 
			
		||||
        if (changeData.add.length > 0) {
 | 
			
		||||
            indexData = indexData.concat(changeData.add);
 | 
			
		||||
        }
 | 
			
		||||
        if (changeData.upd.length > 0) {
 | 
			
		||||
            indexData = indexData.concat(changeData.upd);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (indexData.length > 0) {
 | 
			
		||||
            indexData.forEach((a) => {
 | 
			
		||||
                sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return sql.join(';');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getModifyTableInfoSql(tableData: any): string {
 | 
			
		||||
        let schemaArr = tableData.db.split('/');
 | 
			
		||||
        let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
 | 
			
		||||
 | 
			
		||||
        // sqlite没有表注释
 | 
			
		||||
        let sql = '';
 | 
			
		||||
        if (tableData.tableName != tableData.oldTableName) {
 | 
			
		||||
            let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
 | 
			
		||||
            sql += `ALTER TABLE ${dbTable} RENAME TO ${this.quoteIdentifier(tableData.tableName)}`;
 | 
			
		||||
        }
 | 
			
		||||
        return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDataType(columnType: string): DataType {
 | 
			
		||||
        if (DbInst.isNumber(columnType)) {
 | 
			
		||||
            return DataType.Number;
 | 
			
		||||
        }
 | 
			
		||||
        // 日期时间类型
 | 
			
		||||
        if (/datetime|timestamp/gi.test(columnType)) {
 | 
			
		||||
            return DataType.DateTime;
 | 
			
		||||
        }
 | 
			
		||||
        // 日期类型
 | 
			
		||||
        if (/date/gi.test(columnType)) {
 | 
			
		||||
            return DataType.Date;
 | 
			
		||||
        }
 | 
			
		||||
        // 时间类型
 | 
			
		||||
        if (/time/gi.test(columnType)) {
 | 
			
		||||
            return DataType.Time;
 | 
			
		||||
        }
 | 
			
		||||
        return DataType.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 {
 | 
			
		||||
        let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
 | 
			
		||||
        let prefix = 'insert into';
 | 
			
		||||
        if (duplicateStrategy === DuplicateStrategy.IGNORE) {
 | 
			
		||||
            prefix = 'insert or ignore into';
 | 
			
		||||
        } else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
 | 
			
		||||
            prefix = 'insert or replace into';
 | 
			
		||||
        }
 | 
			
		||||
        return `${prefix} ${this.quoteIdentifier(tableName)}(${fieldArr.join(',')}) values (${placeholder});`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { DialectInfo } from './index';
 | 
			
		||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
 | 
			
		||||
 | 
			
		||||
let vastDialectInfo: DialectInfo;
 | 
			
		||||
 | 
			
		||||
export class VastbaseDialect extends PostgresqlDialect {
 | 
			
		||||
    getInfo(): DialectInfo {
 | 
			
		||||
        if (vastDialectInfo) {
 | 
			
		||||
            return vastDialectInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        vastDialectInfo = {} as DialectInfo;
 | 
			
		||||
        Object.assign(vastDialectInfo, super.getInfo());
 | 
			
		||||
        vastDialectInfo.name = 'VastbaseG100';
 | 
			
		||||
        vastDialectInfo.icon = 'iconfont icon-vastbase';
 | 
			
		||||
        return vastDialectInfo;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,6 +9,11 @@ export const DbSqlExecTypeEnum = {
 | 
			
		||||
    Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DbSqlExecStatusEnum = {
 | 
			
		||||
    Success: EnumValue.of(2, '成功').setTagType('success'),
 | 
			
		||||
    Fail: EnumValue.of(-2, '失败').setTagType('danger'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DbDataSyncRecentStateEnum = {
 | 
			
		||||
    Success: EnumValue.of(1, '成功').setTagType('success'),
 | 
			
		||||
    Fail: EnumValue.of(-1, '失败').setTagType('danger'),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,8 @@
 | 
			
		||||
                                        tagSelectRef.validate();
 | 
			
		||||
                                    }
 | 
			
		||||
                                "
 | 
			
		||||
                                :resource-code="form.code"
 | 
			
		||||
                                :resource-type="TagResourceTypeEnum.Machine.value"
 | 
			
		||||
                                :tag-path="form.tagPath"
 | 
			
		||||
                                :select-tags="form.tagId"
 | 
			
		||||
                                style="width: 100%"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
@@ -57,7 +57,7 @@
 | 
			
		||||
 | 
			
		||||
                    <el-tab-pane label="其他配置" name="other">
 | 
			
		||||
                        <el-form-item prop="enableRecorder" label="终端回放">
 | 
			
		||||
                            <el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
 | 
			
		||||
                            <el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
 | 
			
		||||
                        <el-form-item prop="sshTunnelMachineId" label="SSH隧道">
 | 
			
		||||
@@ -85,7 +85,6 @@ import { ElMessage } from 'element-plus';
 | 
			
		||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
 | 
			
		||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
 | 
			
		||||
import AuthCertSelect from './authcert/AuthCertSelect.vue';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -153,6 +152,7 @@ const state = reactive({
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        code: '',
 | 
			
		||||
        tagPath: '',
 | 
			
		||||
        ip: null,
 | 
			
		||||
        port: 22,
 | 
			
		||||
        name: null,
 | 
			
		||||
@@ -181,7 +181,7 @@ watch(props, async (newValue: any) => {
 | 
			
		||||
    state.tabActiveName = 'basic';
 | 
			
		||||
    if (newValue.machine) {
 | 
			
		||||
        state.form = { ...newValue.machine };
 | 
			
		||||
 | 
			
		||||
        state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
 | 
			
		||||
        // 如果凭证类型为公共的,则表示使用授权凭证认证
 | 
			
		||||
        const authCertId = (state.form as any).authCertId;
 | 
			
		||||
        if (authCertId > 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
    <div class="machine-list">
 | 
			
		||||
        <page-table
 | 
			
		||||
            ref="pageTableRef"
 | 
			
		||||
            :page-api="machineApi.list"
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
                <span v-if="!data.stat">-</span>
 | 
			
		||||
                <div v-else>
 | 
			
		||||
                    <el-row>
 | 
			
		||||
                        <el-text size="small" style="font-size: 10px">
 | 
			
		||||
                        <el-text size="small" class="font11">
 | 
			
		||||
                            内存(可用/总):
 | 
			
		||||
                            <span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
 | 
			
		||||
                                >{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
                        </el-text>
 | 
			
		||||
                    </el-row>
 | 
			
		||||
                    <el-row>
 | 
			
		||||
                        <el-text style="font-size: 10px" size="small">
 | 
			
		||||
                        <el-text class="font11" size="small">
 | 
			
		||||
                            CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
 | 
			
		||||
                        </el-text>
 | 
			
		||||
                    </el-row>
 | 
			
		||||
@@ -44,7 +44,7 @@
 | 
			
		||||
                <span v-if="!data.stat?.fsInfos">-</span>
 | 
			
		||||
                <div v-else>
 | 
			
		||||
                    <el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
 | 
			
		||||
                        <el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
 | 
			
		||||
                        <el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
 | 
			
		||||
                            {{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
 | 
			
		||||
                        </el-text>
 | 
			
		||||
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
                            </template>
 | 
			
		||||
 | 
			
		||||
                            <el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
 | 
			
		||||
                                <el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
 | 
			
		||||
                                <el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
 | 
			
		||||
                                    {{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
 | 
			
		||||
                                </el-text>
 | 
			
		||||
                            </el-row>
 | 
			
		||||
@@ -80,7 +80,7 @@
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
 | 
			
		||||
                <ResourceTags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
@@ -118,26 +118,18 @@
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
 | 
			
		||||
                                终端回放
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
 | 
			
		||||
                            <el-dropdown-item
 | 
			
		||||
                                :command="{ type: 'closeCli', data }"
 | 
			
		||||
                                v-if="actionBtns[perms.closeCli]"
 | 
			
		||||
                                :disabled="!data.hasCli || data.status == -1"
 | 
			
		||||
                            >
 | 
			
		||||
                                关闭连接
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                        </el-dropdown-menu>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-dropdown>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-model="infoDialog.visible">
 | 
			
		||||
        <el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
 | 
			
		||||
            <el-descriptions title="详情" :column="3" border>
 | 
			
		||||
                <el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
 | 
			
		||||
@@ -195,7 +187,7 @@ import { useRouter, useRoute } from 'vue-router';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
import ResourceTag from '../component/ResourceTag.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
@@ -223,25 +215,24 @@ const perms = {
 | 
			
		||||
    updateMachine: 'machine:update',
 | 
			
		||||
    delMachine: 'machine:del',
 | 
			
		||||
    terminal: 'machine:terminal',
 | 
			
		||||
    closeCli: 'machine:close-cli',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), SearchItem.input('ip', 'IP'), SearchItem.input('name', '名称')];
 | 
			
		||||
 | 
			
		||||
const columns = [
 | 
			
		||||
    TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
 | 
			
		||||
    TableColumn.new('name', '名称'),
 | 
			
		||||
    TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
 | 
			
		||||
    TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
 | 
			
		||||
    TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
 | 
			
		||||
    TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
 | 
			
		||||
    TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
 | 
			
		||||
    TableColumn.new('username', '用户名'),
 | 
			
		||||
    TableColumn.new('status', '状态').isSlot().setMinWidth(85),
 | 
			
		||||
    TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
 | 
			
		||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
 | 
			
		||||
const actionBtns = hasPerms([perms.updateMachine]);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    params: {
 | 
			
		||||
@@ -320,10 +311,6 @@ const handleCommand = (commond: any) => {
 | 
			
		||||
            showRec(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'closeCli': {
 | 
			
		||||
            closeCli(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -351,17 +338,6 @@ const showTerminal = (row: any, event: PointerEvent) => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeCli = async (row: any) => {
 | 
			
		||||
    await ElMessageBox.confirm(`确定关闭该机器客户端连接?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    });
 | 
			
		||||
    await machineApi.closeCli.request({ id: row.id });
 | 
			
		||||
    ElMessage.success('关闭成功');
 | 
			
		||||
    search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const openFormDialog = async (machine: any) => {
 | 
			
		||||
    let dialogTitle;
 | 
			
		||||
    if (machine) {
 | 
			
		||||
@@ -464,10 +440,6 @@ const showRec = (row: any) => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.el-dialog__body {
 | 
			
		||||
    padding: 2px 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-dropdown-link-machine-list {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: var(--el-color-primary);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										405
									
								
								mayfly_go_web/src/views/ops/machine/MachineOp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								mayfly_go_web/src/views/ops/machine/MachineOp.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,405 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="flex-all-center">
 | 
			
		||||
        <!--    文档: https://antoniandre.github.io/splitpanes/    -->
 | 
			
		||||
        <Splitpanes class="default-theme" @resized="onResizeTagTree">
 | 
			
		||||
            <Pane size="20" max-size="30">
 | 
			
		||||
                <tag-tree
 | 
			
		||||
                    class="machine-terminal-tree"
 | 
			
		||||
                    ref="tagTreeRef"
 | 
			
		||||
                    :resource-type="TagResourceTypeEnum.Machine.value"
 | 
			
		||||
                    :tag-path-node-type="NodeTypeTagPath"
 | 
			
		||||
                >
 | 
			
		||||
                    <template #prefix="{ data }">
 | 
			
		||||
                        <SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
 | 
			
		||||
                        <SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
 | 
			
		||||
                    </template>
 | 
			
		||||
 | 
			
		||||
                    <template #suffix="{ data }">
 | 
			
		||||
                        <span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
 | 
			
		||||
                            ` ${data.params.username}@${data.params.ip}:${data.params.port}`
 | 
			
		||||
                        }}</span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </tag-tree>
 | 
			
		||||
            </Pane>
 | 
			
		||||
 | 
			
		||||
            <Pane>
 | 
			
		||||
                <div class="machine-terminal-tabs card pd5">
 | 
			
		||||
                    <el-tabs
 | 
			
		||||
                        v-if="state.tabs.size > 0"
 | 
			
		||||
                        type="card"
 | 
			
		||||
                        @tab-remove="onRemoveTab"
 | 
			
		||||
                        @tab-change="onTabChange"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                        v-model="state.activeTermName"
 | 
			
		||||
                        class="h100"
 | 
			
		||||
                    >
 | 
			
		||||
                        <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
 | 
			
		||||
                            <template #label>
 | 
			
		||||
                                <el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
 | 
			
		||||
                                    <template #reference>
 | 
			
		||||
                                        <el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
 | 
			
		||||
                                            ><Connection />
 | 
			
		||||
                                        </el-icon>
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                </el-popconfirm>
 | 
			
		||||
                                <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
 | 
			
		||||
                                    <template #reference>
 | 
			
		||||
                                        <div>
 | 
			
		||||
                                            <span class="machine-terminal-tab-label">{{ dt.label }}</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                    <template #default>
 | 
			
		||||
                                        <el-descriptions :column="1" size="small">
 | 
			
		||||
                                            <el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
 | 
			
		||||
                                        </el-descriptions>
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                </el-popover>
 | 
			
		||||
                            </template>
 | 
			
		||||
 | 
			
		||||
                            <div class="terminal-wrapper" style="height: calc(100vh - 155px)">
 | 
			
		||||
                                <TerminalBody
 | 
			
		||||
                                    :mount-init="false"
 | 
			
		||||
                                    @status-change="terminalStatusChange(dt.key, $event)"
 | 
			
		||||
                                    :ref="(el) => setTerminalRef(el, dt.key)"
 | 
			
		||||
                                    :socket-url="dt.socketUrl"
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
                    </el-tabs>
 | 
			
		||||
 | 
			
		||||
                    <el-dialog v-model="infoDialog.visible">
 | 
			
		||||
                        <el-descriptions title="详情" :column="3" border>
 | 
			
		||||
                            <el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1" label="认证方式">
 | 
			
		||||
                                {{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
 | 
			
		||||
                            </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                            <el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
 | 
			
		||||
                            <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
 | 
			
		||||
                        </el-descriptions>
 | 
			
		||||
                    </el-dialog>
 | 
			
		||||
 | 
			
		||||
                    <process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
 | 
			
		||||
 | 
			
		||||
                    <script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
 | 
			
		||||
 | 
			
		||||
                    <file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
 | 
			
		||||
 | 
			
		||||
                    <machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
 | 
			
		||||
 | 
			
		||||
                    <machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </Pane>
 | 
			
		||||
        </Splitpanes>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs, reactive, defineAsyncComponent, nextTick } from 'vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
 | 
			
		||||
import { dateFormat } from '@/common/utils/date';
 | 
			
		||||
import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { NodeType, TagTreeNode } from '../component/tag';
 | 
			
		||||
import TagTree from '../component/TagTree.vue';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { ContextmenuItem } from '@/components/contextmenu/index';
 | 
			
		||||
// 组件
 | 
			
		||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
 | 
			
		||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
 | 
			
		||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
 | 
			
		||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
 | 
			
		||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
 | 
			
		||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
			
		||||
import { TerminalStatus } from '@/components/terminal/common';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    addMachine: 'machine:add',
 | 
			
		||||
    updateMachine: 'machine:update',
 | 
			
		||||
    delMachine: 'machine:del',
 | 
			
		||||
    terminal: 'machine:terminal',
 | 
			
		||||
    closeCli: 'machine:close-cli',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
 | 
			
		||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
 | 
			
		||||
 | 
			
		||||
class MachineNodeType {
 | 
			
		||||
    static Machine = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    params: {
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 0,
 | 
			
		||||
        ip: null,
 | 
			
		||||
        name: null,
 | 
			
		||||
        tagPath: '',
 | 
			
		||||
    },
 | 
			
		||||
    infoDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    serviceDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        machineId: 0,
 | 
			
		||||
        title: '',
 | 
			
		||||
    },
 | 
			
		||||
    processDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        machineId: 0,
 | 
			
		||||
    },
 | 
			
		||||
    fileDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        machineId: 0,
 | 
			
		||||
        title: '',
 | 
			
		||||
    },
 | 
			
		||||
    machineStatsDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        stats: null,
 | 
			
		||||
        title: '',
 | 
			
		||||
        machineId: 0,
 | 
			
		||||
    },
 | 
			
		||||
    machineRecDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        machineId: 0,
 | 
			
		||||
        title: '',
 | 
			
		||||
    },
 | 
			
		||||
    activeTermName: '',
 | 
			
		||||
    tabs: new Map<string, any>(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const tagTreeRef: any = ref(null);
 | 
			
		||||
 | 
			
		||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
 | 
			
		||||
    // 加载标签树下的机器列表
 | 
			
		||||
    state.params.tagPath = node.key;
 | 
			
		||||
    state.params.pageNum = 1;
 | 
			
		||||
    state.params.pageSize = 1000;
 | 
			
		||||
    const res = await search();
 | 
			
		||||
    // 把list 根据name字段排序
 | 
			
		||||
    res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
 | 
			
		||||
    return res.list.map((x: any) =>
 | 
			
		||||
        new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
 | 
			
		||||
            .withParams(x)
 | 
			
		||||
            .withDisabled(x.status == -1)
 | 
			
		||||
            .withIcon({
 | 
			
		||||
                name: 'Monitor',
 | 
			
		||||
                color: '#409eff',
 | 
			
		||||
            })
 | 
			
		||||
            .withIsLeaf(true)
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let openIds = {};
 | 
			
		||||
 | 
			
		||||
const NodeTypeMachine = (machine: any) => {
 | 
			
		||||
    let contextMenuItems = [];
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
 | 
			
		||||
    contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
 | 
			
		||||
    if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
 | 
			
		||||
        contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
 | 
			
		||||
        // for (let k of state.tabs.keys()) {
 | 
			
		||||
        //     // 存在该机器相关的终端tab,则直接激活该tab
 | 
			
		||||
        //     if (k.startsWith(`${machine.id}_${machine.username}_`)) {
 | 
			
		||||
        //         state.activeTermName = k;
 | 
			
		||||
        //         onTabChange();
 | 
			
		||||
        //         return;
 | 
			
		||||
        //     }
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        openTerminal(machine);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const openTerminal = (machine: any, ex?: boolean) => {
 | 
			
		||||
    // 新窗口打开
 | 
			
		||||
    if (ex) {
 | 
			
		||||
        const { href } = router.resolve({
 | 
			
		||||
            path: `/machine/terminal`,
 | 
			
		||||
            query: {
 | 
			
		||||
                id: machine.id,
 | 
			
		||||
                name: machine.name,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        window.open(href, '_blank');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let { name, id, username } = machine;
 | 
			
		||||
 | 
			
		||||
    // 同一个机器的终端打开多次,key后添加下划线和数字区分
 | 
			
		||||
    openIds[id] = openIds[id] ? ++openIds[id] : 1;
 | 
			
		||||
    let sameIndex = openIds[id];
 | 
			
		||||
 | 
			
		||||
    let key = `${id}_${username}_${sameIndex}`;
 | 
			
		||||
    // 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
 | 
			
		||||
    let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
 | 
			
		||||
 | 
			
		||||
    state.tabs.set(key, {
 | 
			
		||||
        key,
 | 
			
		||||
        label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
 | 
			
		||||
        params: machine,
 | 
			
		||||
        socketUrl: getMachineTerminalSocketUrl(id),
 | 
			
		||||
    });
 | 
			
		||||
    state.activeTermName = key;
 | 
			
		||||
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
        handleReconnect(key);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const serviceManager = (row: any) => {
 | 
			
		||||
    state.serviceDialog.machineId = row.id;
 | 
			
		||||
    state.serviceDialog.visible = true;
 | 
			
		||||
    state.serviceDialog.title = `${row.name} => ${row.ip}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 显示机器状态统计信息
 | 
			
		||||
 */
 | 
			
		||||
const showMachineStats = async (machine: any) => {
 | 
			
		||||
    state.machineStatsDialog.machineId = machine.id;
 | 
			
		||||
    state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
 | 
			
		||||
    state.machineStatsDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    const res = await machineApi.list.request(state.params);
 | 
			
		||||
    return res;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showFileManage = (selectionData: any) => {
 | 
			
		||||
    state.fileDialog.visible = true;
 | 
			
		||||
    state.fileDialog.machineId = selectionData.id;
 | 
			
		||||
    state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showInfo = (info: any) => {
 | 
			
		||||
    state.infoDialog.data = info;
 | 
			
		||||
    state.infoDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showProcess = (row: any) => {
 | 
			
		||||
    state.processDialog.machineId = row.id;
 | 
			
		||||
    state.processDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showRec = (row: any) => {
 | 
			
		||||
    state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
 | 
			
		||||
    state.machineRecDialog.machineId = row.id;
 | 
			
		||||
    state.machineRecDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onRemoveTab = (targetName: string) => {
 | 
			
		||||
    let activeTermName = state.activeTermName;
 | 
			
		||||
    const tabNames = [...state.tabs.keys()];
 | 
			
		||||
    for (let i = 0; i < tabNames.length; i++) {
 | 
			
		||||
        const tabName = tabNames[i];
 | 
			
		||||
        if (tabName !== targetName) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        const nextTab = tabNames[i + 1] || tabNames[i - 1];
 | 
			
		||||
        if (nextTab) {
 | 
			
		||||
            activeTermName = nextTab;
 | 
			
		||||
        } else {
 | 
			
		||||
            activeTermName = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let info = state.tabs.get(targetName);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            terminalRefs[info.key]?.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.tabs.delete(targetName);
 | 
			
		||||
        state.activeTermName = activeTermName;
 | 
			
		||||
        onTabChange();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
 | 
			
		||||
    state.tabs.get(key).status = status;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const terminalRefs: any = {};
 | 
			
		||||
const setTerminalRef = (el: any, key: any) => {
 | 
			
		||||
    if (key) {
 | 
			
		||||
        terminalRefs[key] = el;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onResizeTagTree = () => {
 | 
			
		||||
    fitTerminal();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onTabChange = () => {
 | 
			
		||||
    fitTerminal();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fitTerminal = () => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        let info = state.tabs.get(state.activeTermName);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            terminalRefs[info.key]?.fitTerminal();
 | 
			
		||||
            terminalRefs[info.key]?.focus();
 | 
			
		||||
        }
 | 
			
		||||
    }, 100);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleReconnect = (key: string) => {
 | 
			
		||||
    terminalRefs[key].init();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.machine-terminal-tabs {
 | 
			
		||||
    height: calc(100vh - 108px);
 | 
			
		||||
    --el-tabs-header-height: 30px;
 | 
			
		||||
 | 
			
		||||
    .el-tabs {
 | 
			
		||||
        --el-tabs-header-height: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .machine-terminal-tab-label {
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
    }
 | 
			
		||||
    .el-tabs__header {
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
    .el-tabs__item {
 | 
			
		||||
        padding: 0 8px !important;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -103,8 +103,8 @@ const playRec = async (rec: any) => {
 | 
			
		||||
                idleTimeLimit: 2,
 | 
			
		||||
                // fit: false,
 | 
			
		||||
                // terminalFontSize: 'small',
 | 
			
		||||
                // cols: 100,
 | 
			
		||||
                // rows: 33,
 | 
			
		||||
                cols: 144,
 | 
			
		||||
                rows: 32,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    } finally {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ export const machineApi = {
 | 
			
		||||
    process: Api.newGet('/machines/{id}/process'),
 | 
			
		||||
    // 终止进程
 | 
			
		||||
    killProcess: Api.newDelete('/machines/{id}/process'),
 | 
			
		||||
    closeCli: Api.newDelete('/machines/{id}/close-cli'),
 | 
			
		||||
    testConn: Api.newPost('/machines/test-conn'),
 | 
			
		||||
    // 保存按钮
 | 
			
		||||
    saveMachine: Api.newPost('/machines'),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,7 @@
 | 
			
		||||
                                    }
 | 
			
		||||
                                "
 | 
			
		||||
                                multiple
 | 
			
		||||
                                :resource-code="form.code"
 | 
			
		||||
                                :resource-type="TagResourceTypeEnum.Mongo.value"
 | 
			
		||||
                                :select-tags="form.tagId"
 | 
			
		||||
                                style="width: 100%"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-form-item>
 | 
			
		||||
@@ -58,7 +57,6 @@ import { mongoApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
 | 
			
		||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -129,6 +127,7 @@ watch(props, async (newValue: any) => {
 | 
			
		||||
    state.tabActiveName = 'basic';
 | 
			
		||||
    if (newValue.mongo) {
 | 
			
		||||
        state.form = { ...newValue.mongo };
 | 
			
		||||
        state.form.tagId = newValue.mongo.tags.map((t: any) => t.tagId);
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = { db: 0 } as any;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
 | 
			
		||||
                <resource-tags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
import { mongoApi } from './api';
 | 
			
		||||
import { defineAsyncComponent, ref, toRefs, reactive, onMounted, Ref } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import ResourceTag from '../component/ResourceTag.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
@@ -62,9 +62,9 @@ const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Mongo.value)];
 | 
			
		||||
 | 
			
		||||
const columns = [
 | 
			
		||||
    TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
 | 
			
		||||
    TableColumn.new('name', '名称'),
 | 
			
		||||
    TableColumn.new('uri', '连接uri'),
 | 
			
		||||
    TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
 | 
			
		||||
    TableColumn.new('createTime', '创建时间').isTime(),
 | 
			
		||||
    TableColumn.new('creator', '创建人'),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,15 @@
 | 
			
		||||
                                <el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
 | 
			
		||||
                            </el-col>
 | 
			
		||||
                            <el-col :span="18">
 | 
			
		||||
                                <el-input @clear="clear" v-model="scanParam.match" placeholder="match 支持*模糊key" clearable size="small" class="ml10" />
 | 
			
		||||
                                <el-input
 | 
			
		||||
                                    @clear="clear"
 | 
			
		||||
                                    v-model="scanParam.match"
 | 
			
		||||
                                    @keyup.enter.native="searchKey()"
 | 
			
		||||
                                    placeholder="match 支持*模糊key, 回车搜索"
 | 
			
		||||
                                    clearable
 | 
			
		||||
                                    size="small"
 | 
			
		||||
                                    class="ml10"
 | 
			
		||||
                                />
 | 
			
		||||
                            </el-col>
 | 
			
		||||
                            <el-col :span="4">
 | 
			
		||||
                                <el-button
 | 
			
		||||
@@ -133,7 +141,7 @@
 | 
			
		||||
                <div class="key-detail card pd5">
 | 
			
		||||
                    <el-tabs @tab-remove="removeDataTab" v-model="state.activeName">
 | 
			
		||||
                        <el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
 | 
			
		||||
                            <key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="dt.keyInfo" @change-key="searchKey()" @del-key="delKey" />
 | 
			
		||||
                            <key-detail :redis="redisInst" :key-info="dt.keyInfo" @change-key="searchKey()" @del-key="delKey" />
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
                    </el-tabs>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -170,7 +178,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { redisApi } from './api';
 | 
			
		||||
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick } from 'vue';
 | 
			
		||||
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { isTrue, notBlank, notNull } from '@/common/assert';
 | 
			
		||||
import { copyToClipboard } from '@/common/utils/string';
 | 
			
		||||
@@ -181,6 +189,7 @@ import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -232,6 +241,7 @@ const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async
 | 
			
		||||
        return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
 | 
			
		||||
            id: redisInfo.id,
 | 
			
		||||
            db: x,
 | 
			
		||||
            flowProcdefKey: redisInfo.flowProcdefKey,
 | 
			
		||||
            name: `db${x}`,
 | 
			
		||||
            keys: 0,
 | 
			
		||||
        });
 | 
			
		||||
@@ -261,6 +271,11 @@ const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: T
 | 
			
		||||
    resetScanParam();
 | 
			
		||||
    state.scanParam.id = nodeData.params.id;
 | 
			
		||||
    state.scanParam.db = nodeData.params.db;
 | 
			
		||||
 | 
			
		||||
    redisInst.value.id = nodeData.params.id;
 | 
			
		||||
    redisInst.value.db = Number.parseInt(nodeData.params.db);
 | 
			
		||||
    redisInst.value.flowProcdefKey = nodeData.params.flowProcdefKey;
 | 
			
		||||
 | 
			
		||||
    scan();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -273,6 +288,7 @@ const treeProps = {
 | 
			
		||||
const defaultCount = 250;
 | 
			
		||||
 | 
			
		||||
const keyTreeRef: any = ref(null);
 | 
			
		||||
const redisInst: Ref<RedisInst> = ref(new RedisInst());
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    tags: [],
 | 
			
		||||
@@ -498,12 +514,8 @@ const flushDb = () => {
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
            redisApi.flushDb
 | 
			
		||||
                .request({
 | 
			
		||||
                    id: state.scanParam.id,
 | 
			
		||||
                    db: state.scanParam.db,
 | 
			
		||||
                })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
            // FLUSHDB [ASYNC | SYNC]
 | 
			
		||||
            redisInst.value.runCmd(['FLUSHDB']).then(() => {
 | 
			
		||||
                ElMessage.success('清除成功!');
 | 
			
		||||
                searchKey();
 | 
			
		||||
            });
 | 
			
		||||
@@ -518,19 +530,9 @@ const cancelNewKey = () => {
 | 
			
		||||
 | 
			
		||||
const newKey = async () => {
 | 
			
		||||
    const keyInfo = state.newKeyDialog.keyInfo;
 | 
			
		||||
    const keyType = keyInfo.type;
 | 
			
		||||
    const key = keyInfo.key;
 | 
			
		||||
    notBlank(key, '键名不能为空');
 | 
			
		||||
 | 
			
		||||
    if (keyType == 'string') {
 | 
			
		||||
        await redisApi.setString.request({
 | 
			
		||||
            id: state.scanParam.id,
 | 
			
		||||
            db: state.scanParam.db,
 | 
			
		||||
            key: key,
 | 
			
		||||
            value: '',
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showKeyDetail(
 | 
			
		||||
        {
 | 
			
		||||
            ...keyInfo,
 | 
			
		||||
@@ -557,11 +559,8 @@ const delKey = (key: string) => {
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    })
 | 
			
		||||
        .then(async () => {
 | 
			
		||||
            await redisApi.delKey.request({
 | 
			
		||||
                key,
 | 
			
		||||
                id: state.scanParam.id,
 | 
			
		||||
                db: state.scanParam.db,
 | 
			
		||||
            });
 | 
			
		||||
            // DEL key [key ...]
 | 
			
		||||
            await redisInst.value.runCmd(['DEL', key]);
 | 
			
		||||
            ElMessage.success('删除成功!');
 | 
			
		||||
            searchKey();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -107,10 +107,10 @@ defineExpose({ getContent });
 | 
			
		||||
 | 
			
		||||
.format-viewer-container .el-textarea textarea {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    height: calc(100vh - 546px + v-bind(height));
 | 
			
		||||
    height: calc(100vh - 550px + v-bind(height));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.format-viewer-container .monaco-editor-content {
 | 
			
		||||
    height: calc(100vh - 560px + v-bind(height)) !important;
 | 
			
		||||
    height: calc(100vh - 565px + v-bind(height)) !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@
 | 
			
		||||
            <!-- key info -->
 | 
			
		||||
            <key-header
 | 
			
		||||
                ref="keyHeader"
 | 
			
		||||
                :redis-id="redisId"
 | 
			
		||||
                :db="db"
 | 
			
		||||
                :redis="props.redis"
 | 
			
		||||
                :key-info="state.keyInfo"
 | 
			
		||||
                @refresh-content="refreshContent"
 | 
			
		||||
                @del-key="delKey"
 | 
			
		||||
@@ -15,7 +14,7 @@
 | 
			
		||||
            </key-header>
 | 
			
		||||
 | 
			
		||||
            <!-- key content -->
 | 
			
		||||
            <component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo"> </component>
 | 
			
		||||
            <component ref="keyValueRef" :is="components[componentName]" :redis="props.redis" :key-info="keyInfo"> </component>
 | 
			
		||||
        </el-container>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -23,6 +22,7 @@
 | 
			
		||||
import { defineAsyncComponent, watch, ref, shallowReactive, reactive, computed, onMounted } from 'vue';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import KeyHeader from './KeyHeader.vue';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue'));
 | 
			
		||||
const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue'));
 | 
			
		||||
@@ -41,11 +41,9 @@ const components = shallowReactive({
 | 
			
		||||
const keyValueRef = ref(null) as any;
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    redisId: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    redis: {
 | 
			
		||||
        type: RedisInst,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
@@ -55,7 +53,6 @@ const props = defineProps({
 | 
			
		||||
const emit = defineEmits(['update:visible', 'changeKey', 'delKey']);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    redisId: 0,
 | 
			
		||||
    keyInfo: {} as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,13 +46,13 @@
 | 
			
		||||
import { reactive, watch, toRefs, onMounted } from 'vue';
 | 
			
		||||
import { redisApi } from './api';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { formatTime } from '@/common/utils/format';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    redisId: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
    redis: {
 | 
			
		||||
        type: RedisInst,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
@@ -62,7 +62,6 @@ const props = defineProps({
 | 
			
		||||
const emit = defineEmits(['refreshContent', 'delKey', 'changeKey']);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    redisId: 0,
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        key: '',
 | 
			
		||||
        type: '',
 | 
			
		||||
@@ -84,8 +83,8 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
const refreshKey = async () => {
 | 
			
		||||
    const ttl = await redisApi.keyTtl.request({
 | 
			
		||||
        id: props.redisId,
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        id: props.redis.id,
 | 
			
		||||
        db: props.redis.db,
 | 
			
		||||
        key: state.oldKey,
 | 
			
		||||
    });
 | 
			
		||||
    state.keyInfo.timed = ttl;
 | 
			
		||||
@@ -100,12 +99,8 @@ const renameKey = async () => {
 | 
			
		||||
    if (!state.oldKey || state.ki.key == state.oldKey) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await redisApi.renameKey.request({
 | 
			
		||||
        id: props.redisId,
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        newKey: state.ki.key,
 | 
			
		||||
        key: state.oldKey,
 | 
			
		||||
    });
 | 
			
		||||
    // RENAME key newkey
 | 
			
		||||
    await props.redis.runCmd(['RENAME', state.oldKey, state.ki.key]);
 | 
			
		||||
    ElMessage.success('设置成功');
 | 
			
		||||
    emit('changeKey');
 | 
			
		||||
};
 | 
			
		||||
@@ -130,22 +125,15 @@ const ttlKey = async () => {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await redisApi.expireKey.request({
 | 
			
		||||
        id: props.redisId,
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        key: state.ki.key,
 | 
			
		||||
        seconds: state.ki.timed,
 | 
			
		||||
    });
 | 
			
		||||
    // EXPIRE key seconds [NX | XX | GT | LT]
 | 
			
		||||
    await props.redis.runCmd(['EXPIRE', state.ki.key, state.ki.timed]);
 | 
			
		||||
    ElMessage.success('设置成功');
 | 
			
		||||
    emit('changeKey');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const persistKey = async () => {
 | 
			
		||||
    await redisApi.persistKey.request({
 | 
			
		||||
        id: props.redisId,
 | 
			
		||||
        db: props.db,
 | 
			
		||||
        key: state.keyInfo.key,
 | 
			
		||||
    });
 | 
			
		||||
    // PERSIST key
 | 
			
		||||
    await props.redis.runCmd(['PERSIST', state.keyInfo.key]);
 | 
			
		||||
    ElMessage.success('设置成功');
 | 
			
		||||
    emit('changeKey');
 | 
			
		||||
};
 | 
			
		||||
@@ -174,33 +162,7 @@ const ttlConveter = (ttl: any) => {
 | 
			
		||||
    if (!ttl) {
 | 
			
		||||
        ttl = 0;
 | 
			
		||||
    }
 | 
			
		||||
    let second = parseInt(ttl); // 秒
 | 
			
		||||
    let min = 0; // 分
 | 
			
		||||
    let hour = 0; // 小时
 | 
			
		||||
    let day = 0;
 | 
			
		||||
    if (second > 60) {
 | 
			
		||||
        min = parseInt(second / 60 + '');
 | 
			
		||||
        second = second % 60;
 | 
			
		||||
        if (min > 60) {
 | 
			
		||||
            hour = parseInt(min / 60 + '');
 | 
			
		||||
            min = min % 60;
 | 
			
		||||
            if (hour > 24) {
 | 
			
		||||
                day = parseInt(hour / 24 + '');
 | 
			
		||||
                hour = hour % 24;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let result = '' + second + 's';
 | 
			
		||||
    if (min > 0) {
 | 
			
		||||
        result = '' + min + 'm:' + result;
 | 
			
		||||
    }
 | 
			
		||||
    if (hour > 0) {
 | 
			
		||||
        result = '' + hour + 'h:' + result;
 | 
			
		||||
    }
 | 
			
		||||
    if (day > 0) {
 | 
			
		||||
        result = '' + day + 'd:' + result;
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
    return formatTime(ttl);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
 | 
			
		||||
        <el-table size="small" border :data="hashValues" height="450" min-height="300" stripe>
 | 
			
		||||
        <el-table size="small" border :data="hashValues" height="500" min-height="300" stripe>
 | 
			
		||||
            <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
 | 
			
		||||
            <el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> </el-table-column>
 | 
			
		||||
            <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
                        class="key-detail-filter-value"
 | 
			
		||||
                        v-model="state.filterValue"
 | 
			
		||||
                        @keyup.enter="hscan(true, true)"
 | 
			
		||||
                        placeholder="输入关键词回车搜索"
 | 
			
		||||
                        placeholder="关键词回车搜索"
 | 
			
		||||
                        clearable
 | 
			
		||||
                        size="small"
 | 
			
		||||
                    />
 | 
			
		||||
@@ -51,22 +51,16 @@
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
 | 
			
		||||
import { redisApi } from './api';
 | 
			
		||||
import { ref, onMounted, reactive, toRefs } from 'vue';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { notBlank } from '@/common/assert';
 | 
			
		||||
import FormatViewer from './FormatViewer.vue';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    redisId: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    redis: {
 | 
			
		||||
        type: RedisInst,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
@@ -76,8 +70,6 @@ const props = defineProps({
 | 
			
		||||
const formatViewerRef = ref(null) as any;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    redisId: 0,
 | 
			
		||||
    db: 0,
 | 
			
		||||
    key: '',
 | 
			
		||||
    scanParam: {
 | 
			
		||||
        cursor: 0,
 | 
			
		||||
@@ -98,8 +90,6 @@ const state = reactive({
 | 
			
		||||
const { hashValues, total, loadMoreDisable, editDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    state.redisId = props.redisId;
 | 
			
		||||
    state.db = props.db;
 | 
			
		||||
    state.key = props.keyInfo?.key;
 | 
			
		||||
    initData();
 | 
			
		||||
});
 | 
			
		||||
@@ -118,16 +108,15 @@ const hscan = async (resetTableData = false, resetCursor = false) => {
 | 
			
		||||
        state.scanParam.cursor = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scanRes = await redisApi.hscan.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        match: getScanMatch(),
 | 
			
		||||
        ...state.scanParam,
 | 
			
		||||
    });
 | 
			
		||||
    state.scanParam.cursor = scanRes.cursor;
 | 
			
		||||
    state.loadMoreDisable = scanRes.cursor == 0;
 | 
			
		||||
    state.total = scanRes.keySize;
 | 
			
		||||
    props.redis.runCmd(['HLEN', state.key]).then((res) => (state.total = res));
 | 
			
		||||
 | 
			
		||||
    // HSCAN key cursor [MATCH pattern] [COUNT count]
 | 
			
		||||
    // 返回值 [coursor, keys:[]]
 | 
			
		||||
    let scanRes = await props.redis.runCmd(['HSCAN', state.key, state.scanParam.cursor, 'MATCH', getScanMatch(), 'COUNT', state.scanParam.count]);
 | 
			
		||||
    state.scanParam.cursor = scanRes[0];
 | 
			
		||||
    state.loadMoreDisable = state.scanParam.cursor == 0;
 | 
			
		||||
    const keys = scanRes[1];
 | 
			
		||||
 | 
			
		||||
    const keys = scanRes.keys;
 | 
			
		||||
    const hashValue = [];
 | 
			
		||||
    const fieldCount = keys.length / 2;
 | 
			
		||||
    let nextFieldIndex = 0;
 | 
			
		||||
@@ -143,10 +132,7 @@ const hscan = async (resetTableData = false, resetCursor = false) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const hdel = async (field: any, index: any) => {
 | 
			
		||||
    await redisApi.hdel.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        field,
 | 
			
		||||
    });
 | 
			
		||||
    await props.redis.runCmd(['HDEL', state.key, field]);
 | 
			
		||||
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
    state.hashValues.splice(index, 1);
 | 
			
		||||
@@ -161,37 +147,14 @@ const showEditDialog = (row: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditData = async () => {
 | 
			
		||||
    const param = getBaseReqParam();
 | 
			
		||||
 | 
			
		||||
    const field = state.editDialog.field;
 | 
			
		||||
    notBlank(field, 'field不能为空');
 | 
			
		||||
 | 
			
		||||
    // 存在数据行,则说明为修改,则要先删除旧数据后新增
 | 
			
		||||
    const dataRow = state.editDialog.dataRow;
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
        await redisApi.hdel.request({
 | 
			
		||||
            ...param,
 | 
			
		||||
            field: dataRow.field,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 获取hash value内容并新增
 | 
			
		||||
    const value = formatViewerRef.value.getContent();
 | 
			
		||||
    const res = await redisApi.hset.request({
 | 
			
		||||
        ...param,
 | 
			
		||||
        value: [
 | 
			
		||||
            {
 | 
			
		||||
                field,
 | 
			
		||||
                value: value,
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const res = await props.redis.runCmd(['HSET', state.key, field, value]);
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
        state.editDialog.dataRow.value = value;
 | 
			
		||||
        state.editDialog.dataRow.field = field;
 | 
			
		||||
    } else {
 | 
			
		||||
    // 响应0则为被覆盖,则重新scan
 | 
			
		||||
    if (res == 0) {
 | 
			
		||||
        hscan(true, true);
 | 
			
		||||
@@ -199,19 +162,10 @@ const confirmEditData = async () => {
 | 
			
		||||
        state.hashValues.unshift({ value, field });
 | 
			
		||||
        state.total++;
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
    state.editDialog.visible = false;
 | 
			
		||||
    state.editDialog.dataRow = null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getBaseReqParam = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        id: state.redisId,
 | 
			
		||||
        db: state.db,
 | 
			
		||||
        key: state.key,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ initData });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
 | 
			
		||||
        <el-button @click="showEditDialog(null, -1)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
 | 
			
		||||
        <el-table size="small" border :data="values" height="450" min-height="300" stripe>
 | 
			
		||||
            <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
 | 
			
		||||
            <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
 | 
			
		||||
            <el-table-column label="操作">
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
 | 
			
		||||
                    <el-link @click="showEditDialog(scope.row, scope.$index)" :underline="false" type="primary" icon="edit" plain></el-link>
 | 
			
		||||
                    <el-popconfirm title="确定删除?" @confirm="lrem(scope.row, scope.$index)">
 | 
			
		||||
                        <template #reference>
 | 
			
		||||
                            <el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml5"></el-link>
 | 
			
		||||
@@ -38,20 +38,14 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, reactive, toRefs, onMounted } from 'vue';
 | 
			
		||||
import { redisApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import FormatViewer from './FormatViewer.vue';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    redisId: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    redis: {
 | 
			
		||||
        type: RedisInst,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
@@ -61,8 +55,6 @@ const props = defineProps({
 | 
			
		||||
const formatViewerRef = ref(null) as any;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    redisId: 0,
 | 
			
		||||
    db: 0,
 | 
			
		||||
    key: '',
 | 
			
		||||
    pageNum: 1,
 | 
			
		||||
    pageSize: 50,
 | 
			
		||||
@@ -70,17 +62,15 @@ const state = reactive({
 | 
			
		||||
    values: [] as any,
 | 
			
		||||
    loadMoreDisable: false,
 | 
			
		||||
    editDialog: {
 | 
			
		||||
        index: -1,
 | 
			
		||||
        visible: false,
 | 
			
		||||
        content: '',
 | 
			
		||||
        dataRow: null as any,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { total, values, loadMoreDisable, editDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    state.redisId = props.redisId;
 | 
			
		||||
    state.db = props.db;
 | 
			
		||||
    state.key = props.keyInfo?.key;
 | 
			
		||||
    initData();
 | 
			
		||||
});
 | 
			
		||||
@@ -93,14 +83,12 @@ const initData = () => {
 | 
			
		||||
const getListValue = async (resetTableData = false) => {
 | 
			
		||||
    const pageNum = state.pageNum;
 | 
			
		||||
    const pageSize = state.pageSize;
 | 
			
		||||
    const res = await redisApi.getListValue.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        start: (pageNum - 1) * pageSize,
 | 
			
		||||
        stop: pageNum * pageSize - 1,
 | 
			
		||||
    });
 | 
			
		||||
    state.total = res.len;
 | 
			
		||||
 | 
			
		||||
    const datas = res.list.map((x: any) => {
 | 
			
		||||
    props.redis.runCmd(['LLEN', state.key]).then((res) => (state.total = res));
 | 
			
		||||
 | 
			
		||||
    // LRANGE key start stop
 | 
			
		||||
    const res = await props.redis.runCmd(['LRANGE', state.key, (pageNum - 1) * pageSize, pageNum * pageSize - 1]);
 | 
			
		||||
    const datas = res.map((x: any) => {
 | 
			
		||||
        return {
 | 
			
		||||
            value: x,
 | 
			
		||||
        };
 | 
			
		||||
@@ -114,71 +102,41 @@ const getListValue = async (resetTableData = false) => {
 | 
			
		||||
    state.loadMoreDisable = state.values.length === state.total;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// const lset = async (row: any, rowIndex: number) => {
 | 
			
		||||
//     await redisApi.setListValue.request({
 | 
			
		||||
//         ...getBaseReqParam(),
 | 
			
		||||
//         index: (state.pageNum - 1) * state.pageSize + rowIndex,
 | 
			
		||||
//         value: row.value,
 | 
			
		||||
//     });
 | 
			
		||||
//     ElMessage.success('数据保存成功');
 | 
			
		||||
// };
 | 
			
		||||
 | 
			
		||||
const showEditDialog = (row: any) => {
 | 
			
		||||
    state.editDialog.dataRow = row;
 | 
			
		||||
const showEditDialog = (row: any, index = -1) => {
 | 
			
		||||
    state.editDialog.index = index;
 | 
			
		||||
    state.editDialog.content = row ? row.value : '';
 | 
			
		||||
    state.editDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditData = async () => {
 | 
			
		||||
    const param = getBaseReqParam();
 | 
			
		||||
 | 
			
		||||
    // 存在数据行,则说明为修改,则要先删除旧数据后新增
 | 
			
		||||
    const dataRow = state.editDialog.dataRow;
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
        await redisApi.lrem.request({
 | 
			
		||||
            member: state.editDialog.dataRow.value,
 | 
			
		||||
            count: 1,
 | 
			
		||||
            ...param,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const index = state.editDialog.index;
 | 
			
		||||
    // 获取list member内容并新增
 | 
			
		||||
    const member = formatViewerRef.value.getContent();
 | 
			
		||||
    await redisApi.saveListValue.request({
 | 
			
		||||
        value: [member],
 | 
			
		||||
        ...param,
 | 
			
		||||
    });
 | 
			
		||||
    try {
 | 
			
		||||
        // 索引=-1 说明是新增
 | 
			
		||||
        if (index == -1) {
 | 
			
		||||
            // RPUSH key element [element ...]
 | 
			
		||||
            await props.redis.runCmd(['RPUSH', state.key, member]);
 | 
			
		||||
        } else {
 | 
			
		||||
            // LSET key index element
 | 
			
		||||
            await props.redis.runCmd(['LSET', state.key, index, member]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ElMessage.success('保存成功');
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
        state.editDialog.dataRow.value = member;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.values.push({ value: member });
 | 
			
		||||
        state.total++;
 | 
			
		||||
    }
 | 
			
		||||
        initData();
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.editDialog.visible = false;
 | 
			
		||||
    state.editDialog.dataRow = null;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const lrem = async (row: any, index: any) => {
 | 
			
		||||
    await redisApi.lrem.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        member: row.value,
 | 
			
		||||
        count: 1,
 | 
			
		||||
    });
 | 
			
		||||
    // LREM key count element
 | 
			
		||||
    await props.redis.runCmd(['LREM', state.key, 1, row.value]);
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
    state.values.splice(index, 1);
 | 
			
		||||
    state.total--;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getBaseReqParam = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        id: state.redisId,
 | 
			
		||||
        db: state.db,
 | 
			
		||||
        key: state.key,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ initData });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -48,20 +48,14 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, reactive, toRefs, onMounted } from 'vue';
 | 
			
		||||
import { redisApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import FormatViewer from './FormatViewer.vue';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    redisId: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: [Number],
 | 
			
		||||
        require: true,
 | 
			
		||||
        default: 0,
 | 
			
		||||
    redis: {
 | 
			
		||||
        type: RedisInst,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    keyInfo: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
@@ -71,8 +65,6 @@ const props = defineProps({
 | 
			
		||||
const formatViewerRef = ref(null) as any;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    redisId: 0,
 | 
			
		||||
    db: 0,
 | 
			
		||||
    key: '',
 | 
			
		||||
 | 
			
		||||
    filterValue: '',
 | 
			
		||||
@@ -94,8 +86,6 @@ const state = reactive({
 | 
			
		||||
const { total, setDatas, loadMoreDisable, editDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    state.redisId = props.redisId;
 | 
			
		||||
    state.db = props.db;
 | 
			
		||||
    state.key = props.keyInfo?.key;
 | 
			
		||||
    initData();
 | 
			
		||||
});
 | 
			
		||||
@@ -114,26 +104,24 @@ const sscanData = async (resetDatas = true, resetCursor = false) => {
 | 
			
		||||
    if (resetCursor) {
 | 
			
		||||
        state.scanParam.cursor = 0;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await redisApi.sscan.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        match: getScanMatch(),
 | 
			
		||||
        ...state.scanParam,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // SSCAN key cursor [MATCH pattern] [COUNT count]
 | 
			
		||||
    // 响应[cursor, vals[]]
 | 
			
		||||
    const res = await props.redis.runCmd(['SSCAN', state.key, state.scanParam.cursor, 'MATCH', getScanMatch(), 'COUNT', state.scanParam.count]);
 | 
			
		||||
    if (resetDatas) {
 | 
			
		||||
        state.setDatas = [];
 | 
			
		||||
    }
 | 
			
		||||
    res.keys.forEach((x: any) => {
 | 
			
		||||
    res[1].forEach((x: any) => {
 | 
			
		||||
        state.setDatas.push({
 | 
			
		||||
            value: x,
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    state.scanParam.cursor = res.cursor;
 | 
			
		||||
    state.loadMoreDisable = res.cursor == 0;
 | 
			
		||||
    state.scanParam.cursor = res[0];
 | 
			
		||||
    state.loadMoreDisable = state.scanParam.cursor == 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getTotal = () => {
 | 
			
		||||
    redisApi.scard.request(getBaseReqParam()).then((res) => {
 | 
			
		||||
    // SCARD key
 | 
			
		||||
    props.redis.runCmd(['SCARD', state.key]).then((res) => {
 | 
			
		||||
        state.total = res;
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@@ -145,23 +133,16 @@ const showEditDialog = (row: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditData = async () => {
 | 
			
		||||
    const param = getBaseReqParam();
 | 
			
		||||
 | 
			
		||||
    // 存在数据行,则说明为修改,则要先删除旧数据后新增
 | 
			
		||||
    const dataRow = state.editDialog.dataRow;
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
        await redisApi.srem.request({
 | 
			
		||||
            member: state.editDialog.dataRow.value,
 | 
			
		||||
            ...param,
 | 
			
		||||
        });
 | 
			
		||||
        await props.redis.runCmd(['SREM', state.key, state.editDialog.dataRow.value]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 获取set member内容并新增
 | 
			
		||||
    const member = formatViewerRef.value.getContent();
 | 
			
		||||
    await redisApi.sadd.request({
 | 
			
		||||
        member,
 | 
			
		||||
        ...param,
 | 
			
		||||
    });
 | 
			
		||||
    // SADD key member [member ...]
 | 
			
		||||
    await props.redis.runCmd(['SADD', state.key, member]);
 | 
			
		||||
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    if (dataRow) {
 | 
			
		||||
@@ -175,23 +156,13 @@ const confirmEditData = async () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const srem = async (row: any, index: any) => {
 | 
			
		||||
    await redisApi.srem.request({
 | 
			
		||||
        ...getBaseReqParam(),
 | 
			
		||||
        member: row.value,
 | 
			
		||||
    });
 | 
			
		||||
    // SREM key member [member ...]
 | 
			
		||||
    await props.redis.runCmd(['SREM', state.key, row.value]);
 | 
			
		||||
    ElMessage.success('删除成功');
 | 
			
		||||
    state.setDatas.splice(index, 1);
 | 
			
		||||
    state.total--;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getBaseReqParam = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        id: state.redisId,
 | 
			
		||||
        db: state.db,
 | 
			
		||||
        key: state.key,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ initData });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user