39 Commits

Author SHA1 Message Date
meilin.huang
986b187f0a feat: v1.7.4 2024-03-04 20:33:04 +08:00
zongyangleo
008d34c453 !107 feat:支持修改表名、注释,oracle bug修复
* fix:修复oracle查询数据参数超过1000错误 ORA-01795
* feat:支持右键重命名表
* feat:支持修改表名、表注释
2024-03-04 11:32:04 +00:00
meilin.huang
49d3f988c9 feat: redis支持工单流程审批 2024-03-02 19:08:19 +08:00
zongyangleo
76475e807e !106 feat:数据同步支持唯一键冲突策略
* refactor:sql同步
* fix: 表格右键导出菜单换行符修复
* feat:数据同步支持唯一键冲突策略
2024-03-01 04:03:03 +00:00
meilin.huang
f93231da61 feat: dbms新增支持工单流程审批 2024-02-29 22:12:50 +08:00
meilin.huang
bf75483a3c refactor: 简化api层相关调用 2024-02-25 12:46:18 +08:00
meilin.huang
b56b0187cf refactor: api层尽可能屏蔽gin框架相关代码 2024-02-24 16:30:29 +08:00
meilin.huang
7e7f02b502 refactor: 机器终端操作优化 2024-02-23 22:53:17 +08:00
meilin.huang
878985f7c5 refactor: 依赖版本升级等 2024-02-22 21:03:13 +08:00
meilin.huang
2133d9b737 fix: 终端操作col和row初始化问题修复 2024-02-18 18:42:25 +08:00
meilin.huang
d711a36749 feat: v1.7.3 2024-02-08 09:53:48 +08:00
meilin.huang
9dbf104ef1 refactor: 机器操作界面调整 2024-02-07 21:14:29 +08:00
zongyangleo
20eb06fb28 !101 feat: 新增机器操作菜单
* feat: 新增机器操作菜单
2024-02-07 06:37:59 +00:00
meilin.huang
9c20bdef39 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:33:31 +08:00
zongyangleo
3fdd98a390 !99 feat: DBMS新增kingbaseES、vastbase,还有一些优化
* refactor: 重构机器列表展示
* fix:修复编辑表问题
* refactor: 优化下拉实例显示
* feat: DBMS新增kingbaseES(已测试postgres、oracle兼容模式) 、vastbase
2024-02-06 07:32:03 +00:00
meilin.huang
d4f456c0cf Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:17:39 +08:00
kanzihuang
f2b6e15cf4 !100 定时清理数据库备份数据
* feat: 优化数据库 BINLOG 同步机制
* feat: 删除数据库实例前需删除关联的数据库备份与恢复任务
* refactor: 重构数据库备份与恢复模块
* feat: 定时清理数据库备份历史和本地 Binlog 文件
* feat: 压缩数据库备份文件
2024-02-06 07:16:56 +00:00
meilin.huang
6be0ea6aed fix: dbms数据行编辑 2024-02-01 12:05:41 +08:00
meilin.huang
eee08be2cc feat: 数据库支持编辑行数据 2024-01-31 20:41:41 +08:00
meilin.huang
252fc553f2 feat: v1.7.2 2024-01-31 12:53:27 +08:00
meilin.huang
ac2ceed3f9 refactor: code review 2024-01-30 21:56:49 +08:00
kanzihuang
3f828cc5b0 !96 删除数据库备份和恢复历史
* feat: 删除数据库备份历史
* refactor dbScheduler
* feat: 从数据库备份历史中恢复数据库
* feat: 删除数据库恢复历史记录
* refactor dbScheuler
2024-01-30 13:12:43 +00:00
zongyangleo
fc1b9ef35d !97 一些优化
* refactor: 重构表格分页组件,适配大数据量分页
* fix:定时任务修复
* feat: gaussdb单独提出来
2024-01-30 13:09:26 +00:00
meilin.huang
d0b71a1c40 refactor: dialect使用方式调整 2024-01-29 16:02:28 +08:00
meilin.huang
a743a6a05a Merge branch 'master' into dev 2024-01-29 12:21:22 +08:00
zongyangleo
0e6b9713ce !93 feat: DBMS支持mssql和一些功能优化
* feat: 表格+表格元数据缓存
* feat:跳板机支持多段跳
* fix: 所有数据库区分字段主键和自增
* feat: DBMS支持mssql
* refactor: 去除无用的getter方法
2024-01-29 04:20:23 +00:00
meilin.huang
b9afbc764d refactor: 去除无用的getter方法 2024-01-29 11:34:48 +08:00
meilin.huang
923e183a67 refactor: code review 2024-01-26 17:17:26 +08:00
meilin.huang
7e9a381641 refactor: 数据库meta使用注册方式,方便可插拔 2024-01-24 17:01:17 +08:00
zongyangleo
bed95254d0 !91 fix: oracle数据同步 bug
* fix: oracle数据同步 bug
2024-01-24 08:29:16 +00:00
meilin.huang
e4d13f3377 refactor: 引入日志切割库、indexApi拆分等 2024-01-23 19:30:28 +08:00
Coder慌
d530365ef9 !90 fix: 依赖注入支持私有变量
Merge pull request !90 from kanzihuang/feat-db-bak
2024-01-23 09:02:37 +00:00
wanli
070d4ea104 fix: 依赖注入支持私有变量 2024-01-23 16:29:41 +08:00
zongyangleo
3fc86f0fae !88 feat: dbms表支持右键菜单:删除表、编辑表、新建表、复制表
* feat: 支持复制表
* feat: dbms表支持右键菜单:删除表、编辑表、新建表
2024-01-23 04:08:02 +00:00
kanzihuang
3b77ab2727 !89 feat: 给数据库备份和恢复配置操作权限
* feat: 给数据库备份和恢复配置操作权限
* refactor: 数据库备份与恢复采用最新依赖注入机制
2024-01-23 04:06:08 +00:00
meilin.huang
76cb991282 fix: 数据同步更新时间展示等问题 2024-01-23 09:27:05 +08:00
meilin.huang
9efd20f1b9 refactor: ioc与系统初始化处理方式调整 2024-01-22 11:35:28 +08:00
kanzihuang
de5b9e46d3 !87 fix: 修复数据库备份与恢复问题
* feat: 修复数据库备份与恢复问题
* feat: 启用 BINLOG 支持全量备份和增量备份,未启用 BINLOG 仅支持全量备份
* feat: 数据库恢复后自动备份,避免数据丢失
2024-01-22 03:12:16 +00:00
meilin.huang
f27d3d200f feat: 新增简易版ioc 2024-01-21 22:52:20 +08:00
376 changed files with 12509 additions and 4724 deletions

View File

@@ -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

View File

@@ -13,7 +13,7 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -22,7 +22,7 @@
### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯 sqlite、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架

View File

@@ -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.2",
"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.15",
"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

View File

@@ -74,6 +74,34 @@
"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
}
]
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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()) // 上午好
*/

View 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;
}

View File

@@ -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>

View 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>

View File

@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
}
setTimeout(() => {
editorRef.value?.format();
editorRef.value?.focus();
editorRef.value?.format();
}, 300);
state.dialogVisible = true;

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
});
state.status = TerminalStatus.Connected;
// 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
focus();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendCmd(props.cmd + ' \r');
}
};
// 自适应终端
const fitTerminal = () => {
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
if (!dimensions) {
return;
}
if (dimensions?.cols && dimensions?.rows) {
term.resize(dimensions.cols, dimensions.rows);
}
};
const focus = () => {
setTimeout(() => term.focus(), 400);
};
const clear = () => {
term.clear();
term.clearSelection();
term.focus();
};
}
function initSocket() {
if (props.socketUrl) {
let socketUrl = `${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`;
socket = new WebSocket(socketUrl);
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
}
// 监听socket连接
socket.onopen = () => {
onConnected();
// 注册心跳
pingInterval = setInterval(sendPing, 15000);
state.status = TerminalStatus.Connected;
// 注册 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');
}
};
// 监听socket错误信息
@@ -197,19 +181,46 @@ 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;
socket.onmessage = (msg: any) => {
// msg.data是真正后端返回的数据
term.write(msg.data);
};
}
function getMessage(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,
@@ -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 {

View File

@@ -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;

View 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',
},
};

View File

@@ -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);

View File

@@ -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格式传参

View File

@@ -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>

View File

@@ -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">

View File

@@ -5,26 +5,39 @@
<!-- ssh终端主题 -->
<el-divider content-position="left">终端主题</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
</el-color-picker>
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalTheme" size="small" style="width: 140px">
<el-option v-for="(_, k) in themes" :key="k" :label="k" :value="k"> </el-option>
<el-option label="自定义" value="custom"> </el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
</el-color-picker>
<template v-if="themeConfig.terminalTheme == 'custom'">
<div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
</el-color-picker>
</div>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
</el-color-picker>
</div>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
</el-color-picker>
</div>
</div>
</template>
<div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input-number
@@ -39,7 +52,7 @@
</el-input-number>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
@@ -418,6 +431,7 @@ import { useThemeConfig } from '@/store/themeConfig';
import { getLightColor } from '@/common/utils/theme';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
import themes from '@/components/terminal/themes';
const copyConfigBtnRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig());
@@ -615,6 +629,9 @@ const setLocalThemeConfigStyle = () => {
};
// 一键复制配置
const onCopyConfigClick = (target: any) => {
if (!target) {
return;
}
let copyThemeConfig = getLocal('themeConfig');
copyThemeConfig.isDrawer = false;
const clipboard = new ClipboardJS(target, {
@@ -690,6 +707,25 @@ defineExpose({ openDrawer });
</script>
<style scoped lang="scss">
::v-deep(.el-drawer) {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
}
.layout-breadcrumb-seting-bar {
height: calc(100vh - 50px);
padding: 0 15px;

View 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);
};

View File

@@ -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);
};

View File

@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
state.tagsRefsIndex = k;
try {
router.push(v);
} catch (e) {}
} catch (e) {
// skip
}
};
// 更新滚动条显示
const updateScrollbar = () => {

View File

@@ -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';
}
}
},
},
});

View File

@@ -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);

View File

@@ -7,335 +7,353 @@
------------------------------- */
// 菜单搜索
.el-autocomplete-suggestion__wrap {
max-height: 280px !important;
max-height: 280px !important;
}
/* Form 表单
------------------------------- */
.el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// 修复行内表单最后一个 el-form-item 位置下移问题
&.el-form--inline {
.el-form-item--large.el-form-item:last-of-type {
margin-bottom: 22px !important;
}
.el-form-item--default.el-form-item:last-of-type,
.el-form-item--small.el-form-item:last-of-type {
margin-bottom: 18px !important;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// 修复行内表单最后一个 el-form-item 位置下移问题
&.el-form--inline {
.el-form-item--large.el-form-item:last-of-type {
margin-bottom: 22px !important;
}
.el-form-item--default.el-form-item:last-of-type,
.el-form-item--small.el-form-item:last-of-type {
margin-bottom: 18px !important;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
}
/* Alert 警告
------------------------------- */
.el-alert {
border: 1px solid;
border: 1px solid;
}
.el-alert__title {
word-break: break-all;
word-break: break-all;
}
/* Message 消息提示
------------------------------- */
.el-message {
min-width: unset !important;
padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
min-width: unset !important;
padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
}
/* NavMenu 导航菜单
------------------------------- */
// 鼠标 hover 时颜色
.el-menu-hover-bg-color {
background-color: var(--bg-menuBarActiveColor) !important;
background-color: var(--bg-menuBarActiveColor) !important;
}
// 默认样式修改
.el-menu {
border-right: none !important;
width: 220px;
border-right: none !important;
width: 220px;
}
.el-menu-item {
height: 56px !important;
line-height: 56px !important;
height: 56px !important;
line-height: 56px !important;
}
.el-menu-item,
.el-sub-menu__title {
color: var(--bg-menuBarColor);
color: var(--bg-menuBarColor);
}
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse {
width: 64px !important;
width: 64px !important;
}
// 外部链接时
.el-menu-item a,
.el-menu-item a:hover,
.el-menu-item i,
.el-sub-menu__title i {
color: inherit;
text-decoration: none;
color: inherit;
text-decoration: none;
}
// 第三方图标字体间距/大小设置
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
@include generalIcon;
@include generalIcon;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title,
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
@extend .el-menu-hover-bg-color;
@extend .el-menu-hover-bg-color;
}
.el-menu-item:hover {
@extend .el-menu-hover-bg-color;
@extend .el-menu-hover-bg-color;
}
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
background-color: unset !important;
background-color: unset !important;
}
// 子级菜单背景颜色
// .el-menu--inline {
// background: var(--next-bg-menuBar-light-1);
// }
// 水平菜单、横向菜单折叠 a 标签
.el-popper.is-dark a {
color: var(--el-color-white) !important;
text-decoration: none;
color: var(--el-color-white) !important;
text-decoration: none;
}
// 水平菜单、横向菜单折叠背景色
.el-popper.is-pure.is-light {
// 水平菜单
.el-menu--vertical {
background: var(--bg-menuBar);
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
// 水平菜单
.el-menu--vertical {
background: var(--bg-menuBar);
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
// 横向菜单(经典、横向)布局
.el-menu.el-menu--horizontal {
border-bottom: none !important;
width: 100% !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--bg-topBarColor);
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
}
border-bottom: none !important;
width: 100% !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--bg-topBarColor);
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
}
}
// 菜单收起时,图标不居中问题
.el-menu--collapse {
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
}
.el-sub-menu__title {
padding-right: 0 !important;
}
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
}
.el-sub-menu__title {
padding-right: 0 !important;
}
}
/* Tabs 标签页
------------------------------- */
.el-tabs__nav-wrap::after {
height: 1px !important;
height: 1px !important;
}
/* Dropdown 下拉菜单
------------------------------- */
.el-dropdown-menu {
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
}
.el-dropdown-menu .el-dropdown-menu__item {
white-space: nowrap;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
list-style: none !important;
/*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
}
/* Steps 步骤条
------------------------------- */
.el-step__icon-inner {
font-size: 30px !important;
font-weight: 400 !important;
}
.el-step__title {
font-size: 14px;
.el-dropdown-menu .el-dropdown-menu__item {
white-space: nowrap;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
}
/* Dialog 对话框
------------------------------- */
.el-overlay {
overflow: hidden;
.el-overlay-dialog {
display: flex;
align-items: center;
justify-content: center;
position: unset !important;
width: 100%;
height: 100%;
.el-dialog {
margin: 0 auto !important;
position: absolute;
.el-dialog__body {
padding: 20px !important;
}
}
}
overflow: hidden;
.el-overlay-dialog {
display: flex;
align-items: center;
justify-content: center;
position: unset !important;
width: 100%;
height: 100%;
.el-dialog {
margin: 0 auto !important;
position: absolute;
.el-dialog__body {
padding: 20px !important;
}
}
}
}
.el-dialog__body {
max-height: calc(90vh - 111px) !important;
overflow-y: auto;
overflow-x: hidden;
max-height: calc(90vh - 111px) !important;
overflow-y: auto;
overflow-x: hidden;
}
/* Card 卡片
------------------------------- */
.el-card__header {
padding: 15px 20px;
padding: 15px 20px;
}
/* Table 表格 element plus 2.2.0 版本
------------------------------- */
.el-table {
.el-button.is-text {
padding: 0;
}
.el-button.is-text {
padding: 0;
}
}
/* scrollbar
------------------------------- */
.el-scrollbar__bar {
z-index: 4;
z-index: 4;
}
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
.el-scrollbar__wrap {
max-height: 100%;
max-height: 100%;
}
.el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important;
overflow-x: scroll !important;
}
/*修复Select 选择器高度问题*/
.el-select-dropdown__wrap {
max-height: 274px !important;
max-height: 274px !important;
}
/*修复Cascader 级联选择器高度问题*/
.el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important;
height: 204px !important;
}
/*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/
.layout-container-view .el-scrollbar__view {
height: 100%;
height: 100%;
}
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
.layout-columns-warp .layout-aside .el-scrollbar__view {
height: unset !important;
height: unset !important;
}
/* Pagination 分页
------------------------------- */
.el-pagination__editor {
margin-right: 8px;
margin-right: 8px;
}
/*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active {
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
}
/* Drawer 抽屉
------------------------------- */
.el-drawer {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
color: var(--el-text-color-primary);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
}
/* Breadcrumb 面包屑
------------------------------- */
.el-breadcrumb__inner a:hover,
.el-breadcrumb__inner.is-link:hover {
color: var(--el-color-primary);
color: var(--el-color-primary);
}
.el-breadcrumb__inner a,
.el-breadcrumb__inner.is-link {
color: var(--bg-topBarColor);
font-weight: normal;
color: var(--bg-topBarColor);
font-weight: normal;
}
// el-tooltip使用自定义主题时的样式
.el-popper.is-customized {
/* Set padding to ensure the height is 32px */
// padding: 6px 12px;
// padding: 6px 12px;
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
}
.el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0;
@@ -343,7 +361,9 @@
.el-dialog {
border-radius: 6px; /* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
border-radius: 6px;
/* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
/* 添加轻微阴影效果 */
border: 1px solid var(--el-border-color-lighter);
}

View File

@@ -52,6 +52,7 @@ declare interface ThemeConfigState {
logoIcon: string;
globalI18n: string;
globalComponentSize: string;
terminalTheme: string;
terminalForeground: string;
terminalBackground: string;
terminalCursor: string;

View File

@@ -98,4 +98,3 @@ export default {
}
}
</style>
@/router/staticRouter

View 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>

View 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>

View 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>

View 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>

View 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>

View 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}'),
};

View File

@@ -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>

View 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>

View 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命令'),
};

View 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>

View 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>

View File

@@ -73,12 +73,28 @@ const currentTime = computed(() => {
// 初始化数字滚动
const initNumCountUp = async () => {
const res: any = await indexApi.getIndexCount.request();
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();
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();
});
});
};
@@ -93,7 +109,7 @@ const toPage = (item: any) => {
break;
}
case 'machineNum': {
router.push('/machine/machines');
router.push('/machine/machines-op');
break;
}
case 'dbNum': {

View File

@@ -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'),
};

View File

@@ -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>

View 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>

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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);

View File

@@ -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 右击菜单按钮选项

View File

@@ -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();
}
};

View 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>

View File

@@ -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>

View File

@@ -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];
}
}
};

View File

@@ -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);
};

View File

@@ -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;
}
callback();
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(() => {

View File

@@ -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 });

View File

@@ -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;
}

View File

@@ -9,10 +9,19 @@
</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 v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
@@ -95,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({
@@ -153,33 +162,6 @@ 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',
},
{
type: 'sqlite',
label: 'sqlite',
},
];
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
@@ -196,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;
@@ -218,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;
}
});
@@ -249,7 +231,7 @@ const testConn = async () => {
return false;
}
state.subimtForm = await getReqForm();
state.submitForm = await getReqForm();
await testConnExec();
ElMessage.success('连接成功');
});
@@ -270,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);

View File

@@ -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);

View File

@@ -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,25 +228,40 @@ 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
);
}
};
// tagpath 节点类型
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) {
return [];
}
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
});
// tagpath 节点类型
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) {
return [];
}
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
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) => {
@@ -277,23 +307,29 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.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).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
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 NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => NodeTypeTables(parentNode.params))
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
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 });
@@ -301,7 +337,7 @@ 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;
@@ -309,11 +345,16 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
const tablesNode = tables.map((x: any) => {
const tableSize = x.dataLength + x.indexLength;
dbTableSize += tableSize;
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
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: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
@@ -339,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 params = nodeData.params;
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
});
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)
@@ -384,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,
@@ -407,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';
};
@@ -602,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);
};
/**
* 获取当前操作的数据库信息
*/
@@ -613,6 +785,7 @@ const getNowDbInfo = () => {
name: di.name,
type: di.type,
host: di.host,
flowProcdefKey: di.flowProcdefKey,
dbName: state.db,
};
};

View File

@@ -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-form-item prop="updField" label="更新字段" required>
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
</el-form-item>
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值支持带别名t.create_time" placement="top">
<el-form-item prop="updField" label="更新字段" required>
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="8">
@@ -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">

View File

@@ -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'),
};

View File

@@ -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>

View File

@@ -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, 否则格式化编辑器所有内容

View File

@@ -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;

View File

@@ -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;
}
if (runSuccess) {
if (runSuccessCallback) {
runSuccessCallback();
} finally {
if (runSuccess) {
if (props.runSuccessCallback) {
props.runSuccessCallback();
}
}
state.btnLoading = false;
cancel();
}
state.btnLoading = false;
};
const cancel = () => {
state.dialogVisible = false;
// 没有执行成功,并且取消回调函数存在,则执行
if (!runSuccess && cancelCallback) {
cancelCallback();
}
props.cancelCallback && props.cancelCallback();
setTimeout(() => {
state.dbId = 0;
state.sqlValue = '';
state.remark = '';
runSuccessCallback = null;
cancelCallback = null;
runSuccess = false;
}, 200);
};
const open = (props: SqlExecProps) => {
runSuccessCallback = props.runSuccessCallback;
cancelCallback = props.cancelCallback;
props.dbType = props.dbType || 'mysql';
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
state.dbId = props.dbId;
state.db = props.db;
const open = () => {
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType || 'mysql' });
state.dialogVisible = true;
nextTick(() => {
setTimeout(() => {
remarkInputRef.value?.focus();
});
});
setTimeout(() => {
remarkInputRef.value?.focus();
}, 200);
};
defineExpose({ open });

View File

@@ -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 {

View File

@@ -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;
}
sql = sql.substring(0, sql.length - 1);
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
res += sql;
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
}
dbInst.promptExeSql(
db,
res,
() => {},
() => {
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
}
);
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
});
};
const cancelUpdateFields = () => {
@@ -870,7 +885,13 @@ defineExpose({
position: absolute;
top: -5px;
padding: 2px;
height: 12px;
}
.column-right {
position: absolute;
top: 2px;
right: 0;
padding: 2px;
}
}
</style>

View File

@@ -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>

View File

@@ -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 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>
<div style="font-size: 12px; padding: 0 10px; color: #606266">
<span>{{ state.sql }}</span>
</div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
<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"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeAddDataDialog">取消</el-button>
<el-button type="primary" @click="addRow">确定</el-button>
</span>
</template>
</el-dialog>
<DbTableDataForm
:db-inst="getNowDbInst()"
:db-name="dbName"
:columns="columns"
:title="addDataDialog.title"
:table-name="tableName"
v-model:visible="addDataDialog.visible"
v-model="addDataDialog.data"
@submit-success="onRefresh"
/>
</div>
</template>
@@ -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 () => {
await selectData();
};
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 = parseInt(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 = [];
}
const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res;
} 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>

View File

@@ -30,7 +30,7 @@
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-option
v-for="pgsqlType in state.columnTypeList"
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
:key="pgsqlType.dataType"
:value="pgsqlType.udtName"
:label="pgsqlType.dataType"
@@ -56,7 +56,7 @@
v-else-if="item.prop === 'auto_increment'"
size="small"
v-model="scope.row.auto_increment"
:disabled="dbType === DbType.postgresql"
:disabled="disableEditIncr()"
/>
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
@@ -99,9 +99,7 @@
<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>
@@ -153,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;
@@ -171,8 +172,6 @@ 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: [
@@ -263,17 +262,32 @@ const state = reactive({
},
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;
}
@@ -364,6 +382,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
oldName && (newMap[oldName] = a);
if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
// 新增
data.changed = true;
data.add.push(a);
}
});
@@ -373,6 +392,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
let newData = newMap[k];
if (!newData) {
// 删除
data.changed = true;
data.del.push(a);
} else {
// 判断每个字段是否相等,否则为修改
@@ -380,6 +400,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
let oldV = a[f];
let newV = newData[f];
if (oldV?.toString() !== newV?.toString()) {
data.changed = true;
data.upd.push(newData);
break;
}
@@ -391,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(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
return dbDialect.getModifyColumnSql(data, data.tableName, changeData);
} else if (state.activeName === '2') {
// 修改索引
let changeData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
return dbDialect.getModifyIndexSql(data.tableName, changeData);
}
// 修改
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
// 修改索引
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
// 修改表名,表注释
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
let sqlArr = [];
colSql && sqlArr.push(colSql);
idxSql && sqlArr.push(idxSql);
tableInfoSql && sqlArr.push(tableInfoSql);
return sqlArr.join(';');
}
};
@@ -417,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) => {
@@ -459,6 +469,21 @@ const indexChanges = (row: any) => {
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
};
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) => {
@@ -466,13 +491,18 @@ watch(
// 回显表名表注释
state.tableData.tableName = row.tableName;
state.tableData.tableComment = row.tableComment;
state.tableData.oldTableName = row.tableName;
state.tableData.oldTableComment = row.tableComment;
state.tableData.db = props.db!;
state.tableData.fields.oldFields = [];
state.tableData.fields.res = [];
state.tableData.indexs.oldIndexs = [];
state.tableData.indexs.res = [];
// 索引列下拉选
state.tableData.indexs.columns = [];
// 回显列
if (columns && Array.isArray(columns) && columns.length > 0) {
state.tableData.fields.oldFields = [];
state.tableData.fields.res = [];
// 索引列下拉选
state.tableData.indexs.columns = [];
columns.forEach((a) => {
let typeObj = a.columnType.replace(')', '').split('(');
let type = typeObj[0];
@@ -491,8 +521,8 @@ watch(
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);
@@ -501,10 +531,9 @@ watch(
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
});
}
// 回显索引
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
state.tableData.indexs.oldIndexs = [];
state.tableData.indexs.res = [];
// 索引过滤掉主键
indexs
.filter((a) => a.indexName !== 'PRIMARY')
@@ -512,7 +541,7 @@ 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,
};

View File

@@ -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, DbType.sqlite], // 支持"编辑表"的数据库类型
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();
},

View File

@@ -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;
if (skipNull && data[colName] == null) {
continue;
}
colNames.push(this.wrapName(colName));
values.push(DbInst.wrapValueByType(data[colName]));
values.push(dbDialect.wrapValue(column.dataType, data[colName]));
}
sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
sqls.push(`INSERT INTO ${schema}${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
}
return sqls.join(';\n') + ';';
}
/**
* 生成根据主键更新语句
* @param dbName 数据库名
* @param table 表名
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
* @param rowData 表的一行完整数据(需要获取主键信息)
*/
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
let schema = '';
let dbArr = dbName.split('/');
if (dbArr.length == 2) {
schema = this.wrapName(dbArr[1]) + '.';
}
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
// 主键列信息
const primaryKey = await this.loadTableColumn(dbName, table);
let primaryKeyType = primaryKey.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);

View File

@@ -4,6 +4,7 @@ import {
DataType,
DbDialect,
DialectInfo,
DuplicateStrategy,
EditorCompletion,
EditorCompletionItem,
IndexDefinition,
@@ -366,6 +367,7 @@ class DMDialect implements DbDialect {
};
dmDialectInfo = {
name: 'DM',
icon: 'iconfont icon-db-dm',
defaultPort: 5236,
formatSqlDialect: 'plsql',
@@ -375,12 +377,12 @@ class DMDialect implements DbDialect {
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[] {
@@ -620,7 +622,7 @@ class DMDialect implements DbDialect {
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 dropIndexNames: string[] = [];
let addIndexs: any[] = [];
@@ -661,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)) {
@@ -681,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});`;
}
}
}

View 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};`;
}
}

View File

@@ -4,6 +4,10 @@ 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;
@@ -80,6 +84,11 @@ export const ColumnTypeSubscript = {
// 数据库基础信息
export interface DialectInfo {
/**
* 数据库类型label
*/
name: string;
/**
* 图标
*/
@@ -110,11 +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:
@@ -125,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 {
/**
* 获取一些数据库默认信息
@@ -133,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;
@@ -175,43 +216,62 @@ export interface DbDialect {
/**
* 生成编辑索引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;
/** 包装字符串数据, 如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();
let sqliteDialect = new SqliteDialect();
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;
case DbType.sqlite:
return sqliteDialect;
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());
})();

View 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;
}
}

View File

@@ -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;
}

View 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;
}
}

View File

@@ -1,5 +1,15 @@
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 };
@@ -104,6 +114,7 @@ class MysqlDialect implements DbDialect {
};
mysqlDialectInfo = {
name: 'MySQL',
icon: 'iconfont icon-op-mysql',
defaultPort: 3306,
formatSqlDialect: 'mysql',
@@ -113,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
@@ -226,7 +237,6 @@ class MysqlDialect implements DbDialect {
}
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
let arr = [] as string[];
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
@@ -249,10 +259,15 @@ class MysqlDialect implements DbDialect {
});
}
return sql + arr.join(',') + ';';
if (arr.length > 0) {
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
return sql + arr.join(',') + ';';
}
return '';
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1`
@@ -308,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;
@@ -326,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});`;
}
}

View File

@@ -4,6 +4,7 @@ import {
DataType,
DbDialect,
DialectInfo,
DuplicateStrategy,
EditorCompletion,
EditorCompletionItem,
IndexDefinition,
@@ -84,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格式转换为日期类型' },
// 其他
@@ -92,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 {
@@ -104,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
@@ -118,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',
@@ -142,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 (
@@ -288,6 +311,7 @@ class OracleDialect implements DbDialect {
let createSql = '';
let tableCommentSql = '';
let columCommentSql = '';
let pris = [] as string[];
// 创建表结构
let fields: string[] = [];
@@ -297,9 +321,18 @@ class OracleDialect implements DbDialect {
if (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 ${dbTable} ( ${fields.join(',')} );`;
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ) ${prisql ? ',' + prisql : ''};`;
// 表注释
if (data.tableComment) {
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
@@ -354,7 +387,7 @@ class OracleDialect implements DbDialect {
}
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
if (a.pri) {
priArr.add(`${this.quoteIdentifier(a.name)}"`);
priArr.add(`${this.quoteIdentifier(a.name)}`);
}
});
}
@@ -390,8 +423,8 @@ class OracleDialect implements DbDialect {
}
}
let modifySql = baseSql + modifyArr.join(' ') + ';';
let dropSql = baseSql + ` DROP (${dropArr.join(',')}) ;`;
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(';');
@@ -399,7 +432,7 @@ class OracleDialect implements DbDialect {
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 dropIndexNames: string[] = [];
let addIndexs: any[] = [];
@@ -441,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;
@@ -451,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});`;
}
}
}

View File

@@ -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() === 'pg_systimestamp()' && 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;
@@ -302,11 +304,15 @@ 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} ("${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(';');
@@ -365,7 +371,10 @@ class PostgresqlDialect implements DbDialect {
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[] = [];
@@ -399,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}'`);
}
});
}
@@ -410,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;
@@ -429,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};`;
}
}

View File

@@ -3,6 +3,7 @@ import {
DataType,
DbDialect,
DialectInfo,
DuplicateStrategy,
EditorCompletion,
EditorCompletionItem,
IndexDefinition,
@@ -123,6 +124,7 @@ class SqliteDialect implements DbDialect {
};
sqliteDialectInfo = {
name: 'Sqlite',
icon: 'iconfont icon-sqlite',
defaultPort: 0,
formatSqlDialect: 'sql',
@@ -132,7 +134,7 @@ class SqliteDialect implements DbDialect {
return sqliteDialectInfo;
}
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
@@ -145,7 +147,7 @@ class SqliteDialect implements DbDialect {
getDefaultRows(): RowDefinition[] {
return [
{ name: 'id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
{ 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',
@@ -284,7 +286,7 @@ class SqliteDialect implements DbDialect {
return sql.join(';') + ';';
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// sqlite创建索引需要先删除再创建
// CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );
@@ -312,6 +314,19 @@ class SqliteDialect implements DbDialect {
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;
@@ -330,8 +345,25 @@ class SqliteDialect 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 {
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});`;
}
}

View 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;
}
}

View File

@@ -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'),

View File

@@ -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) {

View File

@@ -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);

View 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>

View File

@@ -103,8 +103,8 @@ const playRec = async (rec: any) => {
idleTimeLimit: 2,
// fit: false,
// terminalFontSize: 'small',
// cols: 100,
// rows: 33,
cols: 144,
rows: 32,
});
});
} finally {

View File

@@ -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'),

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -141,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>
@@ -178,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';
@@ -189,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'));
@@ -240,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,
});
@@ -269,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();
});
@@ -281,6 +288,7 @@ const treeProps = {
const defaultCount = 250;
const keyTreeRef: any = ref(null);
const redisInst: Ref<RedisInst> = ref(new RedisInst());
const state = reactive({
tags: [],
@@ -506,15 +514,11 @@ const flushDb = () => {
type: 'warning',
})
.then(() => {
redisApi.flushDb
.request({
id: state.scanParam.id,
db: state.scanParam.db,
})
.then(() => {
ElMessage.success('清除成功!');
searchKey();
});
// FLUSHDB [ASYNC | SYNC]
redisInst.value.runCmd(['FLUSHDB']).then(() => {
ElMessage.success('清除成功!');
searchKey();
});
})
.catch(() => {});
};
@@ -526,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,
@@ -565,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();

View File

@@ -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,
});

View File

@@ -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">

View File

@@ -52,21 +52,15 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive, toRefs } from 'vue';
import { redisApi } from './api';
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,57 +147,25 @@ 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;
// 响应0则为被覆盖则重新scan
if (res == 0) {
hscan(true, true);
} else {
// 响应0则为被覆盖则重新scan
if (res == 0) {
hscan(true, true);
} else {
state.hashValues.unshift({ value, field });
state.total++;
}
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">

View File

@@ -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++;
ElMessage.success('保存成功');
initData();
} finally {
state.editDialog.visible = false;
}
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>

View File

@@ -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>

View File

@@ -12,21 +12,15 @@
</template>
<script lang="ts" setup>
import { ref, watch, reactive, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notEmpty } 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],
@@ -36,8 +30,6 @@ const props = defineProps({
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
keyInfo: {
key: '',
@@ -61,8 +53,6 @@ watch(props, (newVal) => {
});
const setProps = (val: any) => {
state.redisId = val.redisId;
state.db = val.db;
state.key = val.keyInfo?.key;
initData();
};
@@ -73,7 +63,7 @@ const initData = () => {
const getStringValue = async () => {
if (state.key) {
state.string.value = await redisApi.getString.request(getBaseReqParam());
state.string.value = await props.redis.runCmd(['GET', state.key]);
}
};
@@ -81,21 +71,10 @@ const saveValue = async () => {
state.string.value = formatViewerRef.value.getContent();
notEmpty(state.string.value, 'value不能为空');
await redisApi.setString.request({
...getBaseReqParam(),
value: state.string.value,
});
await props.redis.runCmd(['SET', state.key, state.string.value]);
ElMessage.success('数据保存成功');
};
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key,
};
};
defineExpose({ initData });
</script>
<style lang="scss">

View File

@@ -52,20 +52,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],
@@ -75,8 +69,6 @@ const props = defineProps({
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
filterValue: '',
scanCursor: 0,
@@ -96,8 +88,6 @@ const state = reactive({
const { total, values, loadMoreDisable, editDialog } = toRefs(state);
onMounted(() => {
state.redisId = props.redisId;
state.db = props.db;
state.key = props.keyInfo?.key;
initData();
});
@@ -105,7 +95,7 @@ onMounted(() => {
const initData = async () => {
state.pageNum = 1;
state.filterValue = '';
await getTotal();
getTotal();
await zrevrange(true);
};
@@ -120,17 +110,14 @@ const loadDatas = (resetTableData = false) => {
const zrevrange = async (resetTableData = false) => {
const pageNum = state.pageNum;
const pageSize = state.pageSize;
const res = await redisApi.zrevrange.request({
...getBaseReqParam(),
start: (pageNum - 1) * pageSize,
stop: pageNum * pageSize - 1,
});
// ZREVRANGE key start stop [WITHSCORES]
const res = await props.redis.runCmd(['ZREVRANGE', state.key, (pageNum - 1) * pageSize, pageNum * pageSize - 1, 'WITHSCORES']);
const vs = [];
for (let member of res) {
vs.push({
score: member.Score,
value: member.Member,
value: member[0],
score: member[1],
});
}
if (resetTableData) {
@@ -150,14 +137,11 @@ const zscanData = async (resetTableData = true, resetCursor = false) => {
if (resetCursor) {
state.scanCursor = 0;
}
const res = await redisApi.zscan.request({
...getBaseReqParam(),
match: getScanMatch(),
cursor: state.scanCursor,
count: state.pageSize,
});
// ZSCAN key cursor [MATCH pattern] [COUNT count]
// 响应[coursor, vals[]]
const res = await props.redis.runCmd(['ZSCAN', state.key, state.scanCursor, 'MATCH', getScanMatch(), 'COUNT', state.pageSize]);
const keys = res.keys;
const keys = res[1];
const vs = [];
const memCount = keys.length / 2;
let nextMemndex = 0;
@@ -171,20 +155,15 @@ const zscanData = async (resetTableData = true, resetCursor = false) => {
state.values.push(...vs);
}
state.scanCursor = res.cursor;
state.loadMoreDisable = res.cursor == 0;
state.scanCursor = res[0];
state.loadMoreDisable = state.scanCursor == 0;
};
const getTotal = () => {
redisApi.zcard
.request({
id: state.redisId,
db: state.db,
key: state.key,
})
.then((res) => {
state.total = res;
});
// ZCARD key
props.redis.runCmd(['ZCARD', state.key]).then((res) => {
state.total = res;
});
};
const showEditDialog = (row: any) => {
@@ -195,25 +174,18 @@ const showEditDialog = (row: any) => {
};
const confirmEditData = async () => {
const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow;
if (dataRow) {
await redisApi.zrem.request({
member: state.editDialog.dataRow.value,
...param,
});
// ZREM key member [member ...]
await props.redis.runCmd(['ZREM', state.key, state.editDialog.dataRow.value]);
}
const score = state.editDialog.score;
// 获取zset member内容并新增
const member = formatViewerRef.value.getContent();
await redisApi.zadd.request({
score,
member,
...param,
});
// ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member...]
await props.redis.runCmd(['ZADD', state.key, score, member]);
ElMessage.success('保存成功');
if (dataRow) {
@@ -228,23 +200,12 @@ const confirmEditData = async () => {
};
const zrem = async (row: any, index: any) => {
await redisApi.zrem.request({
...getBaseReqParam(),
member: row.value,
});
await props.redis.runCmd(['ZREM', state.key, 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>

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