mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-05-30 06:45:24 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
986b187f0a | ||
|
|
008d34c453 | ||
|
|
49d3f988c9 | ||
|
|
76475e807e | ||
|
|
f93231da61 | ||
|
|
bf75483a3c | ||
|
|
b56b0187cf | ||
|
|
7e7f02b502 | ||
|
|
878985f7c5 | ||
|
|
2133d9b737 | ||
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc |
@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn build
|
||||
|
||||
# 构建后端资源
|
||||
FROM golang:1.21.5 as be-builder
|
||||
FROM golang:1.22 as be-builder
|
||||
|
||||
ENV GOPROXY https://goproxy.cn
|
||||
WORKDIR /mayfly
|
||||
|
||||
@@ -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 等集工单流程审批于一体的统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -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.3",
|
||||
"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,20 +44,20 @@
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/compiler-sfc": "^3.4.14",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/compiler-sfc": "^3.4.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.12",
|
||||
"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
@@ -88,6 +88,20 @@
|
||||
"font_class": "gauss",
|
||||
"unicode": "e683",
|
||||
"unicode_decimal": 59011
|
||||
},
|
||||
{
|
||||
"icon_id": "34836637",
|
||||
"name": "kingbase",
|
||||
"font_class": "kingbase",
|
||||
"unicode": "e882",
|
||||
"unicode_decimal": 59522
|
||||
},
|
||||
{
|
||||
"icon_id": "33047500",
|
||||
"name": "vastbase",
|
||||
"font_class": "vastbase",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.7.2',
|
||||
version: 'v1.7.4',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -7,24 +7,22 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
for (let column of columns) {
|
||||
let val: any = data[column];
|
||||
if (val == null || val == undefined) {
|
||||
dataValueArr.push('');
|
||||
continue;
|
||||
}
|
||||
val = '';
|
||||
} else if (val && typeof val == 'string') {
|
||||
// 替换换行符
|
||||
val = val.replace(/[\r\n]/g, '\\n');
|
||||
|
||||
if (typeof val == 'string' && val) {
|
||||
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
||||
if (val.indexOf(',') != -1) {
|
||||
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
||||
if (val.indexOf('"') != -1) {
|
||||
val = val.replace(/\"/g, '""');
|
||||
val = val.replace(/"/g, '""');
|
||||
}
|
||||
// 再将逗号转义
|
||||
val = `"${val}"`;
|
||||
}
|
||||
dataValueArr.push(val + '\t');
|
||||
} else {
|
||||
dataValueArr.push(val + '\t');
|
||||
}
|
||||
dataValueArr.push(String(val));
|
||||
}
|
||||
cvsData.push(dataValueArr);
|
||||
}
|
||||
|
||||
@@ -46,60 +46,6 @@ export function convertToBytes(sizeStr: string) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化json字符串
|
||||
* @param txt json字符串
|
||||
* @param compress 是否压缩
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export function formatJsonString(txt: string, compress: boolean) {
|
||||
var indentChar = ' ';
|
||||
if (/^\s*$/.test(txt)) {
|
||||
console.log('数据为空,无法格式化! ');
|
||||
return txt;
|
||||
}
|
||||
try {
|
||||
var data = JSON.parse(txt);
|
||||
} catch (e: any) {
|
||||
console.log('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err');
|
||||
return txt;
|
||||
}
|
||||
var draw: any = [],
|
||||
line = compress ? '' : '\n',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
nodeCount: number = 0,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
maxDepth: number = 0;
|
||||
|
||||
var notify = function (name: any, value: any, isLast: any, indent: any, formObj: any) {
|
||||
nodeCount++; /*节点计数*/
|
||||
for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
|
||||
tab = compress ? '' : tab; /*压缩模式忽略缩进*/
|
||||
maxDepth = ++indent; /*缩进递增并记录*/
|
||||
if (value && value.constructor == Array) {
|
||||
/*处理数组*/
|
||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
|
||||
for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
|
||||
draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
|
||||
} else if (value && typeof value == 'object') {
|
||||
/*处理对象*/
|
||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
|
||||
var len = 0,
|
||||
i = 0;
|
||||
for (var key in value) len++;
|
||||
for (var key in value) notify(key, value[key], ++i == len, indent, true);
|
||||
draw.push(tab + '}' + (isLast ? line : ',' + line)); /*缩进'}'换行,若非尾元素则添加逗号*/
|
||||
} else {
|
||||
if (typeof value == 'string') value = '"' + value + '"';
|
||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + value + (isLast ? '' : ',') + line);
|
||||
}
|
||||
};
|
||||
var isLast = true,
|
||||
indent = 0;
|
||||
notify('', data, isLast, indent, false);
|
||||
return draw.join('');
|
||||
}
|
||||
|
||||
/*
|
||||
* 年(Y) 可用1-4个占位符
|
||||
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
|
||||
@@ -204,6 +150,45 @@ export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
||||
*
|
||||
* @param time 时间数
|
||||
* @param unit time对应的单位
|
||||
* @returns
|
||||
*/
|
||||
export function formatTime(time: number, unit: string = 's') {
|
||||
const units = {
|
||||
y: 31536000,
|
||||
M: 2592000,
|
||||
d: 86400,
|
||||
h: 3600,
|
||||
m: 60,
|
||||
s: 1,
|
||||
};
|
||||
|
||||
if (!units[unit]) {
|
||||
return 'Invalid unit';
|
||||
}
|
||||
|
||||
let seconds = time * units[unit];
|
||||
let result = '';
|
||||
|
||||
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||
const value = Math.floor(seconds / duration);
|
||||
seconds %= duration;
|
||||
return { value, unit };
|
||||
});
|
||||
|
||||
timeUnits.forEach(({ value, unit }) => {
|
||||
if (value > 0) {
|
||||
result += `${value}${unit} `;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatAxis(new Date()) // 上午好
|
||||
*/
|
||||
|
||||
27
mayfly_go_web/src/common/utils/object.ts
Normal file
27
mayfly_go_web/src/common/utils/object.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 根据对象访问路径,获取对应的值
|
||||
export function getValueByPath(obj: any, path: string) {
|
||||
const keys = path.split('.');
|
||||
let result = obj;
|
||||
for (let key of keys) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// 处理包含数组索引的情况
|
||||
const arrayKey = key.substring(0, key.indexOf('['));
|
||||
const matchIndex = key.match(/\[(.*?)\]/);
|
||||
|
||||
if (!matchIndex) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const index = parseInt(matchIndex[1]);
|
||||
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
|
||||
} else {
|
||||
result = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-page-header @back="props.back">
|
||||
<template #content>
|
||||
<span>{{ header }}</span>
|
||||
<span v-if="resource && !hideResource">
|
||||
-
|
||||
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
|
||||
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
|
||||
</span>
|
||||
<el-divider v-if="slots.buttons" direction="vertical" />
|
||||
<slot v-if="slots.buttons" name="buttons"></slot>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot v-if="slots.extra" name="extra"></slot>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSlots } from 'vue';
|
||||
const slots = useSlots();
|
||||
|
||||
defineOptions({ name: 'DrawerHeader' });
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
back: Function,
|
||||
resource: String,
|
||||
hideResource: Boolean,
|
||||
});
|
||||
</script>
|
||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
editorRef.value?.format();
|
||||
editorRef.value?.focus();
|
||||
editorRef.value?.format();
|
||||
}, 300);
|
||||
|
||||
state.dialogVisible = true;
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
trigger="click"
|
||||
>
|
||||
<div v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
|
||||
<el-checkbox v-model="item.show" :label="item.label" :true-value="true" :false-value="false" />
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||
@@ -115,18 +115,18 @@
|
||||
>
|
||||
<!-- 插槽:预留功能 -->
|
||||
<template #default="scope" v-if="item.slot">
|
||||
<slot :name="item.prop" :data="scope.row"></slot>
|
||||
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||
</template>
|
||||
|
||||
<!-- 枚举类型使用tab展示 -->
|
||||
<template #default="scope" v-else-if="item.type == 'tag'">
|
||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
|
||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else>
|
||||
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
||||
<el-popover
|
||||
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
|
||||
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||
effect="light"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
@@ -137,7 +137,7 @@
|
||||
</template>
|
||||
<template #reference>
|
||||
<el-link
|
||||
@click="formatText(scope.row[item.prop])"
|
||||
@click="formatText(item.getValueByData(scope.row))"
|
||||
:underline="false"
|
||||
type="success"
|
||||
icon="MagicStick"
|
||||
@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
pageApi: Api; // 请求表格数据的 api
|
||||
pageApi?: Api; // 请求表格数据的 api
|
||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||
showSelection?: boolean;
|
||||
selectable?: (row: any) => boolean; // 是否可选
|
||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
||||
nowSearchItem.value = searchItem;
|
||||
};
|
||||
|
||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
props.pageable,
|
||||
props.pageApi,
|
||||
queryForm,
|
||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
tableData = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
calcuTableHeight();
|
||||
useEventListener(window, 'resize', calcuTableHeight);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { getValueByPath } from '@/common/utils/object';
|
||||
import { getTextWidth } from '@/common/utils/string';
|
||||
|
||||
export class TableColumn {
|
||||
@@ -29,10 +30,15 @@ export class TableColumn {
|
||||
minWidth: number | string;
|
||||
|
||||
/**
|
||||
* 是否插槽,是的话插槽名则为prop属性名
|
||||
* 是否为插槽,若slotName为空则插槽名为prop属性名
|
||||
*/
|
||||
slot: boolean = false;
|
||||
|
||||
/**
|
||||
* 插槽名,
|
||||
*/
|
||||
slotName: string = '';
|
||||
|
||||
showOverflowTooltip: boolean = true;
|
||||
|
||||
sortable: boolean = false;
|
||||
@@ -87,7 +93,7 @@ export class TableColumn {
|
||||
if (this.formatFunc) {
|
||||
return this.formatFunc(rowData, this.prop);
|
||||
}
|
||||
return rowData[this.prop];
|
||||
return getValueByPath(rowData, this.prop);
|
||||
}
|
||||
|
||||
static new(prop: string, label: string): TableColumn {
|
||||
@@ -144,8 +150,9 @@ export class TableColumn {
|
||||
* 标识该列为插槽
|
||||
* @returns this
|
||||
*/
|
||||
isSlot(): TableColumn {
|
||||
isSlot(slotName: string = ''): TableColumn {
|
||||
this.slot = true;
|
||||
this.slotName = slotName;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -165,7 +172,7 @@ export class TableColumn {
|
||||
*/
|
||||
isTime(): TableColumn {
|
||||
this.setFormatFunc((data: any, prop: string) => {
|
||||
return dateFormat(data[prop]);
|
||||
return dateFormat(getValueByPath(data, prop));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
@@ -176,7 +183,7 @@ export class TableColumn {
|
||||
*/
|
||||
isEnum(enums: any): TableColumn {
|
||||
this.setFormatFunc((data: any, prop: string) => {
|
||||
return EnumValue.getLabelByValue(enums, data[prop]);
|
||||
return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
@@ -218,7 +225,7 @@ export class TableColumn {
|
||||
// 获取该列中最长的数据(内容)
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let nowData = tableData[i];
|
||||
let nowValue = nowData[prop];
|
||||
let nowValue = getValueByPath(nowData, prop);
|
||||
if (!nowValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
|
||||
<div id="terminal-body" :style="{ height }">
|
||||
<div ref="terminalRef" class="terminal" />
|
||||
|
||||
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal, ITheme } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
@@ -20,8 +20,14 @@ import TerminalSearch from './TerminalSearch.vue';
|
||||
import { debounce } from 'lodash';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import themes from './themes';
|
||||
|
||||
const props = defineProps({
|
||||
// mounted时,是否执行init方法
|
||||
mountInit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 初始化执行命令
|
||||
*/
|
||||
@@ -64,9 +70,9 @@ const state = reactive({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props.mountInit) {
|
||||
init();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -76,6 +82,14 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 themeConfig terminalTheme配置的变化
|
||||
watch(
|
||||
() => themeConfig.value.terminalTheme,
|
||||
() => {
|
||||
term.options.theme = getTerminalTheme();
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
close();
|
||||
});
|
||||
@@ -85,6 +99,13 @@ function init() {
|
||||
console.log('重新连接...');
|
||||
close();
|
||||
}
|
||||
nextTick(() => {
|
||||
initTerm();
|
||||
initSocket();
|
||||
});
|
||||
}
|
||||
|
||||
function initTerm() {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -92,13 +113,10 @@ function init() {
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
allowProposedApi: true,
|
||||
theme: {
|
||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||
// cursorAccent: "red", // 光标停止颜色
|
||||
} as any,
|
||||
fastScrollModifier: 'ctrl',
|
||||
theme: getTerminalTheme(),
|
||||
});
|
||||
|
||||
term.open(terminalRef.value);
|
||||
|
||||
// 注册自适应组件
|
||||
@@ -117,21 +135,6 @@ function init() {
|
||||
state.addon.weblinks = weblinks;
|
||||
term.loadAddon(weblinks);
|
||||
|
||||
// 初始化websocket
|
||||
initSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接成功
|
||||
*/
|
||||
const onConnected = () => {
|
||||
// 注册心跳
|
||||
pingInterval = setInterval(sendPing, 15000);
|
||||
|
||||
// 注册 terminal 事件
|
||||
term.onResize((event) => sendResize(event.cols, event.rows));
|
||||
term.onData((event) => sendCmd(event));
|
||||
|
||||
// 注册自定义快捷键
|
||||
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||
// 注册搜索键 ctrl + f
|
||||
@@ -142,50 +145,31 @@ const onConnected = () => {
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
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 {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
||||
<div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
||||
<TerminalBody
|
||||
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
||||
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||
@@ -259,6 +259,10 @@ defineExpose({
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
padding: 1px 1px;
|
||||
}
|
||||
|
||||
// 取消body最大高度,否则全屏有问题
|
||||
.el-dialog__body {
|
||||
max-height: 100% !important;
|
||||
|
||||
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
@@ -0,0 +1,92 @@
|
||||
export default {
|
||||
dark: {
|
||||
foreground: '#c7c7c7',
|
||||
background: '#000000',
|
||||
cursor: '#c7c7c7',
|
||||
selectionBackground: '#686868',
|
||||
|
||||
black: '#000000',
|
||||
brightBlack: '#676767',
|
||||
|
||||
red: '#c91b00',
|
||||
brightRed: '#ff6d67',
|
||||
|
||||
green: '#00c200',
|
||||
brightGreen: '#5ff967',
|
||||
|
||||
yellow: '#c7c400',
|
||||
brightYellow: '#fefb67',
|
||||
|
||||
blue: '#0225c7',
|
||||
brightBlue: '#6871ff',
|
||||
|
||||
magenta: '#c930c7',
|
||||
brightMagenta: '#ff76ff',
|
||||
|
||||
cyan: '#00c5c7',
|
||||
brightCyan: '#5ffdff',
|
||||
|
||||
white: '#c7c7c7',
|
||||
brightWhite: '#fffefe',
|
||||
},
|
||||
light: {
|
||||
foreground: '#000000',
|
||||
background: '#fffefe',
|
||||
cursor: '#000000',
|
||||
selectionBackground: '#c7c7c7',
|
||||
|
||||
black: '#000000',
|
||||
brightBlack: '#676767',
|
||||
|
||||
red: '#c91b00',
|
||||
brightRed: '#ff6d67',
|
||||
|
||||
green: '#00c200',
|
||||
brightGreen: '#5ff967',
|
||||
|
||||
yellow: '#c7c400',
|
||||
brightYellow: '#fefb67',
|
||||
|
||||
blue: '#0225c7',
|
||||
brightBlue: '#6871ff',
|
||||
|
||||
magenta: '#c930c7',
|
||||
brightMagenta: '#ff76ff',
|
||||
|
||||
cyan: '#00c5c7',
|
||||
brightCyan: '#5ffdff',
|
||||
|
||||
white: '#c7c7c7',
|
||||
brightWhite: '#fffefe',
|
||||
},
|
||||
solarizedLight: {
|
||||
foreground: '#657b83',
|
||||
background: '#fdf6e3',
|
||||
cursor: '#657b83',
|
||||
selectionBackground: '#c7c7c7',
|
||||
|
||||
black: '#073642',
|
||||
brightBlack: '#002b36',
|
||||
|
||||
red: '#dc322f',
|
||||
brightRed: '#cb4b16',
|
||||
|
||||
green: '#859900',
|
||||
brightGreen: '#586e75',
|
||||
|
||||
yellow: '#b58900',
|
||||
brightYellow: '#657b83',
|
||||
|
||||
blue: '#268bd2',
|
||||
brightBlue: '#839496',
|
||||
|
||||
magenta: '#d33682',
|
||||
brightMagenta: '#6c71c4',
|
||||
|
||||
cyan: '#2aa198',
|
||||
brightCyan: '#93a1a1',
|
||||
|
||||
white: '#eee8d5',
|
||||
brightWhite: '#fdf6e3',
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import Api from '@/common/Api';
|
||||
import { isReactive, reactive, toRefs, toValue } from 'vue';
|
||||
import { reactive, toRefs, toValue } from 'vue';
|
||||
|
||||
/**
|
||||
* @description table 页面操作方法封装
|
||||
@@ -41,12 +41,6 @@ export const usePageTable = (
|
||||
let sp = toValue(state.searchParams);
|
||||
if (beforeQueryFn) {
|
||||
sp = beforeQueryFn(sp);
|
||||
|
||||
if (isReactive(state.searchParams)) {
|
||||
state.searchParams.value = sp;
|
||||
} else {
|
||||
state.searchParams = sp;
|
||||
}
|
||||
}
|
||||
|
||||
let res = await api.request(sp);
|
||||
|
||||
@@ -48,9 +48,6 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
}
|
||||
|
||||
let paramsValue = unref(params);
|
||||
if (api.beforeHandler) {
|
||||
paramsValue = api.beforeHandler(paramsValue);
|
||||
}
|
||||
|
||||
let apiUrl = url;
|
||||
// 简单判断该url是否是restful风格
|
||||
@@ -58,6 +55,10 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
apiUrl = templateResolve(apiUrl, paramsValue);
|
||||
}
|
||||
|
||||
if (api.beforeHandler) {
|
||||
paramsValue = api.beforeHandler(paramsValue);
|
||||
}
|
||||
|
||||
if (paramsValue) {
|
||||
const method = options.method?.toLowerCase();
|
||||
// post和put使用json格式传参
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<template>
|
||||
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
|
||||
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'"
|
||||
@click="onThemeConfigChange" />
|
||||
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
|
||||
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
|
||||
<transition-group name="breadcrumb" mode="out-in">
|
||||
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
|
||||
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
|
||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
||||
v-if="themeConfig.isBreadcrumbIcon" />
|
||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||
{{ v.meta.title }}
|
||||
</span>
|
||||
<a v-else @click.prevent="onBreadcrumbClick(v)">
|
||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
||||
v-if="themeConfig.isBreadcrumbIcon" />
|
||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||
{{ v.meta.title }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
Normal file
20
mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { saveThemeConfig } from '@/common/utils/storage';
|
||||
import { isDark } from './user.vue';
|
||||
|
||||
export const switchDark = () => {
|
||||
themeConfig.value.isDark = isDark.value;
|
||||
if (isDark.value) {
|
||||
themeConfig.value.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
themeConfig.value.editorTheme = 'vs';
|
||||
}
|
||||
// 如果终端主题不是自定义主题,则切换主题
|
||||
if (themeConfig.value.terminalTheme != 'custom') {
|
||||
if (isDark.value) {
|
||||
themeConfig.value.terminalTheme = 'dark';
|
||||
} else {
|
||||
themeConfig.value.terminalTheme = 'solarizedLight';
|
||||
}
|
||||
}
|
||||
saveThemeConfig(themeConfig.value);
|
||||
};
|
||||
@@ -174,12 +174,7 @@ watch(preDark, (newValue) => {
|
||||
});
|
||||
|
||||
const switchDark = () => {
|
||||
themeConfig.value.isDark = isDark.value;
|
||||
if (isDark.value) {
|
||||
themeConfig.value.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
themeConfig.value.editorTheme = 'vs';
|
||||
}
|
||||
themeConfigStore.switchDark(isDark.value);
|
||||
saveThemeConfig(themeConfig.value);
|
||||
};
|
||||
|
||||
|
||||
@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
|
||||
state.tagsRefsIndex = k;
|
||||
try {
|
||||
router.push(v);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
// skip
|
||||
}
|
||||
};
|
||||
// 更新滚动条显示
|
||||
const updateScrollbar = () => {
|
||||
|
||||
@@ -114,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
||||
layout: 'classic',
|
||||
|
||||
terminalTheme: 'solarizedLight',
|
||||
// ssh终端字体颜色
|
||||
terminalForeground: '#C5C8C6',
|
||||
// ssh终端背景色
|
||||
@@ -192,5 +193,23 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
setWatermarkNowTime() {
|
||||
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
||||
},
|
||||
// 切换暗黑模式
|
||||
switchDark(isDark: boolean) {
|
||||
this.themeConfig.isDark = isDark;
|
||||
// 切换编辑器主题
|
||||
if (isDark) {
|
||||
this.themeConfig.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
this.themeConfig.editorTheme = 'vs';
|
||||
}
|
||||
// 如果终端主题不是自定义主题,则切换主题
|
||||
if (this.themeConfig.terminalTheme != 'custom') {
|
||||
if (isDark) {
|
||||
this.themeConfig.terminalTheme = 'dark';
|
||||
} else {
|
||||
this.themeConfig.terminalTheme = 'solarizedLight';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ body,
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
font-weight: 450;
|
||||
font-weight: 500;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background-color: var(--bg-main-color);
|
||||
|
||||
@@ -7,335 +7,353 @@
|
||||
------------------------------- */
|
||||
// 菜单搜索
|
||||
.el-autocomplete-suggestion__wrap {
|
||||
max-height: 280px !important;
|
||||
max-height: 280px !important;
|
||||
}
|
||||
|
||||
/* Form 表单
|
||||
------------------------------- */
|
||||
.el-form {
|
||||
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
||||
.el-form-item:last-of-type {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
// 修复行内表单最后一个 el-form-item 位置下移问题
|
||||
&.el-form--inline {
|
||||
.el-form-item--large.el-form-item:last-of-type {
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
.el-form-item--default.el-form-item:last-of-type,
|
||||
.el-form-item--small.el-form-item:last-of-type {
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
}
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
||||
.el-form-item .el-form-item__label .el-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
||||
.el-form-item:last-of-type {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// 修复行内表单最后一个 el-form-item 位置下移问题
|
||||
&.el-form--inline {
|
||||
.el-form-item--large.el-form-item:last-of-type {
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
|
||||
.el-form-item--default.el-form-item:last-of-type,
|
||||
.el-form-item--small.el-form-item:last-of-type {
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
||||
.el-form-item .el-form-item__label .el-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alert 警告
|
||||
------------------------------- */
|
||||
.el-alert {
|
||||
border: 1px solid;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.el-alert__title {
|
||||
word-break: break-all;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Message 消息提示
|
||||
------------------------------- */
|
||||
.el-message {
|
||||
min-width: unset !important;
|
||||
padding: 15px !important;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
||||
min-width: unset !important;
|
||||
padding: 15px !important;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* NavMenu 导航菜单
|
||||
------------------------------- */
|
||||
// 鼠标 hover 时颜色
|
||||
.el-menu-hover-bg-color {
|
||||
background-color: var(--bg-menuBarActiveColor) !important;
|
||||
background-color: var(--bg-menuBarActiveColor) !important;
|
||||
}
|
||||
|
||||
// 默认样式修改
|
||||
.el-menu {
|
||||
border-right: none !important;
|
||||
width: 220px;
|
||||
border-right: none !important;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
height: 56px !important;
|
||||
line-height: 56px !important;
|
||||
height: 56px !important;
|
||||
line-height: 56px !important;
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--bg-menuBarColor);
|
||||
color: var(--bg-menuBarColor);
|
||||
}
|
||||
|
||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
||||
.el-menu--collapse {
|
||||
width: 64px !important;
|
||||
width: 64px !important;
|
||||
}
|
||||
|
||||
// 外部链接时
|
||||
.el-menu-item a,
|
||||
.el-menu-item a:hover,
|
||||
.el-menu-item i,
|
||||
.el-sub-menu__title i {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// 第三方图标字体间距/大小设置
|
||||
.el-menu-item .iconfont,
|
||||
.el-sub-menu .iconfont,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
@include generalIcon;
|
||||
@include generalIcon;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title,
|
||||
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
@extend .el-menu-hover-bg-color;
|
||||
}
|
||||
|
||||
.el-menu-item:hover {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
@extend .el-menu-hover-bg-color;
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
// 子级菜单背景颜色
|
||||
// .el-menu--inline {
|
||||
// background: var(--next-bg-menuBar-light-1);
|
||||
// }
|
||||
// 水平菜单、横向菜单折叠 a 标签
|
||||
.el-popper.is-dark a {
|
||||
color: var(--el-color-white) !important;
|
||||
text-decoration: none;
|
||||
color: var(--el-color-white) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单折叠背景色
|
||||
.el-popper.is-pure.is-light {
|
||||
// 水平菜单
|
||||
.el-menu--vertical {
|
||||
background: var(--bg-menuBar);
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--vertical {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-menuBarColor);
|
||||
}
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 横向菜单
|
||||
.el-menu--horizontal {
|
||||
background: var(--bg-topBar);
|
||||
.el-menu-item,
|
||||
.el-sub-menu {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--horizontal {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 水平菜单
|
||||
.el-menu--vertical {
|
||||
background: var(--bg-menuBar);
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--vertical {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-menuBarColor);
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 横向菜单
|
||||
.el-menu--horizontal {
|
||||
background: var(--bg-topBar);
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--horizontal {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 横向菜单(经典、横向)布局
|
||||
.el-menu.el-menu--horizontal {
|
||||
border-bottom: none !important;
|
||||
width: 100% !important;
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
.el-menu-item:not(.is-active):hover,
|
||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
border-bottom: none !important;
|
||||
width: 100% !important;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
|
||||
.el-menu-item:not(.is-active):hover,
|
||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单收起时,图标不居中问题
|
||||
.el-menu--collapse {
|
||||
.el-menu-item .iconfont,
|
||||
.el-sub-menu .iconfont,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.el-menu-item .iconfont,
|
||||
.el-sub-menu .iconfont,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tabs 标签页
|
||||
------------------------------- */
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px !important;
|
||||
height: 1px !important;
|
||||
}
|
||||
|
||||
/* Dropdown 下拉菜单
|
||||
------------------------------- */
|
||||
.el-dropdown-menu {
|
||||
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
||||
}
|
||||
.el-dropdown-menu .el-dropdown-menu__item {
|
||||
white-space: nowrap;
|
||||
&:not(.is-disabled):hover {
|
||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||
color: var(--el-dropdown-menuItem-hover-color);
|
||||
}
|
||||
list-style: none !important;
|
||||
/*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
||||
}
|
||||
|
||||
/* Steps 步骤条
|
||||
------------------------------- */
|
||||
.el-step__icon-inner {
|
||||
font-size: 30px !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
.el-step__title {
|
||||
font-size: 14px;
|
||||
.el-dropdown-menu .el-dropdown-menu__item {
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(.is-disabled):hover {
|
||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||
color: var(--el-dropdown-menuItem-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog 对话框
|
||||
------------------------------- */
|
||||
.el-overlay {
|
||||
overflow: hidden;
|
||||
.el-overlay-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: unset !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-dialog {
|
||||
margin: 0 auto !important;
|
||||
position: absolute;
|
||||
.el-dialog__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
|
||||
.el-overlay-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: unset !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.el-dialog {
|
||||
margin: 0 auto !important;
|
||||
position: absolute;
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
max-height: calc(90vh - 111px) !important;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: calc(90vh - 111px) !important;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Card 卡片
|
||||
------------------------------- */
|
||||
.el-card__header {
|
||||
padding: 15px 20px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
/* Table 表格 element plus 2.2.0 版本
|
||||
------------------------------- */
|
||||
.el-table {
|
||||
.el-button.is-text {
|
||||
padding: 0;
|
||||
}
|
||||
.el-button.is-text {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* scrollbar
|
||||
------------------------------- */
|
||||
.el-scrollbar__bar {
|
||||
z-index: 4;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
|
||||
.el-scrollbar__wrap {
|
||||
max-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.el-select-dropdown .el-scrollbar__wrap {
|
||||
overflow-x: scroll !important;
|
||||
overflow-x: scroll !important;
|
||||
}
|
||||
|
||||
/*修复Select 选择器高度问题*/
|
||||
.el-select-dropdown__wrap {
|
||||
max-height: 274px !important;
|
||||
max-height: 274px !important;
|
||||
}
|
||||
|
||||
/*修复Cascader 级联选择器高度问题*/
|
||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
|
||||
height: 204px !important;
|
||||
height: 204px !important;
|
||||
}
|
||||
|
||||
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
|
||||
.layout-container-view .el-scrollbar__view {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
|
||||
.layout-columns-warp .layout-aside .el-scrollbar__view {
|
||||
height: unset !important;
|
||||
height: unset !important;
|
||||
}
|
||||
|
||||
/* Pagination 分页
|
||||
------------------------------- */
|
||||
.el-pagination__editor {
|
||||
margin-right: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/*深色模式时分页高亮问题*/
|
||||
.el-pagination.is-background .btn-next.is-active,
|
||||
.el-pagination.is-background .btn-prev.is-active,
|
||||
.el-pagination.is-background .el-pager li.is-active {
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
|
||||
/* Drawer 抽屉
|
||||
------------------------------- */
|
||||
.el-drawer {
|
||||
--el-drawer-padding-primary: unset !important;
|
||||
.el-drawer__header {
|
||||
padding: 0 15px !important;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0 !important;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.el-drawer__body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb 面包屑
|
||||
------------------------------- */
|
||||
.el-breadcrumb__inner a:hover,
|
||||
.el-breadcrumb__inner.is-link:hover {
|
||||
color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-breadcrumb__inner a,
|
||||
.el-breadcrumb__inner.is-link {
|
||||
color: var(--bg-topBarColor);
|
||||
font-weight: normal;
|
||||
color: var(--bg-topBarColor);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
// el-tooltip使用自定义主题时的样式
|
||||
.el-popper.is-customized {
|
||||
/* Set padding to ensure the height is 32px */
|
||||
// padding: 6px 12px;
|
||||
// padding: 6px 12px;
|
||||
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
|
||||
}
|
||||
|
||||
.el-popper.is-customized .el-popper__arrow::before {
|
||||
background: linear-gradient(45deg, #b2e68d, #bce689);
|
||||
right: 0;
|
||||
@@ -343,7 +361,9 @@
|
||||
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 6px; /* 设置圆角 */
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
|
||||
border-radius: 6px;
|
||||
/* 设置圆角 */
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
/* 添加轻微阴影效果 */
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
1
mayfly_go_web/src/types/pinia.d.ts
vendored
1
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -52,6 +52,7 @@ declare interface ThemeConfigState {
|
||||
logoIcon: string;
|
||||
globalI18n: string;
|
||||
globalComponentSize: string;
|
||||
terminalTheme: string;
|
||||
terminalForeground: string;
|
||||
terminalBackground: string;
|
||||
terminalCursor: string;
|
||||
|
||||
@@ -98,4 +98,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@/router/staticRouter
|
||||
|
||||
211
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
211
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model.trim="form.name" placeholder="请输入流程名称" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="defKey" label="key">
|
||||
<el-input :disabled="form.id" v-model.trim="form.defKey" placeholder="请输入流程key" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
|
||||
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="名称" min-width="100px">
|
||||
<template #header>
|
||||
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addTask()"> </el-button>
|
||||
<span class="ml10">节点名称</span>
|
||||
<el-tooltip content="点击指定节点可进行拖拽排序" placement="top">
|
||||
<el-icon class="ml5">
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.name"> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userId" label="审核人员" min-width="150px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<AccountSelectFormItem v-model="scope.row.userId" label="" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60px">
|
||||
<template #default="scope">
|
||||
<el-link @click="deleteTask(scope.$index)" class="ml5" type="danger" icon="delete" plain></el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, ref, nextTick } from 'vue';
|
||||
import { procdefApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import { randomUuid } from '../../common/utils/string';
|
||||
import { ProcdefStatus } from './enums';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
const formRef: any = ref(null);
|
||||
const taskTableRef: any = ref(null);
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入流程名称',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
defKey: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入流程key',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
tasks: [] as any,
|
||||
form: {
|
||||
id: null,
|
||||
name: null,
|
||||
defKey: null,
|
||||
status: null,
|
||||
remark: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
},
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { form, tasks } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
const tasks = JSON.parse(state.form.tasks);
|
||||
tasks.forEach((t: any) => {
|
||||
t.userId = Number.parseInt(t.userId);
|
||||
});
|
||||
state.tasks = tasks;
|
||||
} else {
|
||||
state.form = { status: ProcdefStatus.Enable.value } as any;
|
||||
state.tasks = [];
|
||||
}
|
||||
});
|
||||
|
||||
const initSort = () => {
|
||||
nextTick(() => {
|
||||
const table = taskTableRef.value.$el.querySelector('table > tbody') as any;
|
||||
state.sortable = Sortable.create(table, {
|
||||
animation: 200,
|
||||
//拖拽结束事件
|
||||
onEnd: (evt) => {
|
||||
const curRow = state.tasks.splice(evt.oldIndex, 1)[0];
|
||||
state.tasks.splice(evt.newIndex, 0, curRow);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addTask = () => {
|
||||
state.tasks.push({ taskKey: randomUuid() });
|
||||
};
|
||||
|
||||
const deleteTask = (idx: any) => {
|
||||
state.tasks.splice(idx, 1);
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('表单填写有误');
|
||||
return false;
|
||||
}
|
||||
const checkRes = checkTasks();
|
||||
if (checkRes.err) {
|
||||
ElMessage.error(checkRes.err);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.form.tasks = JSON.stringify(checkRes.tasks);
|
||||
await saveFlowDefExec();
|
||||
ElMessage.success('操作成功');
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
state.form = {} as any;
|
||||
});
|
||||
};
|
||||
|
||||
const checkTasks = () => {
|
||||
if (state.tasks?.length == 0) {
|
||||
return { err: '请完善审批节点任务' };
|
||||
}
|
||||
|
||||
const tasks = [];
|
||||
for (let i = 0; i < state.tasks.length; i++) {
|
||||
const task = { ...state.tasks[i] };
|
||||
if (!task.name || !task.userId) {
|
||||
return { err: `请完善第${i + 1}个审批节点任务信息` };
|
||||
}
|
||||
// 转为字符串(方便后续万一需要调整啥的)
|
||||
task.userId = `${task.userId}`;
|
||||
if (!task.taskKey) {
|
||||
task.taskKey = randomUuid();
|
||||
}
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
return { tasks: tasks };
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
141
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
141
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="procdefApi.list"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.save" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button>
|
||||
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="deleteProcdef()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #tasks="{ data }">
|
||||
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog v-model="flowTasksDialog.visible" :title="flowTasksDialog.title">
|
||||
<procdef-tasks :tasks="flowTasksDialog.tasks" />
|
||||
</el-dialog>
|
||||
|
||||
<procdef-edit v-model:visible="flowDefEditor.visible" :title="flowDefEditor.title" v-model:data="flowDefEditor.data" @val-change="valChange()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import { procdefApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import ProcdefEdit from './ProcdefEdit.vue';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import { ProcdefStatus } from './enums';
|
||||
|
||||
const perms = {
|
||||
save: 'flow:procdef:save',
|
||||
del: 'flow:procdef:del',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', '名称'), SearchItem.input('defKey', 'key')];
|
||||
const columns = [
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('defKey', 'key'),
|
||||
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
|
||||
TableColumn.new('creator', '创建账号'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.save, perms.del]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
flowDefEditor: {
|
||||
title: '新建流程定义',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
flowTasksDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
tasks: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, flowDefEditor, flowTasksDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showProcdefTasks = (procdef: any) => {
|
||||
state.flowTasksDialog.tasks = procdef.tasks;
|
||||
state.flowTasksDialog.title = procdef.name + '-审批节点';
|
||||
state.flowTasksDialog.visible = true;
|
||||
};
|
||||
|
||||
const editFlowDef = (data: any) => {
|
||||
if (!data) {
|
||||
state.flowDefEditor.data = null;
|
||||
state.flowDefEditor.title = '新建流程定义';
|
||||
} else {
|
||||
state.flowDefEditor.data = data;
|
||||
state.flowDefEditor.title = '编辑流程定义';
|
||||
}
|
||||
state.flowDefEditor.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
state.flowDefEditor.visible = false;
|
||||
search();
|
||||
};
|
||||
|
||||
const deleteProcdef = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】的流程定义?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
171
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
171
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :close-on-click-modal="!props.instTaskId">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">流程信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务">
|
||||
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="发起人">
|
||||
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||
<!-- {{ procinst.creator }} -->
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
|
||||
|
||||
<div v-if="procinst.duration">
|
||||
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
|
||||
</div>
|
||||
|
||||
<el-descriptions-item label="流程状态">
|
||||
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务状态">
|
||||
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="备注">
|
||||
{{ procinst.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :tasks="procinst?.procdef?.tasks" :procinst-tasks="procinst.procinstTasks" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">业务信息</el-divider>
|
||||
<component
|
||||
v-if="procinst.bizType"
|
||||
ref="keyValueRef"
|
||||
:is="bizComponents[procinst.bizType]"
|
||||
:biz-key="procinst.bizKey"
|
||||
:biz-form="procinst.bizForm"
|
||||
>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<div v-if="props.instTaskId">
|
||||
<el-divider content-position="left">审批表单</el-divider>
|
||||
<el-form :model="form" label-width="auto">
|
||||
<el-form-item prop="status" label="结果" required>
|
||||
<el-select v-model="form.status" placeholder="请选择审批结果">
|
||||
<el-option :label="ProcinstTaskStatus.Pass.label" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
||||
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
|
||||
<el-option :label="ProcinstTaskStatus.Reject.label" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" placeholder="备注" type="textarea" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer v-if="props.instTaskId">
|
||||
<div>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||
|
||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
|
||||
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
procinstId: {
|
||||
type: Number,
|
||||
},
|
||||
// 流程实例任务id(存在则展示审批相关信息)
|
||||
instTaskId: {
|
||||
type: Number,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
// 业务组件
|
||||
const bizComponents = shallowReactive({
|
||||
db_sql_exec_flow: DbSqlExecBiz,
|
||||
redis_run_write_cmd_flow: RedisRunWriteCmdBiz,
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
procinst: {} as any,
|
||||
tasks: [] as any,
|
||||
form: {
|
||||
status: ProcinstTaskStatus.Pass.value,
|
||||
remark: '',
|
||||
},
|
||||
saveBtnLoading: false,
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { procinst, form, saveBtnLoading } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.procinstId,
|
||||
async (newValue: any) => {
|
||||
if (newValue) {
|
||||
state.procinst = await procinstApi.detail.request({ id: newValue });
|
||||
} else {
|
||||
state.procinst = {};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const btnOk = async () => {
|
||||
const status = state.form.status;
|
||||
let api = procinstApi.completeTask;
|
||||
if (status === ProcinstTaskStatus.Back.value) {
|
||||
api = procinstApi.backTask;
|
||||
} else if (status === ProcinstTaskStatus.Reject.value) {
|
||||
api = procinstApi.rejectTask;
|
||||
}
|
||||
|
||||
try {
|
||||
state.saveBtnLoading = true;
|
||||
await api.request({ id: props.instTaskId, remark: state.form.remark });
|
||||
ElMessage.success('操作成功');
|
||||
cancel();
|
||||
emit('val-change');
|
||||
} finally {
|
||||
state.saveBtnLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
120
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
120
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="procinstApi.list"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="showProcinst(data)" type="primary">查看</el-button>
|
||||
|
||||
<el-popconfirm
|
||||
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
|
||||
title="确认取消该流程?"
|
||||
width="160"
|
||||
@confirm="procinstCancel(data)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button link type="warning">取消</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<ProcinstDetail
|
||||
v-model:visible="procinstDetail.visible"
|
||||
:title="procinstDetail.title"
|
||||
:procinst-id="procinstDetail.procinstId"
|
||||
:inst-task-id="procinstDetail.instTaskId"
|
||||
@val-change="valChange()"
|
||||
@cancel="procinstDetail.procinstId = 0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
|
||||
const searchItems = [SearchItem.select('status', '流程状态').withEnum(ProcinstStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
|
||||
const columns = [
|
||||
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('creator', '发起人'),
|
||||
TableColumn.new('procdefName', '流程名'),
|
||||
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
|
||||
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
|
||||
TableColumn.new('createTime', '发起时间').isTime(),
|
||||
TableColumn.new('endTime', '结束时间').isTime(),
|
||||
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||
const duration = data[prop];
|
||||
if (!duration) {
|
||||
return '';
|
||||
}
|
||||
return formatTime(duration);
|
||||
}),
|
||||
TableColumn.new('bizHandleRes', '业务处理结果'),
|
||||
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||
];
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
status: null,
|
||||
bizType: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
procinstDetail: {
|
||||
title: '查看流程',
|
||||
visible: false,
|
||||
procinstId: 0,
|
||||
instTaskId: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const procinstCancel = async (data: any) => {
|
||||
await procinstApi.cancel.request({ id: data.id });
|
||||
ElMessage.success('操作成功');
|
||||
search();
|
||||
};
|
||||
|
||||
const showProcinst = (data: any) => {
|
||||
state.procinstDetail.procinstId = data.id;
|
||||
state.procinstDetail.title = '流程查看';
|
||||
state.procinstDetail.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
state.procinstDetail.visible = false;
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
110
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
110
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="procinstApi.tasks"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="showProcinst(data, false)" type="primary">查看</el-button>
|
||||
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="showProcinst(data, true)" type="primary">审核</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<ProcinstDetail
|
||||
v-model:visible="procinstDetail.visible"
|
||||
:title="procinstDetail.title"
|
||||
:procinst-id="procinstDetail.procinstId"
|
||||
:inst-task-id="procinstDetail.instTaskId"
|
||||
@val-change="valChange()"
|
||||
@cancel="procinstDetail.procinstId = 0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
|
||||
const searchItems = [SearchItem.select('status', '任务状态').withEnum(ProcinstTaskStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
|
||||
const columns = [
|
||||
TableColumn.new('procinst.bizType', '业务').typeTag(FlowBizType),
|
||||
TableColumn.new('procinst.remark', '备注'),
|
||||
TableColumn.new('procinst.creator', '发起人'),
|
||||
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
|
||||
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
|
||||
TableColumn.new('procinst.procdefName', '流程名'),
|
||||
TableColumn.new('taskName', '当前节点'),
|
||||
TableColumn.new('procinst.createTime', '发起时间').isTime(),
|
||||
TableColumn.new('createTime', '开始时间').isTime(),
|
||||
TableColumn.new('endTime', '结束时间').isTime(),
|
||||
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||
const duration = data[prop];
|
||||
if (!duration) {
|
||||
return '';
|
||||
}
|
||||
return formatTime(duration);
|
||||
}),
|
||||
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||
];
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
status: ProcinstTaskStatus.Process.value,
|
||||
bizType: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
procinstDetail: {
|
||||
title: '查看流程',
|
||||
visible: false,
|
||||
procinstId: 0,
|
||||
instTaskId: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showProcinst = (data: any, audit: boolean) => {
|
||||
state.procinstDetail.procinstId = data.procinstId;
|
||||
if (!audit) {
|
||||
state.procinstDetail.instTaskId = 0;
|
||||
state.procinstDetail.title = '流程查看';
|
||||
} else {
|
||||
state.procinstDetail.instTaskId = data.id;
|
||||
state.procinstDetail.title = '流程审批';
|
||||
}
|
||||
state.procinstDetail.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
state.procinstDetail.visible = false;
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
20
mayfly_go_web/src/views/flow/api.ts
Normal file
20
mayfly_go_web/src/views/flow/api.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const procdefApi = {
|
||||
list: Api.newGet('/flow/procdefs'),
|
||||
getByKey: Api.newGet('/flow/procdefs/{key}'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||
};
|
||||
|
||||
export const procinstApi = {
|
||||
list: Api.newGet('/flow/procinsts'),
|
||||
detail: Api.newGet('/flow/procinsts/{id}'),
|
||||
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
|
||||
tasks: Api.newGet('/flow/procinsts/tasks'),
|
||||
completeTask: Api.newPost('/flow/procinsts/tasks/complete'),
|
||||
backTask: Api.newPost('/flow/procinsts/tasks/back'),
|
||||
rejectTask: Api.newPost('/flow/procinsts/tasks/reject'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-form-item :label="props.label">
|
||||
<el-select style="width: 100%" v-model="procdefKey" filterable placeholder="绑定流程则开启对应审批流程" v-bind="$attrs" clearable>
|
||||
<el-option v-for="item in procdefs" :key="item.defKey" :label="`${item.defKey} [${item.name}]`" :value="item.defKey"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { procdefApi } from '../api';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '工单流程',
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getProcdefs();
|
||||
});
|
||||
|
||||
const procdefKey = defineModel('modelValue');
|
||||
|
||||
const procdefs: any = ref([]);
|
||||
|
||||
const getProcdefs = () => {
|
||||
procdefApi.list.request({ pageSize: 200 }).then((res) => {
|
||||
procdefs.value = res.list;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
136
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
136
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<el-steps align-center :active="stepActive">
|
||||
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
||||
<template #description>
|
||||
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
||||
<div v-if="task.completeTime">{{ `${dateFormat(task.completeTime)}` }}</div>
|
||||
<div v-if="task.remark">{{ task.remark }}</div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import { accountApi } from '../../system/api';
|
||||
import { ProcinstTaskStatus } from '../enums';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { procdefApi } from '../api';
|
||||
import { ElSteps, ElStep } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
// 流程定义任务
|
||||
tasks: {
|
||||
type: [String, Object],
|
||||
},
|
||||
procdefKey: {
|
||||
type: String,
|
||||
},
|
||||
// 流程实例任务列表
|
||||
procinstTasks: {
|
||||
type: [Array],
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tasksArr: [] as any,
|
||||
stepActive: 0,
|
||||
});
|
||||
|
||||
const { tasksArr, stepActive } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tasks,
|
||||
(newValue: any) => {
|
||||
parseTasks(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.procinstTasks,
|
||||
() => {
|
||||
parseTasks(props.tasks);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.procdefKey,
|
||||
async (newValue: any) => {
|
||||
if (newValue) {
|
||||
parseTasksByKey(newValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.procdefKey) {
|
||||
parseTasksByKey(props.procdefKey);
|
||||
return;
|
||||
}
|
||||
parseTasks(props.tasks);
|
||||
});
|
||||
|
||||
const parseTasksByKey = async (key: string) => {
|
||||
const procdef = await procdefApi.getByKey.request({ key });
|
||||
parseTasks(procdef.tasks);
|
||||
};
|
||||
|
||||
const parseTasks = async (tasksStr: any) => {
|
||||
if (!tasksStr) return;
|
||||
const tasks = JSON.parse(tasksStr);
|
||||
const userIds = tasks.map((x: any) => x.userId);
|
||||
const usersRes = await accountApi.querySimple.request({ ids: [...new Set(userIds)].join(','), pageSize: 50 });
|
||||
const users = usersRes.list;
|
||||
// 将数组转换为 Map 结构,以 id 为 key
|
||||
const userMap = users.reduce((acc: any, obj: any) => {
|
||||
acc.set(obj.id, obj);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
// 流程实例任务(用于显示完成时间,完成到哪一步等)
|
||||
let instTasksMap: any;
|
||||
if (props.procinstTasks) {
|
||||
state.stepActive = props.procinstTasks.length - 1;
|
||||
instTasksMap = props.procinstTasks.reduce((acc: any, obj: any) => {
|
||||
acc.set(obj.taskKey, obj);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
for (let task of tasks) {
|
||||
const user = userMap.get(Number.parseInt(task.userId));
|
||||
task.accountUsername = user.username;
|
||||
task.accountName = user.name;
|
||||
|
||||
// 存在实例任务,则赋值实例任务对应的完成时间和备注
|
||||
const instTask = instTasksMap?.get(task.taskKey);
|
||||
if (instTask) {
|
||||
task.status = instTask.status;
|
||||
task.completeTime = instTask.endTime;
|
||||
task.remark = instTask.remark;
|
||||
}
|
||||
}
|
||||
|
||||
state.tasksArr = tasks;
|
||||
};
|
||||
|
||||
const getStepStatus = (task: any): any => {
|
||||
const taskStatus = task.status;
|
||||
if (!taskStatus) {
|
||||
return 'wait';
|
||||
}
|
||||
|
||||
if (taskStatus == ProcinstTaskStatus.Pass.value) {
|
||||
return 'success';
|
||||
}
|
||||
if (taskStatus == ProcinstTaskStatus.Process.value) {
|
||||
return 'proccess';
|
||||
}
|
||||
if (taskStatus == ProcinstTaskStatus.Back.value || taskStatus == ProcinstTaskStatus.Reject.value) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'wait';
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const ProcdefStatus = {
|
||||
Enable: EnumValue.of(1, '启用').setTagType('success'),
|
||||
Disable: EnumValue.of(-1, '禁用').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const ProcinstStatus = {
|
||||
Active: EnumValue.of(1, '执行中').setTagType('primary'),
|
||||
Completed: EnumValue.of(2, '完成').setTagType('success'),
|
||||
Suspended: EnumValue.of(-1, '挂起').setTagType('warning'),
|
||||
Terminated: EnumValue.of(-2, '终止').setTagType('danger'),
|
||||
Cancelled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const ProcinstBizStatus = {
|
||||
Wait: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||
Success: EnumValue.of(2, '处理成功').setTagType('success'),
|
||||
Fail: EnumValue.of(-2, '处理失败').setTagType('danger'),
|
||||
No: EnumValue.of(-1, '不处理').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const ProcinstTaskStatus = {
|
||||
Process: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||
Pass: EnumValue.of(2, '通过').setTagType('success'),
|
||||
Reject: EnumValue.of(-1, '拒绝').setTagType('danger'),
|
||||
Back: EnumValue.of(-2, '驳回').setTagType('warning'),
|
||||
Canceled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const FlowBizType = {
|
||||
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL'),
|
||||
RedisRunWriteCmd: EnumValue.of('redis_run_write_cmd_flow', 'Redis-执行write命令'),
|
||||
};
|
||||
79
mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
Executable file
79
mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
Executable file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="类型">
|
||||
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
|
||||
<el-descriptions-item label="表">
|
||||
{{ sqlExec.table }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行SQL">
|
||||
<monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务key
|
||||
bizKey: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
sqlExec: {
|
||||
sql: '',
|
||||
} as any,
|
||||
db: {} as any,
|
||||
});
|
||||
|
||||
const { sqlExec, db } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
getDbSqlExec(props.bizKey);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.bizKey,
|
||||
(newValue: any) => {
|
||||
getDbSqlExec(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const getDbSqlExec = async (bizKey: string) => {
|
||||
if (!bizKey) {
|
||||
return;
|
||||
}
|
||||
const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
|
||||
if (!res.list) {
|
||||
return;
|
||||
}
|
||||
state.sqlExec = res.list?.[0];
|
||||
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
|
||||
state.db = dbRes.list?.[0];
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
80
mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
Executable file
80
mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
Executable file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="mode">
|
||||
{{ redis.mode }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="执行Cmd">
|
||||
<el-input type="textarea" disabled v-model="cmd" rows="5" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务表单
|
||||
bizForm: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
cmd: '',
|
||||
db: 0,
|
||||
redis: {} as any,
|
||||
});
|
||||
|
||||
const { cmd, redis } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
parseRunCmdForm(props.bizForm);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.bizForm,
|
||||
(newValue: any) => {
|
||||
parseRunCmdForm(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const parseRunCmdForm = async (bizForm: string) => {
|
||||
if (!bizForm) {
|
||||
return;
|
||||
}
|
||||
const form = JSON.parse(bizForm);
|
||||
|
||||
const cmds = form.cmd.map((item: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return item; // 第一个元素直接返回原值
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return `'${item}'`; // 字符串加单引号
|
||||
}
|
||||
return item; // 其他类型直接返回
|
||||
});
|
||||
state.cmd = cmds.join(' ');
|
||||
state.db = form.db;
|
||||
|
||||
const res = await redisApi.redisList.request({ id: form.id });
|
||||
if (!res.list) {
|
||||
return;
|
||||
}
|
||||
state.redis = res.list?.[0];
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -109,7 +109,7 @@ const toPage = (item: any) => {
|
||||
break;
|
||||
}
|
||||
case 'machineNum': {
|
||||
router.push('/machine/machines');
|
||||
router.push('/machine/machines-op');
|
||||
break;
|
||||
}
|
||||
case 'dbNum': {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
|
||||
<template #reference>
|
||||
<div>
|
||||
<!-- <el-button type="primary" link size="small">标签</el-button> -->
|
||||
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [] as any,
|
||||
});
|
||||
|
||||
const { tags } = toRefs(state);
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
33
mayfly_go_web/src/views/ops/component/ResourceTags.vue
Normal file
33
mayfly_go_web/src/views/ops/component/ResourceTags.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div v-if="props.tags">
|
||||
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
|
||||
<TagInfo :tag-path="tag.tagPath" />
|
||||
<span class="ml3">{{ tag.tagPath }}</span>
|
||||
|
||||
<!-- 展示剩余的标签信息 -->
|
||||
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
|
||||
<template #reference>
|
||||
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
|
||||
</template>
|
||||
|
||||
<el-row v-for="i in props.tags.slice(1)" :key="i">
|
||||
<TagInfo :tag-path="i.tagPath" />
|
||||
<span class="ml3">{{ i.tagPath }}</span>
|
||||
</el-row>
|
||||
</el-popover>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
const props = defineProps({
|
||||
tags: {
|
||||
type: [Array<any>],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -17,7 +17,7 @@
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
@@ -25,7 +25,13 @@
|
||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||
|
||||
<span class="ml3" :title="data.labelRemark">
|
||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
|
||||
<!-- 禁用状态 -->
|
||||
<slot name="disabledLabel" :data="data" v-else>
|
||||
<el-link type="danger" disabled :underline="false">
|
||||
{{ `${data.label}` }}
|
||||
</el-link>
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<slot :node="node" :data="data" name="suffix"></slot>
|
||||
@@ -135,15 +141,29 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
emit('nodeClick', data);
|
||||
if (data.type.nodeClickFunc) {
|
||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||
data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点双击事件
|
||||
const treeNodeDblclick = (data: any) => {
|
||||
// emit('nodeDblick', data);
|
||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||
data.type.nodeDblclickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
if (data.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
v-model="modelValue"
|
||||
@change="changeNode"
|
||||
>
|
||||
<template #prefix="{ node, data }">
|
||||
<slot name="iconPrefix" :node="node" :data="data" />
|
||||
</template>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
@@ -33,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div>
|
||||
<el-tree-select
|
||||
v-bind="$attrs"
|
||||
v-model="selectTags"
|
||||
v-model="state.selectTags"
|
||||
@change="changeTag"
|
||||
style="width: 100%"
|
||||
:data="tags"
|
||||
placeholder="请选择关联标签"
|
||||
:render-after-expand="true"
|
||||
:default-expanded-keys="[selectTags]"
|
||||
:default-expanded-keys="[state.selectTags]"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{
|
||||
@@ -40,32 +40,22 @@ import { tagApi } from '../tag/api';
|
||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
selectTags: {
|
||||
type: [Array<any>],
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
// 单选则为id,多选为id数组
|
||||
selectTags: [],
|
||||
selectTags: [] as any,
|
||||
});
|
||||
|
||||
const { tags, selectTags } = toRefs(state);
|
||||
const { tags } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.resourceCode) {
|
||||
const resourceTags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
state.selectTags = resourceTags.map((x: any) => x.tagId);
|
||||
changeTag();
|
||||
if (props.selectTags) {
|
||||
state.selectTags = props.selectTags;
|
||||
}
|
||||
|
||||
state.tags = await tagApi.getTagTrees.request(null);
|
||||
|
||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
|
||||
*/
|
||||
isLeaf: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否禁用状态
|
||||
*/
|
||||
disabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 额外需要传递的参数
|
||||
*/
|
||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
withDisabled(disabled: boolean) {
|
||||
this.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
withParams(params: any) {
|
||||
this.params = params;
|
||||
return this;
|
||||
@@ -91,8 +101,14 @@ export class NodeType {
|
||||
|
||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||
|
||||
/**
|
||||
* 节点点击事件
|
||||
*/
|
||||
nodeClickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
// 节点双击事件
|
||||
nodeDblclickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
@@ -117,6 +133,16 @@ export class NodeType {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值节点双击事件回调函数
|
||||
* @param func 节点双击事件回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
|
||||
this.nodeDblclickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值右击菜单按钮选项
|
||||
* @param contextMenuItems 右击菜单按钮选项
|
||||
|
||||
@@ -28,8 +28,11 @@
|
||||
<el-form-item prop="startTime" label="开始时间">
|
||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="intervalDay" label="备份周期">
|
||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天)"></el-input>
|
||||
<el-form-item prop="intervalDay" label="备份周期(天)">
|
||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
||||
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -92,6 +95,14 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
maxSaveDays: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^[0-9]\d*$/,
|
||||
message: '请输入非负整数',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const backupForm: any = ref(null);
|
||||
@@ -102,9 +113,10 @@ const state = reactive({
|
||||
dbId: 0,
|
||||
dbNames: '',
|
||||
name: '',
|
||||
intervalDay: null,
|
||||
intervalDay: 1,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
repeated: true,
|
||||
maxSaveDays: 0,
|
||||
},
|
||||
btnLoading: false,
|
||||
dbNamesSelected: [] as any,
|
||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
|
||||
state.form.name = data.name;
|
||||
state.form.intervalDay = data.intervalDay;
|
||||
state.form.startTime = data.startTime;
|
||||
state.form.maxSaveDays = data.maxSaveDays;
|
||||
} else {
|
||||
state.editOrCreate = false;
|
||||
state.form.name = '';
|
||||
state.form.intervalDay = null;
|
||||
state.form.intervalDay = 1;
|
||||
const now = new Date();
|
||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
state.form.maxSaveDays = 0;
|
||||
getDbNamesWithoutBackup();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
:select-tags="form.tagId"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -75,6 +74,8 @@
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<procdef-select-form-item v-model="form.flowProcdefKey" />
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -92,8 +93,9 @@ import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import type { CheckboxValueType } from 'element-plus';
|
||||
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
|
||||
import { DbType } from '@/views/ops/db/dialect';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -163,6 +165,7 @@ const state = reactive({
|
||||
database: '',
|
||||
remark: '',
|
||||
instanceId: null as any,
|
||||
flowProcdefKey: '',
|
||||
},
|
||||
instances: [] as any,
|
||||
});
|
||||
@@ -178,7 +181,7 @@ watch(props, async (newValue: any) => {
|
||||
}
|
||||
if (newValue.db) {
|
||||
state.form = { ...newValue.db };
|
||||
|
||||
state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
|
||||
// 将数据库名使用空格切割,获取所有数据库列表
|
||||
state.dbNamesSelected = newValue.db.database.split(' ');
|
||||
} else {
|
||||
@@ -194,7 +197,14 @@ const changeInstance = () => {
|
||||
|
||||
const getAllDatabase = async () => {
|
||||
if (state.form.instanceId > 0) {
|
||||
state.allDatabases = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
|
||||
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
|
||||
state.allDatabases = dbs;
|
||||
|
||||
// 如果是oracle,且没查出数据库列表,则取实例sid
|
||||
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
|
||||
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
|
||||
state.allDatabases = [instance.sid];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -83,7 +83,7 @@
|
||||
</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="导出内容: ">
|
||||
@@ -164,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>
|
||||
|
||||
@@ -196,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';
|
||||
@@ -210,6 +218,7 @@ 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'));
|
||||
|
||||
@@ -224,12 +233,13 @@ const perms = {
|
||||
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', '备注'),
|
||||
]);
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-link
|
||||
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
|
||||
v-if="
|
||||
data.status == DbSqlExecStatusEnum.Success.value &&
|
||||
(data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value)
|
||||
"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
@@ -38,7 +41,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
@@ -66,9 +69,11 @@ const columns = ref([
|
||||
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
|
||||
TableColumn.new('creator', '执行人'),
|
||||
TableColumn.new('sql', 'SQL').canBeautify(),
|
||||
TableColumn.new('oldValue', '原值').canBeautify(),
|
||||
TableColumn.new('createTime', '执行时间').isTime(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('status', '执行状态').typeTag(DbSqlExecStatusEnum),
|
||||
TableColumn.new('res', '执行结果'),
|
||||
TableColumn.new('createTime', '执行时间').isTime(),
|
||||
TableColumn.new('oldValue', '原值').canBeautify(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
@@ -80,6 +85,7 @@ const state = reactive({
|
||||
dbId: 0,
|
||||
db: '',
|
||||
table: '',
|
||||
status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
|
||||
type: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -61,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, Ref } from 'vue';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dbApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
@@ -91,7 +91,7 @@ const columns = ref([
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveInstance]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<el-descriptions-item label-align="right">
|
||||
<template #label>
|
||||
<div>
|
||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
|
||||
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||
实例
|
||||
</div>
|
||||
</template>
|
||||
@@ -143,6 +143,7 @@
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:flow-procdef-key="dt.params.flowProcdefKey"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
@@ -157,6 +158,7 @@
|
||||
: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"
|
||||
@@ -180,6 +182,7 @@ 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'));
|
||||
@@ -225,7 +228,18 @@ const SqlIcon = {
|
||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
if (params.db) {
|
||||
changeDb({ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.dbs }, params.db);
|
||||
changeDb(
|
||||
{
|
||||
id: params.id,
|
||||
host: `${params.host}`,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
tagPath: params.tagPath,
|
||||
databases: params.dbs,
|
||||
flowProcdefKey: params.flowProcdefKey,
|
||||
},
|
||||
params.db
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,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);
|
||||
});
|
||||
@@ -322,7 +337,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db, type } = params;
|
||||
let { id, db, type, flowProcdefKey } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
@@ -337,6 +352,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
id,
|
||||
db,
|
||||
type,
|
||||
flowProcdefKey: flowProcdefKey,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
@@ -374,6 +390,7 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
|
||||
new ContextmenuItem('renameTable', '重命名').withIcon('edit').withOnClick((data: any) => onRenameTable(data)),
|
||||
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
|
||||
])
|
||||
@@ -417,6 +434,7 @@ const state = reactive({
|
||||
dbId: 0,
|
||||
db: '',
|
||||
dbType: '',
|
||||
flowProcdefKey: '',
|
||||
data: {},
|
||||
parentKey: '',
|
||||
},
|
||||
@@ -639,7 +657,7 @@ const reloadNode = (nodeKey: string) => {
|
||||
};
|
||||
|
||||
const onEditTable = async (data: any) => {
|
||||
let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
|
||||
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
|
||||
// data.label就是表名
|
||||
if (tableName) {
|
||||
state.tableCreateDialog.title = '修改表';
|
||||
@@ -654,22 +672,28 @@ const onEditTable = async (data: any) => {
|
||||
state.tableCreateDialog.parentKey = key;
|
||||
}
|
||||
|
||||
state.tableCreateDialog.visible = true;
|
||||
state.tableCreateDialog.activeName = '1';
|
||||
state.tableCreateDialog.dbId = id;
|
||||
state.tableCreateDialog.db = db;
|
||||
state.tableCreateDialog.dbType = type;
|
||||
state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
|
||||
state.tableCreateDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
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 ${tableName}` }).then(() => {
|
||||
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);
|
||||
@@ -677,6 +701,39 @@ const onDeleteTable = async (data: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -728,6 +785,7 @@ const getNowDbInfo = () => {
|
||||
name: di.name,
|
||||
type: di.type,
|
||||
host: di.host,
|
||||
flowProcdefKey: di.flowProcdefKey,
|
||||
dbName: state.db,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<el-row>
|
||||
<el-col :span="11">
|
||||
<el-form-item prop="taskName" label="任务名" required>
|
||||
<el-input v-model.trim="form.taskName" placeholder="请输入数据库别名" auto-complete="off" />
|
||||
<el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<db-select-tree
|
||||
placeholder="请选择源数据库"
|
||||
v-model:db-id="form.srcDbId"
|
||||
v-model:inst-name="form.srcInstName"
|
||||
v-model:db-name="form.srcDbName"
|
||||
v-model:tag-path="form.srcTagPath"
|
||||
v-model:db-type="form.srcDbType"
|
||||
@@ -56,8 +57,10 @@
|
||||
<db-select-tree
|
||||
placeholder="请选择目标数据库"
|
||||
v-model:db-id="form.targetDbId"
|
||||
v-model:inst-name="form.targetInstName"
|
||||
v-model:db-name="form.targetDbName"
|
||||
v-model:tag-path="form.targetTagPath"
|
||||
v-model:db-type="form.targetDbType"
|
||||
@select-db="onSelectTargetDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -86,9 +89,11 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="updField" label="更新字段" required>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||
<el-form-item prop="updField" label="更新字段" required>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
@@ -122,10 +127,17 @@
|
||||
|
||||
<el-tab-pane label="sql预览" :name="sqlPreviewTab" :disabled="!baseFieldCompleted">
|
||||
<el-form-item prop="fieldMap" label="查询sql">
|
||||
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '190px' }" />
|
||||
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '170px' }" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="fieldMap" label="插入sql">
|
||||
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '190px' }" />
|
||||
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '170px' }" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="isReplace" v-if="compatibleDuplicateStrategy(form.targetDbType!)" label="键冲突策略">
|
||||
<el-select v-model="form.duplicateStrategy" @change="handleDuplicateStrategy" style="width: 100px">
|
||||
<el-option label="无" :value="DuplicateStrategy.NONE" />
|
||||
<el-option label="忽略" :value="DuplicateStrategy.IGNORE" />
|
||||
<el-option label="替换" :value="DuplicateStrategy.REPLACE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -182,7 +194,7 @@ import { ElMessage } from 'element-plus';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
import {DbType, getDbDialect} from '@/views/ops/db/dialect'
|
||||
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -227,19 +239,23 @@ type FormData = {
|
||||
taskName?: string;
|
||||
taskCron: string;
|
||||
srcDbId?: number;
|
||||
srcInstName?: string;
|
||||
srcDbName?: string;
|
||||
srcDbType?: string;
|
||||
srcTagPath?: string;
|
||||
targetDbId?: number;
|
||||
targetInstName?: string;
|
||||
targetDbName?: string;
|
||||
targetTagPath?: string;
|
||||
targetTableName?: string;
|
||||
targetDbType?: string;
|
||||
dataSql?: string;
|
||||
pageSize?: number;
|
||||
updField?: string;
|
||||
updFieldVal?: string;
|
||||
fieldMap?: { src: string; target: string }[];
|
||||
status?: 1 | 2;
|
||||
duplicateStrategy?: -1 | 1 | 2;
|
||||
};
|
||||
|
||||
const basicFormData = {
|
||||
@@ -251,6 +267,7 @@ const basicFormData = {
|
||||
updFieldVal: '0',
|
||||
fieldMap: [{ src: 'a', target: 'b' }],
|
||||
status: 1,
|
||||
duplicateStrategy: -1,
|
||||
} as FormData;
|
||||
|
||||
const state = reactive({
|
||||
@@ -265,6 +282,7 @@ const state = reactive({
|
||||
previewRes: {} as any,
|
||||
previewDataSql: '',
|
||||
previewInsertSql: '',
|
||||
previewFieldArr: [] as string[],
|
||||
});
|
||||
|
||||
const { tabActiveName, form, submitForm } = toRefs(state);
|
||||
@@ -289,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) {
|
||||
@@ -304,7 +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.srcDbType = state.srcDbInst.type;
|
||||
state.form.srcInstName = db.instanceName;
|
||||
}
|
||||
|
||||
// 初始化target数据源
|
||||
@@ -315,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) {
|
||||
@@ -334,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();
|
||||
@@ -356,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; // 数据源里需要这个值
|
||||
@@ -413,16 +438,24 @@ const handleGetSrcFields = async () => {
|
||||
}
|
||||
|
||||
// 执行sql
|
||||
// oracle的分页关键字不一样
|
||||
let limit = ' limit 1'
|
||||
if(state.form.srcDbType === DbType.oracle){
|
||||
limit = ' where rownum <= 1'
|
||||
let sql: string;
|
||||
|
||||
if (state.form.srcDbType === DbType.mssql) {
|
||||
// mssql的分页语法不一样
|
||||
let top1 = `select top 1`;
|
||||
sql = `${top1} * from (${state.form.dataSql}) a`;
|
||||
} else if (state.form.srcDbType === DbType.oracle) {
|
||||
// oracle的分页关键字不一样
|
||||
let hasCondition = /where/i.test(state.form.dataSql!);
|
||||
sql = `${state.form.dataSql} ${hasCondition ? 'and' : 'where'} rownum <= 1`;
|
||||
} else {
|
||||
sql = `${state.form.dataSql} limit 1`;
|
||||
}
|
||||
|
||||
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: state.form.srcDbId,
|
||||
db: state.form.srcDbName,
|
||||
sql: `select * from (${state.form.dataSql}) t ${limit}`
|
||||
sql,
|
||||
});
|
||||
|
||||
if (!res.columns) {
|
||||
@@ -496,6 +529,11 @@ const btnOk = async () => {
|
||||
const cancel = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('cancel');
|
||||
state.form = basicFormData;
|
||||
};
|
||||
|
||||
const handleDuplicateStrategy = () => {
|
||||
refreshPreviewInsertSql();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
@@ -35,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}'),
|
||||
@@ -84,3 +84,8 @@ export const dbApi = {
|
||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||
};
|
||||
|
||||
export const dbSqlExecApi = {
|
||||
// 根据业务key获取sql执行信息
|
||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
>
|
||||
<template #iconPrefix>
|
||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||
</template>
|
||||
<template #prefix="{ data }">
|
||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||
@@ -27,6 +30,9 @@ const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
},
|
||||
instName: {
|
||||
type: String,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
},
|
||||
@@ -38,7 +44,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
@@ -56,7 +62,7 @@ class SqlExecNodeType {
|
||||
|
||||
const selectNode = computed({
|
||||
get: () => {
|
||||
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
|
||||
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||
},
|
||||
set: () => {
|
||||
//
|
||||
@@ -151,6 +157,7 @@ const changeNode = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
// postgres
|
||||
emits('update:dbName', params.db);
|
||||
emits('update:instName', params.name);
|
||||
emits('update:dbId', params.id);
|
||||
emits('update:tagPath', params.tagPath);
|
||||
emits('update:dbType', params.type);
|
||||
|
||||
@@ -148,7 +148,6 @@ import { buildProgressProps } from '@/components/progress-notify/progress-notify
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from '../../dialect';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess']);
|
||||
@@ -298,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: '确定',
|
||||
@@ -321,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;
|
||||
@@ -453,7 +467,7 @@ const formatSql = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
|
||||
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { h, render, VNode } from 'vue';
|
||||
import { h, render } from 'vue';
|
||||
import SqlExecDialog from './SqlExecDialog.vue';
|
||||
|
||||
export type SqlExecProps = {
|
||||
@@ -6,31 +6,26 @@ export type SqlExecProps = {
|
||||
dbId: number;
|
||||
db: string;
|
||||
dbType?: string;
|
||||
flowProcdefKey?: string;
|
||||
runSuccessCallback?: Function;
|
||||
cancelCallback?: Function;
|
||||
};
|
||||
|
||||
const boxId = 'sql-exec-dialog-id';
|
||||
|
||||
let boxInstance: VNode;
|
||||
|
||||
const SqlExecBox = (props: SqlExecProps): void => {
|
||||
if (!boxInstance) {
|
||||
const container = document.createElement('div');
|
||||
container.id = boxId;
|
||||
// 创建 虚拟dom
|
||||
boxInstance = h(SqlExecDialog);
|
||||
// 将虚拟dom渲染到 container dom 上
|
||||
render(boxInstance, container);
|
||||
// 最后将 container 追加到 body 上
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const boxVue = boxInstance.component;
|
||||
if (boxVue) {
|
||||
// 调用open方法显示弹框,注意不能使用boxVue.ctx来调用组件函数(build打包后ctx会获取不到)
|
||||
boxVue.exposed?.open(props);
|
||||
}
|
||||
const propsCancelFn = props.cancelCallback;
|
||||
// 包装取消回调函数,新增销毁组件代码
|
||||
props.cancelCallback = () => {
|
||||
propsCancelFn && propsCancelFn();
|
||||
setTimeout(() => {
|
||||
// 销毁组件
|
||||
render(null, document.body);
|
||||
}, 500);
|
||||
};
|
||||
const vnode = h(SqlExecDialog, {
|
||||
...props,
|
||||
visible: true,
|
||||
});
|
||||
render(vnode, document.body);
|
||||
};
|
||||
|
||||
export default SqlExecBox;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
||||
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
|
||||
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
|
||||
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
|
||||
<div v-if="props.flowProcdefKey">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :procdef-key="props.flowProcdefKey" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
@@ -14,47 +20,34 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, nextTick, reactive } from 'vue';
|
||||
import { toRefs, ref, reactive, onMounted } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
|
||||
import { SqlExecProps } from './SqlExecBox';
|
||||
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
dbId: {
|
||||
type: [Number],
|
||||
},
|
||||
db: {
|
||||
type: String,
|
||||
},
|
||||
sql: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const props = withDefaults(defineProps<SqlExecProps>(), {});
|
||||
|
||||
const remarkInputRef = ref<InputInstance>();
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
sqlValue: '',
|
||||
dbId: 0,
|
||||
db: '',
|
||||
remark: '',
|
||||
btnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
|
||||
|
||||
state.sqlValue = props.sql as any;
|
||||
let runSuccessCallback: any;
|
||||
let cancelCallback: any;
|
||||
let runSuccess: boolean = false;
|
||||
|
||||
onMounted(() => {
|
||||
open();
|
||||
});
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
@@ -67,12 +60,19 @@ const runSql = async () => {
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: state.dbId,
|
||||
db: state.db,
|
||||
id: props.dbId,
|
||||
db: props.db,
|
||||
remark: state.remark,
|
||||
sql: state.sqlValue.trim(),
|
||||
});
|
||||
|
||||
// 存在流程审批
|
||||
if (props.flowProcdefKey) {
|
||||
runSuccess = false;
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let re of res.res) {
|
||||
if (re.result !== 'success') {
|
||||
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
|
||||
@@ -84,45 +84,33 @@ const runSql = async () => {
|
||||
ElMessage.success('执行成功');
|
||||
} catch (e) {
|
||||
runSuccess = false;
|
||||
}
|
||||
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 });
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
: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"
|
||||
@@ -20,7 +19,6 @@
|
||||
:disabled="disabled"
|
||||
@blur="handleBlur"
|
||||
class="w100 mb4"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model.number="itemValue"
|
||||
:placeholder="placeholder"
|
||||
@@ -185,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
|
||||
.el-input__prefix {
|
||||
display: none;
|
||||
}
|
||||
.el-input__inner {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-time-picker-popper {
|
||||
|
||||
@@ -137,13 +137,25 @@
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
|
||||
<DbTableDataForm
|
||||
v-if="state.tableDataFormDialog.visible"
|
||||
:db-inst="getNowDbInst()"
|
||||
:db-name="db"
|
||||
:columns="columns!"
|
||||
:title="state.tableDataFormDialog.title"
|
||||
:table-name="table"
|
||||
v-model:visible="state.tableDataFormDialog.visible"
|
||||
v-model="state.tableDataFormDialog.data"
|
||||
@submit-success="emits('changeUpdatedField')"
|
||||
/>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
@@ -153,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
|
||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||
|
||||
@@ -246,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||
.withIcon('edit')
|
||||
.withOnClick(() => onEditRowData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
.withOnClick(() => onGenerateInsertSql())
|
||||
@@ -332,7 +352,11 @@ const state = reactive({
|
||||
},
|
||||
items: [] as ContextmenuItem[],
|
||||
},
|
||||
|
||||
tableDataFormDialog: {
|
||||
data: {},
|
||||
title: '',
|
||||
visible: false,
|
||||
},
|
||||
genTxtDialog: {
|
||||
title: 'SQL',
|
||||
visible: false,
|
||||
@@ -443,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);
|
||||
@@ -575,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 });
|
||||
};
|
||||
|
||||
@@ -600,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);
|
||||
@@ -713,52 +749,28 @@ const submitUpdateFields = async () => {
|
||||
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
const dbDialect = getDbDialect(dbInst.type);
|
||||
let schema = '';
|
||||
let dbArr = db.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
schema = dbInst.wrapName(dbArr[1]) + '.';
|
||||
}
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
let sql = `UPDATE ${schema}${dbInst.wrapName(state.table)} SET `;
|
||||
const rowData = updateRow.rowData;
|
||||
// 主键列信息
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
let primaryKeyType = primaryKey.columnType;
|
||||
let primaryKeyName = primaryKey.columnName;
|
||||
let primaryKeyValue = rowData[primaryKeyName];
|
||||
const rowData = { ...updateRow.rowData };
|
||||
let updateColumnValue = {};
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
||||
|
||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
|
||||
|
||||
// 如果修改的字段是主键
|
||||
if (k === primaryKeyName) {
|
||||
primaryKeyValue = v.oldValue;
|
||||
}
|
||||
updateColumnValue[k] = rowData[k];
|
||||
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
|
||||
rowData[k] = v.oldValue;
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
||||
res += sql;
|
||||
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(
|
||||
db,
|
||||
res,
|
||||
() => {},
|
||||
() => {
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
}
|
||||
);
|
||||
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
});
|
||||
};
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
|
||||
<el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
|
||||
<el-form-item
|
||||
v-for="column in columns"
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<template #label>
|
||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||
{{ column.columnName }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<ColumnFormItem
|
||||
v-model="modelValue[`${column.columnName}`]"
|
||||
:data-type="dbInst.getDialect().getDataType(column.columnType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import { DbInst } from '../../db';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
|
||||
export interface ColumnFormItemProps {
|
||||
dbInst: DbInst;
|
||||
dbName: string;
|
||||
tableName: string;
|
||||
columns: any[];
|
||||
title?: string; // dialog title
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||
title: '',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<any>('modelValue');
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submitSuccess']);
|
||||
|
||||
const dataForm: any = ref(null);
|
||||
|
||||
let oldValue = null as any;
|
||||
|
||||
onMounted(() => {
|
||||
setOldValue();
|
||||
});
|
||||
|
||||
watch(visible, (newValue) => {
|
||||
if (newValue) {
|
||||
setOldValue();
|
||||
}
|
||||
});
|
||||
|
||||
const setOldValue = () => {
|
||||
// 空对象则为insert操作,否则为update
|
||||
if (Object.keys(modelValue.value).length > 0) {
|
||||
oldValue = Object.assign({}, modelValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
visible.value = false;
|
||||
modelValue.value = {};
|
||||
};
|
||||
|
||||
const confirm = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写数据信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbInst = props.dbInst;
|
||||
const data = modelValue.value;
|
||||
const db = props.dbName;
|
||||
const tableName = props.tableName;
|
||||
|
||||
let sql = '';
|
||||
if (oldValue) {
|
||||
const updateColumnValue = {};
|
||||
Object.keys(oldValue).forEach((key) => {
|
||||
// 如果新旧值不相等,则为需要更新的字段
|
||||
if (oldValue[key] !== modelValue.value[key]) {
|
||||
updateColumnValue[key] = modelValue.value[key];
|
||||
}
|
||||
});
|
||||
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
|
||||
} else {
|
||||
sql = await dbInst.genInsertSql(db, tableName, [data], true);
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(db, sql, null, () => {
|
||||
closeDialog();
|
||||
emit('submitSuccess');
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -17,8 +17,8 @@
|
||||
<el-checkbox
|
||||
v-model="item.show"
|
||||
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
||||
:true-label="true"
|
||||
:false-label="false"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
title="展示配置"
|
||||
trigger="click"
|
||||
>
|
||||
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-label="true" :false-label="false" size="small" />
|
||||
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
</template>
|
||||
@@ -234,32 +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.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<ColumnFormItem
|
||||
v-model="addDataDialog.data[`${column.columnName}`]"
|
||||
:data-type="dbDialect.getDataType(column.columnType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeAddDataDialog">取消</el-button>
|
||||
<el-button type="primary" @click="addRow">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<DbTableDataForm
|
||||
:db-inst="getNowDbInst()"
|
||||
:db-name="dbName"
|
||||
:columns="columns"
|
||||
:title="addDataDialog.title"
|
||||
:table-name="tableName"
|
||||
v-model:visible="addDataDialog.visible"
|
||||
v-model="addDataDialog.data"
|
||||
@submit-success="onRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -269,11 +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: {
|
||||
@@ -294,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);
|
||||
@@ -341,7 +324,6 @@ const state = reactive({
|
||||
addDataDialog: {
|
||||
data: {},
|
||||
title: '',
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
},
|
||||
tableHeight: '600px',
|
||||
@@ -349,7 +331,7 @@ const state = reactive({
|
||||
dbDialect: {} as DbDialect,
|
||||
});
|
||||
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tableHeight,
|
||||
@@ -367,7 +349,7 @@ onMounted(async () => {
|
||||
state.tableHeight = props.tableHeight;
|
||||
await onRefresh();
|
||||
|
||||
state.dbDialect = getDbDialect(getNowDbInst().type);
|
||||
state.dbDialect = getNowDbInst().getDialect();
|
||||
useEventListener('click', handlerWindowClick);
|
||||
});
|
||||
|
||||
@@ -542,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();
|
||||
};
|
||||
@@ -601,46 +583,6 @@ const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
const closeAddDataDialog = () => {
|
||||
state.addDataDialog.visible = false;
|
||||
state.addDataDialog.data = {};
|
||||
};
|
||||
|
||||
// 添加新数据行
|
||||
const addRow = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const dbInst = getNowDbInst();
|
||||
const data = state.addDataDialog.data;
|
||||
// key: 字段名,value: 字段名提示
|
||||
let obj: any = {};
|
||||
for (let item of state.columns) {
|
||||
const value = data[item.columnName];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
|
||||
}
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
// 获取schema
|
||||
let schema = '';
|
||||
let arr = props.dbName?.split('/');
|
||||
if (arr && arr.length > 1) {
|
||||
schema = dbInst.wrapName(arr[1]) + '.';
|
||||
}
|
||||
let sql = `INSERT INTO ${schema}${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
||||
closeAddDataDialog();
|
||||
onRefresh();
|
||||
});
|
||||
} else {
|
||||
ElMessage.error('请正确填写数据信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
v-for="pgsqlType in state.columnTypeList"
|
||||
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
|
||||
:key="pgsqlType.dataType"
|
||||
:value="pgsqlType.udtName"
|
||||
:label="pgsqlType.dataType"
|
||||
@@ -151,6 +151,9 @@ const props = defineProps({
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
flowProcdefKey: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
@@ -169,7 +172,6 @@ const state = reactive({
|
||||
dialogVisible: false,
|
||||
btnloading: false,
|
||||
activeName: '1',
|
||||
columnTypeList: dbDialect.getInfo().columnTypes,
|
||||
tableData: {
|
||||
fields: {
|
||||
colNames: [
|
||||
@@ -260,6 +262,8 @@ const state = reactive({
|
||||
},
|
||||
tableName: '',
|
||||
tableComment: '',
|
||||
oldTableName: '',
|
||||
oldTableComment: '',
|
||||
height: 450,
|
||||
db: '',
|
||||
},
|
||||
@@ -272,6 +276,18 @@ watch(props, async (newValue) => {
|
||||
dbDialect = getDbDialect(newValue.dbType);
|
||||
});
|
||||
|
||||
// 切换到索引tab时,刷新索引字段下拉选项
|
||||
watch(
|
||||
() => state.activeName,
|
||||
(newValue) => {
|
||||
if (newValue === '2') {
|
||||
state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
|
||||
return { name: a.name, remark: a.remark };
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
reset();
|
||||
@@ -318,6 +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();
|
||||
@@ -331,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;
|
||||
}
|
||||
|
||||
@@ -362,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);
|
||||
}
|
||||
});
|
||||
@@ -371,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 {
|
||||
// 判断每个字段是否相等,否则为修改
|
||||
@@ -378,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;
|
||||
}
|
||||
@@ -389,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, data.tableName, changeData);
|
||||
}
|
||||
// 修改列
|
||||
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||
// 修改索引
|
||||
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||
// 修改表名,表注释
|
||||
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
|
||||
|
||||
let sqlArr = [];
|
||||
colSql && sqlArr.push(colSql);
|
||||
idxSql && sqlArr.push(idxSql);
|
||||
tableInfoSql && sqlArr.push(tableInfoSql);
|
||||
|
||||
return sqlArr.join(';');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -416,7 +446,9 @@ const reset = () => {
|
||||
state.tableData.tableName = '';
|
||||
state.tableData.tableComment = '';
|
||||
state.tableData.fields.res = [];
|
||||
state.tableData.fields.oldFields = [];
|
||||
state.tableData.indexs.res = [];
|
||||
state.tableData.indexs.oldIndexs = [];
|
||||
};
|
||||
|
||||
const indexChanges = (row: any) => {
|
||||
@@ -459,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];
|
||||
@@ -494,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')
|
||||
|
||||
@@ -108,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"
|
||||
@@ -125,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, editDbTypes } from '../../dialect/index';
|
||||
import { compatibleMysql, editDbTypes } from '../../dialect/index';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -146,6 +147,9 @@ const props = defineProps({
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
flowProcdefKey: {
|
||||
type: [String],
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
@@ -317,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();
|
||||
},
|
||||
|
||||
@@ -40,6 +40,11 @@ export class DbInst {
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 流程定义key,若存在则需要审批执行
|
||||
*/
|
||||
flowProcdefKey: string;
|
||||
|
||||
/**
|
||||
* dbName -> db
|
||||
*/
|
||||
@@ -74,6 +79,11 @@ export class DbInst {
|
||||
return db;
|
||||
}
|
||||
|
||||
// 获取数据库实例方言
|
||||
getDialect(): DbDialect {
|
||||
return getDbDialect(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据库表信息
|
||||
* @param dbName 数据库名
|
||||
@@ -156,7 +166,7 @@ export class DbInst {
|
||||
const db = this.getDb(dbName);
|
||||
// 优先从 table map中获取
|
||||
let columns = db.getColumns(table);
|
||||
if (columns) {
|
||||
if (columns && columns.length > 0) {
|
||||
return columns;
|
||||
}
|
||||
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
|
||||
@@ -248,7 +258,7 @@ export class DbInst {
|
||||
|
||||
// 获取指定表的默认查询sql
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||
return this.getDialect().getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,12 +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) {
|
||||
@@ -269,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})`;
|
||||
}
|
||||
|
||||
@@ -297,20 +353,20 @@ export class DbInst {
|
||||
sql,
|
||||
dbId: this.id,
|
||||
db,
|
||||
dbType: getDbDialect(this.type).getInfo().formatSqlDialect,
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -334,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;
|
||||
@@ -363,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 字段类型
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
@@ -662,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)) {
|
||||
@@ -682,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});`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
import { DialectInfo } from '@/views/ops/db/dialect/index';
|
||||
import { DialectInfo, DuplicateStrategy } from '@/views/ops/db/dialect/index';
|
||||
|
||||
let gsDialectInfo: DialectInfo;
|
||||
export class GaussDialect extends PostgresqlDialect {
|
||||
@@ -14,4 +14,17 @@ export class GaussDialect extends PostgresqlDialect {
|
||||
gsDialectInfo.name = 'GaussDB';
|
||||
return gsDialectInfo;
|
||||
}
|
||||
|
||||
getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
|
||||
// 构建占位符字符串 "($1, $2, $3 ...)"
|
||||
let placeholder = fieldArr.map((_, i) => `$${i + 1}`).join(',');
|
||||
let suffix = '';
|
||||
if (duplicateStrategy === DuplicateStrategy.IGNORE) {
|
||||
suffix = '\nON DUPLICATE KEY UPDATE NOTHING';
|
||||
} else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
|
||||
suffix = '\n-- 执行前会删除唯一键涉及到的字段 \nON DUPLICATE KEY UPDATE ' + fieldArr.map((a) => `${a}=excluded.${a}`).join(',');
|
||||
}
|
||||
|
||||
return `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder}) ${suffix};`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
@@ -122,13 +124,15 @@ export const DbType = {
|
||||
oracle: 'oracle',
|
||||
sqlite: 'sqlite',
|
||||
mssql: 'mssql', // ms sqlserver
|
||||
kingbaseEs: 'kingbaseEs', // 人大金仓 pgsql模式 https://help.kingbase.com.cn/v8/index.html
|
||||
vastbase: 'vastbase', // https://docs.vastdata.com.cn/zh/docs/VastbaseG100Ver2.2.5/doc/%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/SQL%E5%8F%82%E8%80%83/SQL%E5%8F%82%E8%80%83.html
|
||||
};
|
||||
|
||||
// mysql兼容的数据库
|
||||
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
|
||||
|
||||
// 有schema层的数据库
|
||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql];
|
||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql, DbType.kingbaseEs, DbType.vastbase];
|
||||
|
||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
|
||||
|
||||
@@ -142,6 +146,25 @@ export const compatibleMysql = (dbType: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
// 哪些数据库支持键冲突策略
|
||||
export const compatibleDuplicateStrategy = (dbType: string): boolean => {
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
case DbType.mariadb:
|
||||
case DbType.postgresql:
|
||||
case DbType.gauss:
|
||||
case DbType.kingbaseEs:
|
||||
case DbType.vastbase:
|
||||
case DbType.dm:
|
||||
case DbType.oracle:
|
||||
case DbType.sqlite:
|
||||
case DbType.mssql:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DbDialect {
|
||||
/**
|
||||
* 获取一些数据库默认信息
|
||||
@@ -199,11 +222,28 @@ export interface DbDialect {
|
||||
*/
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
|
||||
/** 生成编辑表信息sql */
|
||||
getModifyTableInfoSql(tableData: any): string;
|
||||
|
||||
/** 通过数据库字段类型,返回基本数据类型 */
|
||||
getDataType(columnType: string): DataType;
|
||||
|
||||
/** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
|
||||
wrapStrValue(columnType: string, value: string): string;
|
||||
/** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') mssql需要把中文字符串数据包装为 N'中文字符串' */
|
||||
wrapValue(columnType: string, value: any): any;
|
||||
|
||||
/**
|
||||
* 生成插入数据预览sql
|
||||
* @param tableName 表名
|
||||
* @param columns 列名
|
||||
* @param duplicateStrategy 重复策略
|
||||
*/
|
||||
getBatchInsertPreviewSql(tableName: string, columns: string[], duplicateStrategy: DuplicateStrategy): string;
|
||||
}
|
||||
|
||||
export enum DuplicateStrategy {
|
||||
NONE = -1, // 无
|
||||
IGNORE = 1, // 忽略
|
||||
REPLACE = 2, // 覆盖
|
||||
}
|
||||
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
@@ -218,8 +258,8 @@ export const getDbDialectMap = () => {
|
||||
return dbType2DialectMap;
|
||||
};
|
||||
|
||||
export const getDbDialect = (dbType: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType) || mysqlDialect;
|
||||
export const getDbDialect = (dbType?: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||
};
|
||||
|
||||
(function () {
|
||||
@@ -232,4 +272,6 @@ export const getDbDialect = (dbType: string): DbDialect => {
|
||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
|
||||
registerDbDialect(DbType.vastbase, new VastbaseDialect());
|
||||
})();
|
||||
|
||||
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DialectInfo } from './index';
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
|
||||
let kbpgDialectInfo: DialectInfo;
|
||||
|
||||
export class KingbaseEsDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (kbpgDialectInfo) {
|
||||
return kbpgDialectInfo;
|
||||
}
|
||||
|
||||
kbpgDialectInfo = {} as DialectInfo;
|
||||
Object.assign(kbpgDialectInfo, super.getInfo());
|
||||
kbpgDialectInfo.name = 'KingbaseES';
|
||||
kbpgDialectInfo.icon = 'iconfont icon-kingbase';
|
||||
return kbpgDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,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 sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||
|
||||
export { MSSQL_TYPE_LIST, MssqlDialect };
|
||||
@@ -134,7 +144,7 @@ class MssqlDialect implements DbDialect {
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
type: 'nvarchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
@@ -157,7 +167,7 @@ class MssqlDialect implements DbDialect {
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{
|
||||
name: 'updator',
|
||||
type: 'varchar',
|
||||
type: 'nvarchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
@@ -263,7 +273,10 @@ class MssqlDialect implements DbDialect {
|
||||
}
|
||||
});
|
||||
|
||||
return sql.join(';') + ';' + indexComment.join(';');
|
||||
let arr = [];
|
||||
sql.length > 0 && arr.push(sql.join(';'));
|
||||
indexComment.length > 0 && arr.push(indexComment.join(';'));
|
||||
return arr.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
@@ -326,14 +339,15 @@ ELSE
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
delSql +
|
||||
(addArr.length > 0 ? addArr.join(';') + ';' : '') +
|
||||
(renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
|
||||
(updArr.length > 0 ? updArr.join(';') + ';' : '') +
|
||||
(changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
|
||||
(addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
|
||||
);
|
||||
let arr = [];
|
||||
delSql && arr.push(delSql);
|
||||
addArr.length > 0 && arr.push(addArr.join(';'));
|
||||
renameArr.length > 0 && arr.push(renameArr.join(';'));
|
||||
updArr.length > 0 && arr.push(updArr.join(';'));
|
||||
changeCommentArr.length > 0 && arr.push(changeCommentArr.join(';'));
|
||||
addCommentArr.length > 0 && arr.push(addCommentArr.join(';'));
|
||||
|
||||
return arr.join(';');
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
@@ -377,7 +391,43 @@ ELSE
|
||||
let dropSql = dropArr.join(';');
|
||||
let addSql = addArr.join(';');
|
||||
let commentSql = commentArr.join(';');
|
||||
return dropSql + ';' + addSql + ';' + commentSql + ';';
|
||||
|
||||
let arr = [];
|
||||
dropSql && arr.push(dropSql);
|
||||
addSql && arr.push(addSql);
|
||||
commentSql && arr.push(commentSql);
|
||||
return arr.join(';');
|
||||
}
|
||||
|
||||
getModifyTableInfoSql(tableData: any): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
|
||||
let sql = '';
|
||||
|
||||
if (tableData.oldTableName !== tableData.tableName) {
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||
// 查找是否存在注释,存在则修改,不存在则添加
|
||||
sql += `EXEC sp_rename '${baseTable}', '${tableData.tableName}';`;
|
||||
}
|
||||
|
||||
if (tableData.oldTableComment !== tableData.tableComment) {
|
||||
// 转义注释中的单引号和换行符
|
||||
let tableComment = tableData.tableComment.replaceAll(/'/g, '').replaceAll(/[\r\n]/g, ' ');
|
||||
sql += `IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableData.tableName}', NULL, NULL)) > 0)
|
||||
EXEC sp_updateextendedproperty
|
||||
'MS_Description', N'${tableComment}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableData.tableName}'
|
||||
ELSE
|
||||
EXEC sp_addextendedproperty
|
||||
'MS_Description', N'${tableComment}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableData.tableName}'`;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
getDataType(columnType: string): DataType {
|
||||
@@ -398,8 +448,46 @@ ELSE
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
|
||||
wrapValue(columnType: string, value: any): any {
|
||||
if (value == null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (this.getDataType(columnType) == DataType.Number) {
|
||||
return value;
|
||||
}
|
||||
if (this.getDataType(columnType) == DataType.String) {
|
||||
return `N'${value}'`;
|
||||
}
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
|
||||
let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
|
||||
let baseSql = `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder});`;
|
||||
if (duplicateStrategy === DuplicateStrategy.IGNORE) {
|
||||
let on = `ALTER TABLE ${tableName} ADD CONSTRAINT uniqueRows UNIQUE (id) WITH (IGNORE_DUP_KEY = ON);`;
|
||||
return on + '\n' + baseSql;
|
||||
}
|
||||
|
||||
if (duplicateStrategy === DuplicateStrategy.REPLACE) {
|
||||
// 字段数组生成占位符sql
|
||||
let phs = [];
|
||||
let values = [];
|
||||
for (let i = 0; i < fieldArr.length; i++) {
|
||||
phs.push(`? ${fieldArr[i]}`);
|
||||
values.push(`T2.${fieldArr[i]}`);
|
||||
}
|
||||
let placeholder = phs.join(',');
|
||||
let sql = `MERGE INTO ${tableName} T1 USING
|
||||
(
|
||||
SELECT ${placeholder}
|
||||
) T2 ON (T1.id = T2.id)
|
||||
WHEN NOT MATCHED THEN INSERT(${fieldArr.join(',')}) VALUES (${values.join(',')})
|
||||
WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
|
||||
return sql;
|
||||
}
|
||||
|
||||
return baseSql;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -227,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) => {
|
||||
@@ -250,7 +259,12 @@ 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(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
@@ -309,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;
|
||||
@@ -327,8 +353,27 @@ class MysqlDialect implements DbDialect {
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
|
||||
wrapValue(columnType: string, value: any): any {
|
||||
if (value == null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return value;
|
||||
}
|
||||
// 转义所有的换行符
|
||||
value = value.replace(/[\r\n]/g, '\\n');
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: number): string {
|
||||
let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
|
||||
let prefix = 'insert into';
|
||||
if (duplicateStrategy === DuplicateStrategy.IGNORE) {
|
||||
prefix = 'insert ignore into';
|
||||
} else if (duplicateStrategy === DuplicateStrategy.REPLACE) {
|
||||
prefix = 'replace into';
|
||||
}
|
||||
return `${prefix} ${this.quoteIdentifier(tableName)}(${fieldArr.join(',')}) values (${placeholder});`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
@@ -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格式转换为日期类型' },
|
||||
// 其他
|
||||
@@ -310,6 +311,7 @@ class OracleDialect implements DbDialect {
|
||||
let createSql = '';
|
||||
let tableCommentSql = '';
|
||||
let columCommentSql = '';
|
||||
let pris = [] as string[];
|
||||
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
@@ -319,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}'; `;
|
||||
@@ -376,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)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -412,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(';');
|
||||
@@ -463,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;
|
||||
@@ -473,11 +500,51 @@ class OracleDialect implements DbDialect {
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
|
||||
wrapValue(columnType: string, value: any): any {
|
||||
if (value == null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return value;
|
||||
}
|
||||
if (value && this.getDataType(columnType) === DataType.DateTime) {
|
||||
return `to_timestamp('${value}', 'yyyy-mm-dd hh24:mi:ss')`;
|
||||
}
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
|
||||
if (duplicateStrategy == DuplicateStrategy.REPLACE) {
|
||||
// 字段数组生成占位符sql
|
||||
let phs = [];
|
||||
let values = [];
|
||||
let insertFields = [];
|
||||
for (let i = 0; i < fieldArr.length; i++) {
|
||||
phs.push(`:${i + 1} ${fieldArr[i]}`);
|
||||
values.push(`T2.${fieldArr[i]}`);
|
||||
insertFields.push(`T1.${fieldArr[i]}`);
|
||||
}
|
||||
let placeholder = phs.join(',');
|
||||
let sql = `MERGE INTO ${this.quoteIdentifier(tableName)} T1 USING
|
||||
(
|
||||
SELECT ${placeholder} FROM dual
|
||||
) T2 ON (T1.id = T2.id)
|
||||
WHEN NOT MATCHED THEN INSERT (${insertFields.join(',')}) VALUES (${values.join(',')})
|
||||
WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
|
||||
return sql;
|
||||
} else {
|
||||
// 字段数组生成占位符sql
|
||||
let phs = [];
|
||||
for (let i = 0; i < fieldArr.length; i++) {
|
||||
phs.push(`:${i + 1} ${fieldArr[i]}`);
|
||||
}
|
||||
let ignore = '';
|
||||
if (duplicateStrategy == DuplicateStrategy.IGNORE) {
|
||||
ignore = `/*+ IGNORE_ROW_ON_DUPKEY_INDEX(${tableName}(id)) */`;
|
||||
}
|
||||
let placeholder = phs.join(',');
|
||||
return `INSERT ${ignore} INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder});`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
@@ -303,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(';');
|
||||
@@ -367,6 +372,9 @@ class PostgresqlDialect implements DbDialect {
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
@@ -400,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}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -411,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;
|
||||
@@ -430,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};`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
@@ -313,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;
|
||||
@@ -331,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});`;
|
||||
}
|
||||
}
|
||||
|
||||
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DialectInfo } from './index';
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
|
||||
let vastDialectInfo: DialectInfo;
|
||||
|
||||
export class VastbaseDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (vastDialectInfo) {
|
||||
return vastDialectInfo;
|
||||
}
|
||||
|
||||
vastDialectInfo = {} as DialectInfo;
|
||||
Object.assign(vastDialectInfo, super.getInfo());
|
||||
vastDialectInfo.name = 'VastbaseG100';
|
||||
vastDialectInfo.icon = 'iconfont icon-vastbase';
|
||||
return vastDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@ export const DbSqlExecTypeEnum = {
|
||||
Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
|
||||
};
|
||||
|
||||
export const DbSqlExecStatusEnum = {
|
||||
Success: EnumValue.of(2, '成功').setTagType('success'),
|
||||
Fail: EnumValue.of(-2, '失败').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncRecentStateEnum = {
|
||||
Success: EnumValue.of(1, '成功').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
:tag-path="form.tagPath"
|
||||
:select-tags="form.tagId"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<el-tab-pane label="其他配置" name="other">
|
||||
<el-form-item prop="enableRecorder" label="终端回放">
|
||||
<el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
|
||||
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
||||
@@ -85,7 +85,6 @@ import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import AuthCertSelect from './authcert/AuthCertSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -153,6 +152,7 @@ const state = reactive({
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
tagPath: '',
|
||||
ip: null,
|
||||
port: 22,
|
||||
name: null,
|
||||
@@ -181,7 +181,7 @@ watch(props, async (newValue: any) => {
|
||||
state.tabActiveName = 'basic';
|
||||
if (newValue.machine) {
|
||||
state.form = { ...newValue.machine };
|
||||
|
||||
state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
|
||||
// 如果凭证类型为公共的,则表示使用授权凭证认证
|
||||
const authCertId = (state.form as any).authCertId;
|
||||
if (authCertId > 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="machine-list">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="machineApi.list"
|
||||
@@ -25,7 +25,7 @@
|
||||
<span v-if="!data.stat">-</span>
|
||||
<div v-else>
|
||||
<el-row>
|
||||
<el-text size="small" style="font-size: 10px">
|
||||
<el-text size="small" class="font11">
|
||||
内存(可用/总):
|
||||
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
||||
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
||||
@@ -33,7 +33,7 @@
|
||||
</el-text>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-text style="font-size: 10px" size="small">
|
||||
<el-text class="font11" size="small">
|
||||
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
||||
</el-text>
|
||||
</el-row>
|
||||
@@ -44,7 +44,7 @@
|
||||
<span v-if="!data.stat?.fsInfos">-</span>
|
||||
<div v-else>
|
||||
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||
</el-text>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</template>
|
||||
|
||||
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
|
||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||
</el-text>
|
||||
</el-row>
|
||||
@@ -80,7 +80,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -118,26 +118,18 @@
|
||||
<el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
|
||||
终端回放
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'closeCli', data }"
|
||||
v-if="actionBtns[perms.closeCli]"
|
||||
:disabled="!data.hasCli || data.status == -1"
|
||||
>
|
||||
关闭连接
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible">
|
||||
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||
@@ -195,7 +187,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
@@ -223,25 +215,24 @@ const perms = {
|
||||
updateMachine: 'machine:update',
|
||||
delMachine: 'machine:del',
|
||||
terminal: 'machine:terminal',
|
||||
closeCli: 'machine:close-cli',
|
||||
};
|
||||
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), SearchItem.input('ip', 'IP'), SearchItem.input('name', '名称')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||
const actionBtns = hasPerms([perms.updateMachine]);
|
||||
|
||||
const state = reactive({
|
||||
params: {
|
||||
@@ -320,10 +311,6 @@ const handleCommand = (commond: any) => {
|
||||
showRec(data);
|
||||
return;
|
||||
}
|
||||
case 'closeCli': {
|
||||
closeCli(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -351,17 +338,6 @@ const showTerminal = (row: any, event: PointerEvent) => {
|
||||
});
|
||||
};
|
||||
|
||||
const closeCli = async (row: any) => {
|
||||
await ElMessageBox.confirm(`确定关闭该机器客户端连接?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await machineApi.closeCli.request({ id: row.id });
|
||||
ElMessage.success('关闭成功');
|
||||
search();
|
||||
};
|
||||
|
||||
const openFormDialog = async (machine: any) => {
|
||||
let dialogTitle;
|
||||
if (machine) {
|
||||
@@ -464,10 +440,6 @@ const showRec = (row: any) => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-dialog__body {
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.el-dropdown-link-machine-list {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
|
||||
405
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
405
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<div class="flex-all-center">
|
||||
<!-- 文档: https://antoniandre.github.io/splitpanes/ -->
|
||||
<Splitpanes class="default-theme" @resized="onResizeTagTree">
|
||||
<Pane size="20" max-size="30">
|
||||
<tag-tree
|
||||
class="machine-terminal-tree"
|
||||
ref="tagTreeRef"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
>
|
||||
<template #prefix="{ data }">
|
||||
<SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
|
||||
<SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
|
||||
</template>
|
||||
|
||||
<template #suffix="{ data }">
|
||||
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
|
||||
` ${data.params.username}@${data.params.ip}:${data.params.port}`
|
||||
}}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</Pane>
|
||||
|
||||
<Pane>
|
||||
<div class="machine-terminal-tabs card pd5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeTermName"
|
||||
class="h100"
|
||||
>
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||
><Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<div class="terminal-wrapper" style="height: calc(100vh - 155px)">
|
||||
<TerminalBody
|
||||
:mount-init="false"
|
||||
@status-change="terminalStatusChange(dt.key, $event)"
|
||||
:ref="(el) => setTerminalRef(el, dt.key)"
|
||||
:socket-url="dt.socketUrl"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="认证方式">
|
||||
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||
|
||||
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
|
||||
|
||||
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
|
||||
|
||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||
|
||||
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { NodeType, TagTreeNode } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus } from '@/components/terminal/common';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const perms = {
|
||||
addMachine: 'machine:add',
|
||||
updateMachine: 'machine:update',
|
||||
delMachine: 'machine:del',
|
||||
terminal: 'machine:terminal',
|
||||
closeCli: 'machine:close-cli',
|
||||
};
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||
|
||||
class MachineNodeType {
|
||||
static Machine = 1;
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
tagPath: '',
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
serviceDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
processDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
},
|
||||
fileDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
machineStatsDialog: {
|
||||
visible: false,
|
||||
stats: null,
|
||||
title: '',
|
||||
machineId: 0,
|
||||
},
|
||||
machineRecDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
activeTermName: '',
|
||||
tabs: new Map<string, any>(),
|
||||
});
|
||||
|
||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
||||
|
||||
const tagTreeRef: any = ref(null);
|
||||
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
|
||||
// 加载标签树下的机器列表
|
||||
state.params.tagPath = node.key;
|
||||
state.params.pageNum = 1;
|
||||
state.params.pageSize = 1000;
|
||||
const res = await search();
|
||||
// 把list 根据name字段排序
|
||||
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
return res.list.map((x: any) =>
|
||||
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
|
||||
.withParams(x)
|
||||
.withDisabled(x.status == -1)
|
||||
.withIcon({
|
||||
name: 'Monitor',
|
||||
color: '#409eff',
|
||||
})
|
||||
.withIsLeaf(true)
|
||||
);
|
||||
});
|
||||
|
||||
let openIds = {};
|
||||
|
||||
const NodeTypeMachine = (machine: any) => {
|
||||
let contextMenuItems = [];
|
||||
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
|
||||
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
|
||||
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
|
||||
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
|
||||
}
|
||||
|
||||
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
|
||||
// for (let k of state.tabs.keys()) {
|
||||
// // 存在该机器相关的终端tab,则直接激活该tab
|
||||
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
|
||||
// state.activeTermName = k;
|
||||
// onTabChange();
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
openTerminal(machine);
|
||||
});
|
||||
};
|
||||
|
||||
const openTerminal = (machine: any, ex?: boolean) => {
|
||||
// 新窗口打开
|
||||
if (ex) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
id: machine.id,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
let { name, id, username } = machine;
|
||||
|
||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||
openIds[id] = openIds[id] ? ++openIds[id] : 1;
|
||||
let sameIndex = openIds[id];
|
||||
|
||||
let key = `${id}_${username}_${sameIndex}`;
|
||||
// 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
|
||||
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
|
||||
|
||||
state.tabs.set(key, {
|
||||
key,
|
||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||
params: machine,
|
||||
socketUrl: getMachineTerminalSocketUrl(id),
|
||||
});
|
||||
state.activeTermName = key;
|
||||
|
||||
nextTick(() => {
|
||||
handleReconnect(key);
|
||||
});
|
||||
};
|
||||
|
||||
const serviceManager = (row: any) => {
|
||||
state.serviceDialog.machineId = row.id;
|
||||
state.serviceDialog.visible = true;
|
||||
state.serviceDialog.title = `${row.name} => ${row.ip}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示机器状态统计信息
|
||||
*/
|
||||
const showMachineStats = async (machine: any) => {
|
||||
state.machineStatsDialog.machineId = machine.id;
|
||||
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
|
||||
state.machineStatsDialog.visible = true;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const res = await machineApi.list.request(state.params);
|
||||
return res;
|
||||
};
|
||||
|
||||
const showFileManage = (selectionData: any) => {
|
||||
state.fileDialog.visible = true;
|
||||
state.fileDialog.machineId = selectionData.id;
|
||||
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
|
||||
};
|
||||
|
||||
const showInfo = (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const showProcess = (row: any) => {
|
||||
state.processDialog.machineId = row.id;
|
||||
state.processDialog.visible = true;
|
||||
};
|
||||
|
||||
const showRec = (row: any) => {
|
||||
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
|
||||
state.machineRecDialog.machineId = row.id;
|
||||
state.machineRecDialog.visible = true;
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
let activeTermName = state.activeTermName;
|
||||
const tabNames = [...state.tabs.keys()];
|
||||
for (let i = 0; i < tabNames.length; i++) {
|
||||
const tabName = tabNames[i];
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
state.activeTermName = activeTermName;
|
||||
onTabChange();
|
||||
}
|
||||
};
|
||||
|
||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
||||
state.tabs.get(key).status = status;
|
||||
};
|
||||
|
||||
const terminalRefs: any = {};
|
||||
const setTerminalRef = (el: any, key: any) => {
|
||||
if (key) {
|
||||
terminalRefs[key] = el;
|
||||
}
|
||||
};
|
||||
|
||||
const onResizeTagTree = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const onTabChange = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.fitTerminal();
|
||||
terminalRefs[info.key]?.focus();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleReconnect = (key: string) => {
|
||||
terminalRefs[key].init();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-terminal-tabs {
|
||||
height: calc(100vh - 108px);
|
||||
--el-tabs-header-height: 30px;
|
||||
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
.machine-terminal-tab-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-tabs__header {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.el-tabs__item {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -103,8 +103,8 @@ const playRec = async (rec: any) => {
|
||||
idleTimeLimit: 2,
|
||||
// fit: false,
|
||||
// terminalFontSize: 'small',
|
||||
// cols: 100,
|
||||
// rows: 33,
|
||||
cols: 144,
|
||||
rows: 32,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -12,7 +12,6 @@ export const machineApi = {
|
||||
process: Api.newGet('/machines/{id}/process'),
|
||||
// 终止进程
|
||||
killProcess: Api.newDelete('/machines/{id}/process'),
|
||||
closeCli: Api.newDelete('/machines/{id}/close-cli'),
|
||||
testConn: Api.newPost('/machines/test-conn'),
|
||||
// 保存按钮
|
||||
saveMachine: Api.newPost('/machines'),
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Mongo.value"
|
||||
:select-tags="form.tagId"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -58,7 +57,6 @@ import { mongoApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -129,6 +127,7 @@ watch(props, async (newValue: any) => {
|
||||
state.tabActiveName = 'basic';
|
||||
if (newValue.mongo) {
|
||||
state.form = { ...newValue.mongo };
|
||||
state.form.tagId = newValue.mongo.tags.map((t: any) => t.tagId);
|
||||
} else {
|
||||
state.form = { db: 0 } as any;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
|
||||
<resource-tags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -45,7 +45,7 @@
|
||||
import { mongoApi } from './api';
|
||||
import { defineAsyncComponent, ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
@@ -62,9 +62,9 @@ const pageTableRef: Ref<any> = ref(null);
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Mongo.value)];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('uri', '连接uri'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<!-- key info -->
|
||||
<key-header
|
||||
ref="keyHeader"
|
||||
:redis-id="redisId"
|
||||
:db="db"
|
||||
:redis="props.redis"
|
||||
:key-info="state.keyInfo"
|
||||
@refresh-content="refreshContent"
|
||||
@del-key="delKey"
|
||||
@@ -15,7 +14,7 @@
|
||||
</key-header>
|
||||
|
||||
<!-- key content -->
|
||||
<component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo"> </component>
|
||||
<component ref="keyValueRef" :is="components[componentName]" :redis="props.redis" :key-info="keyInfo"> </component>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,6 +22,7 @@
|
||||
import { defineAsyncComponent, watch, ref, shallowReactive, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import KeyHeader from './KeyHeader.vue';
|
||||
import { RedisInst } from './redis';
|
||||
|
||||
const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue'));
|
||||
const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue'));
|
||||
@@ -41,11 +41,9 @@ const components = shallowReactive({
|
||||
const keyValueRef = ref(null) as any;
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: Number,
|
||||
},
|
||||
db: {
|
||||
type: Number,
|
||||
redis: {
|
||||
type: RedisInst,
|
||||
required: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
@@ -55,7 +53,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:visible', 'changeKey', 'delKey']);
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
keyInfo: {} as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
import { reactive, watch, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
import { RedisInst } from './redis';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: Number,
|
||||
},
|
||||
db: {
|
||||
type: Number,
|
||||
redis: {
|
||||
type: RedisInst,
|
||||
required: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
@@ -62,7 +62,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['refreshContent', 'delKey', 'changeKey']);
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
keyInfo: {
|
||||
key: '',
|
||||
type: '',
|
||||
@@ -84,8 +83,8 @@ onMounted(() => {
|
||||
|
||||
const refreshKey = async () => {
|
||||
const ttl = await redisApi.keyTtl.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
id: props.redis.id,
|
||||
db: props.redis.db,
|
||||
key: state.oldKey,
|
||||
});
|
||||
state.keyInfo.timed = ttl;
|
||||
@@ -100,12 +99,8 @@ const renameKey = async () => {
|
||||
if (!state.oldKey || state.ki.key == state.oldKey) {
|
||||
return;
|
||||
}
|
||||
await redisApi.renameKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
newKey: state.ki.key,
|
||||
key: state.oldKey,
|
||||
});
|
||||
// RENAME key newkey
|
||||
await props.redis.runCmd(['RENAME', state.oldKey, state.ki.key]);
|
||||
ElMessage.success('设置成功');
|
||||
emit('changeKey');
|
||||
};
|
||||
@@ -130,22 +125,15 @@ const ttlKey = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await redisApi.expireKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
key: state.ki.key,
|
||||
seconds: state.ki.timed,
|
||||
});
|
||||
// EXPIRE key seconds [NX | XX | GT | LT]
|
||||
await props.redis.runCmd(['EXPIRE', state.ki.key, state.ki.timed]);
|
||||
ElMessage.success('设置成功');
|
||||
emit('changeKey');
|
||||
};
|
||||
|
||||
const persistKey = async () => {
|
||||
await redisApi.persistKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
key: state.keyInfo.key,
|
||||
});
|
||||
// PERSIST key
|
||||
await props.redis.runCmd(['PERSIST', state.keyInfo.key]);
|
||||
ElMessage.success('设置成功');
|
||||
emit('changeKey');
|
||||
};
|
||||
@@ -174,33 +162,7 @@ const ttlConveter = (ttl: any) => {
|
||||
if (!ttl) {
|
||||
ttl = 0;
|
||||
}
|
||||
let second = parseInt(ttl); // 秒
|
||||
let min = 0; // 分
|
||||
let hour = 0; // 小时
|
||||
let day = 0;
|
||||
if (second > 60) {
|
||||
min = parseInt(second / 60 + '');
|
||||
second = second % 60;
|
||||
if (min > 60) {
|
||||
hour = parseInt(min / 60 + '');
|
||||
min = min % 60;
|
||||
if (hour > 24) {
|
||||
day = parseInt(hour / 24 + '');
|
||||
hour = hour % 24;
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = '' + second + 's';
|
||||
if (min > 0) {
|
||||
result = '' + min + 'm:' + result;
|
||||
}
|
||||
if (hour > 0) {
|
||||
result = '' + hour + 'h:' + result;
|
||||
}
|
||||
if (day > 0) {
|
||||
result = '' + day + 'd:' + result;
|
||||
}
|
||||
return result;
|
||||
return formatTime(ttl);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Redis.value"
|
||||
:select-tags="form.tagId"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -72,6 +71,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-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他配置" name="other">
|
||||
@@ -100,7 +101,7 @@ import { ElMessage } from 'element-plus';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -172,6 +173,7 @@ const state = reactive({
|
||||
db: '',
|
||||
remark: '',
|
||||
sshTunnelMachineId: -1,
|
||||
flowProcdefKey: '',
|
||||
},
|
||||
submitForm: {} as any,
|
||||
dbList: [0],
|
||||
@@ -191,6 +193,7 @@ watch(props, async (newValue: any) => {
|
||||
state.tabActiveName = 'basic';
|
||||
if (newValue.redis) {
|
||||
state.form = { ...newValue.redis };
|
||||
state.form.tagId = newValue.redis.tags.map((t: any) => t.tagId);
|
||||
convertDb(state.form.db);
|
||||
} else {
|
||||
state.form = { db: '0' } as any;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Redis.value" />
|
||||
<resource-tags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -116,18 +116,18 @@
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailDialog.visible">
|
||||
<el-dialog v-if="detailDialog.visible" v-model="detailDialog.visible">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="id">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="名称">{{ detailDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="标签路径">{{ detailDialog.data.tagPath }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="主机">{{ detailDialog.data.host }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="工单流程key">{{ detailDialog.data?.flowProcdefKey }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||
@@ -154,7 +154,7 @@ import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import RedisEdit from './RedisEdit.vue';
|
||||
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 { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
@@ -167,10 +167,10 @@ const pageTableRef: Ref<any> = ref(null);
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Redis.value)];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port'),
|
||||
TableColumn.new('mode', 'mode'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
@@ -13,40 +13,125 @@ export const redisApi = {
|
||||
keyInfo: Api.newGet('/redis/{id}/{db}/key-info'),
|
||||
keyTtl: Api.newGet('/redis/{id}/{db}/key-ttl'),
|
||||
keyMemuse: Api.newGet('/redis/{id}/{db}/key-memuse'),
|
||||
renameKey: Api.newPost('/redis/{id}/{db}/rename-key'),
|
||||
expireKey: Api.newPost('/redis/{id}/{db}/expire-key'),
|
||||
persistKey: Api.newDelete('/redis/{id}/{db}/persist-key'),
|
||||
|
||||
// 获取权限列表
|
||||
// 获取key列表
|
||||
scan: Api.newPost('/redis/{id}/{db}/scan'),
|
||||
getString: Api.newGet('/redis/{id}/{db}/string-value'),
|
||||
setString: Api.newPost('/redis/{id}/{db}/string-value'),
|
||||
getHashValue: Api.newGet('/redis/{id}/{db}/hash-value'),
|
||||
hscan: Api.newGet('/redis/{id}/{db}/hscan'),
|
||||
hget: Api.newGet('/redis/{id}/{db}/hget'),
|
||||
hset: Api.newPost('/redis/{id}/{db}/hset'),
|
||||
hdel: Api.newDelete('/redis/{id}/{db}/hdel'),
|
||||
saveHashValue: Api.newPost('/redis/{id}/{db}/hash-value'),
|
||||
|
||||
getSetValue: Api.newGet('/redis/{id}/{db}/set-value'),
|
||||
scard: Api.newGet('/redis/{id}/{db}/scard'),
|
||||
sscan: Api.newPost('/redis/{id}/{db}/sscan'),
|
||||
sadd: Api.newPost('/redis/{id}/{db}/sadd'),
|
||||
srem: Api.newPost('/redis/{id}/{db}/srem'),
|
||||
saveSetValue: Api.newPost('/redis/{id}/{db}/set-value'),
|
||||
|
||||
del: Api.newDelete('/redis/{id}/{db}/scan/{cursor}/{count}'),
|
||||
delKey: Api.newDelete('/redis/{id}/{db}/key'),
|
||||
flushDb: Api.newDelete('/redis/{id}/{db}/flushdb'),
|
||||
|
||||
lrem: Api.newPost('/redis/{id}/{db}/lrem'),
|
||||
getListValue: Api.newGet('/redis/{id}/{db}/list-value'),
|
||||
saveListValue: Api.newPost('/redis/{id}/{db}/list-value'),
|
||||
setListValue: Api.newPost('/redis/{id}/{db}/list-value/lset'),
|
||||
|
||||
zcard: Api.newGet('/redis/{id}/{db}/zcard'),
|
||||
zscan: Api.newGet('/redis/{id}/{db}/zscan'),
|
||||
zrevrange: Api.newGet('/redis/{id}/{db}/zrevrange'),
|
||||
zadd: Api.newPost('/redis/{id}/{db}/zadd'),
|
||||
zrem: Api.newPost('/redis/{id}/{db}/zrem'),
|
||||
runCmd: Api.newPost('/redis/{id}/{db}/run-cmd'),
|
||||
};
|
||||
|
||||
export function splitargs(line: string) {
|
||||
var ret = [] as any;
|
||||
if (!line || typeof line.length !== 'number') {
|
||||
return ret;
|
||||
}
|
||||
|
||||
var len = line.length;
|
||||
var pos = 0;
|
||||
while (true) {
|
||||
// skip blanks
|
||||
while (pos < len && isspace(line[pos])) {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
if (pos === len) {
|
||||
break;
|
||||
}
|
||||
|
||||
var inq = false; // if we are in "quotes"
|
||||
var insq = false; // if we are in "single quotes"
|
||||
var done = false;
|
||||
var current = '';
|
||||
|
||||
while (!done) {
|
||||
var c = line[pos];
|
||||
if (inq) {
|
||||
if (c === '\\' && pos + 1 < len) {
|
||||
pos += 1;
|
||||
|
||||
switch (line[pos]) {
|
||||
case 'n':
|
||||
c = '\n';
|
||||
break;
|
||||
case 'r':
|
||||
c = '\r';
|
||||
break;
|
||||
case 't':
|
||||
c = '\t';
|
||||
break;
|
||||
case 'b':
|
||||
c = '\b';
|
||||
break;
|
||||
case 'a':
|
||||
c = 'a';
|
||||
break;
|
||||
default:
|
||||
c = line[pos];
|
||||
break;
|
||||
}
|
||||
current += c;
|
||||
} else if (c === '"') {
|
||||
// closing quote must be followed by a space or
|
||||
// nothing at all.
|
||||
if (pos + 1 < len && !isspace(line[pos + 1])) {
|
||||
throw new Error("Expect '\"' followed by a space or nothing, got '" + line[pos + 1] + "'.");
|
||||
}
|
||||
done = true;
|
||||
} else if (pos === len) {
|
||||
throw new Error('Unterminated quotes.');
|
||||
} else {
|
||||
current += c;
|
||||
}
|
||||
} else if (insq) {
|
||||
if (c === '\\' && line[pos + 1] === "'") {
|
||||
pos += 1;
|
||||
current += "'";
|
||||
} else if (c === "'") {
|
||||
// closing quote must be followed by a space or
|
||||
// nothing at all.
|
||||
if (pos + 1 < len && !isspace(line[pos + 1])) {
|
||||
throw new Error('Expect "\'" followed by a space or nothing, got "' + line[pos + 1] + '".');
|
||||
}
|
||||
done = true;
|
||||
} else if (pos === len) {
|
||||
throw new Error('Unterminated quotes.');
|
||||
} else {
|
||||
current += c;
|
||||
}
|
||||
} else {
|
||||
if (pos === len) {
|
||||
done = true;
|
||||
} else {
|
||||
switch (c) {
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
done = true;
|
||||
break;
|
||||
case '"':
|
||||
inq = true;
|
||||
break;
|
||||
case "'":
|
||||
insq = true;
|
||||
break;
|
||||
default:
|
||||
current += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos < len) {
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
ret.push(current);
|
||||
current = '';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function isspace(ch: string) {
|
||||
return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\v' || ch === '\f';
|
||||
}
|
||||
|
||||
28
mayfly_go_web/src/views/ops/redis/components/CmdExecBox.ts
Normal file
28
mayfly_go_web/src/views/ops/redis/components/CmdExecBox.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { h, render } from 'vue';
|
||||
import CmdExecDialog from './CmdExecDialog.vue';
|
||||
|
||||
export type CmdExecProps = {
|
||||
id: number;
|
||||
db: number | string;
|
||||
cmd: any[];
|
||||
flowProcdefKey?: string;
|
||||
visible?: boolean;
|
||||
runSuccessFn?: Function;
|
||||
cancelFn?: Function;
|
||||
};
|
||||
|
||||
const showCmdExecBox = (props: CmdExecProps): void => {
|
||||
const propsCancelFn = props.cancelFn;
|
||||
props.cancelFn = () => {
|
||||
propsCancelFn && propsCancelFn();
|
||||
setTimeout(() => {
|
||||
// 销毁组件
|
||||
render(null, document.body);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const vnode = h(CmdExecDialog, { ...props, visible: true });
|
||||
render(vnode, document.body);
|
||||
};
|
||||
|
||||
export default showCmdExecBox;
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog title="待执行cmd" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
||||
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" rows="5" />
|
||||
<el-input @keyup.enter="runCmd" 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>
|
||||
<el-button @click="runCmd" type="primary" :loading="btnLoading">执 行</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, reactive, onMounted } from 'vue';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
|
||||
|
||||
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
|
||||
import { redisApi } from '../api';
|
||||
import { CmdExecProps } from './CmdExecBox';
|
||||
|
||||
const props = withDefaults(defineProps<CmdExecProps>(), {});
|
||||
|
||||
const remarkInputRef = ref<InputInstance>();
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
flowProcdefKey: '' as any,
|
||||
cmdStr: '',
|
||||
remark: '',
|
||||
btnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, remark, btnLoading } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
show(props);
|
||||
});
|
||||
|
||||
const show = (props: CmdExecProps) => {
|
||||
const cmdArr = props.cmd.map((item: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return item; // 第一个元素直接返回原值
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return `'${item}'`; // 字符串加单引号
|
||||
}
|
||||
return item; // 其他类型直接返回
|
||||
});
|
||||
state.cmdStr = cmdArr.join(' ');
|
||||
|
||||
state.dialogVisible = props.visible || true;
|
||||
setTimeout(() => {
|
||||
remarkInputRef.value?.focus();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行cmd
|
||||
*/
|
||||
const runCmd = async () => {
|
||||
if (!state.remark) {
|
||||
ElMessage.error('请输入执行备注信息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
await redisApi.runCmd.request({
|
||||
id: props.id,
|
||||
db: props.db,
|
||||
cmd: props.cmd,
|
||||
remark: state.remark,
|
||||
});
|
||||
props.runSuccessFn && props.runSuccessFn();
|
||||
ElMessage.success('工单提交成功');
|
||||
} finally {
|
||||
state.btnLoading = false;
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
state.dialogVisible = false;
|
||||
props.cancelFn && props.cancelFn();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
138
mayfly_go_web/src/views/ops/redis/redis.ts
Normal file
138
mayfly_go_web/src/views/ops/redis/redis.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { redisApi } from './api';
|
||||
import showCmdExecBox from './components/CmdExecBox';
|
||||
|
||||
export class RedisInst {
|
||||
/**
|
||||
* 实例id
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* db
|
||||
*/
|
||||
db: number;
|
||||
|
||||
/**
|
||||
* 流程定义key,若存在则需要审批执行
|
||||
*/
|
||||
flowProcdefKey: string;
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
* @param cmd 命令列表如:['SET', 'key', 'value']
|
||||
* @returns 执行结果
|
||||
*/
|
||||
async runCmd(cmd: any[]) {
|
||||
// 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交
|
||||
if (this.flowProcdefKey && writeCmd[cmd[0].toUpperCase()]) {
|
||||
showCmdExecBox({
|
||||
id: this.id,
|
||||
db: this.db,
|
||||
flowProcdefKey: this.flowProcdefKey,
|
||||
cmd,
|
||||
});
|
||||
// 报错,阻止后续继续执行
|
||||
throw new Error('提交工单执行');
|
||||
}
|
||||
|
||||
return await redisApi.runCmd.request({
|
||||
id: this.id,
|
||||
db: this.db,
|
||||
cmd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const writeCmd = {
|
||||
APPEND: 'APPEND key value',
|
||||
BLMOVE: 'BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout',
|
||||
BLPOP: 'BLPOP key [key ...] timeout',
|
||||
BRPOP: 'BRPOP key [key ...] timeout',
|
||||
BRPOPLPUSH: 'BRPOPLPUSH source destination timeout',
|
||||
BZPOPMAX: 'BZPOPMAX key [key ...] timeout',
|
||||
BZPOPMIN: 'BZPOPMIN key [key ...] timeout',
|
||||
COPY: 'COPY source destination [DB destination-db] [REPLACE]',
|
||||
DECR: 'DECR key',
|
||||
DECRBY: 'DECRBY key decrement',
|
||||
DEL: 'DEL key [key ...]',
|
||||
EVAL: 'EVAL script numkeys key [key ...] arg [arg ...]',
|
||||
EVALSHA: 'EVALSHA sha1 numkeys key [key ...] arg [arg ...]',
|
||||
EXPIRE: 'EXPIRE key seconds',
|
||||
EXPIREAT: 'EXPIREAT key timestamp',
|
||||
FLUSHALL: 'FLUSHALL',
|
||||
FLUSHDB: 'FLUSHDB',
|
||||
GEOADD: 'GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]',
|
||||
GETDEL: 'GETDEL key',
|
||||
GETSET: 'GETSET key value',
|
||||
HDEL: 'HDEL key field [field ...]',
|
||||
HINCRBY: 'HINCRBY key field increment',
|
||||
HINCRBYFLOAT: 'HINCRBYFLOAT key field increment',
|
||||
HMSET: 'HMSET key field value [field value ...]',
|
||||
HSET: 'HSET key field value',
|
||||
HSETNX: 'HSETNX key field value',
|
||||
INCR: 'INCR key',
|
||||
INCRBY: 'INCRBY key increment',
|
||||
INCRBYFLOAT: 'INCRBYFLOAT key increment',
|
||||
LINSERT: 'LINSERT key BEFORE|AFTER pivot value',
|
||||
LMOVE: 'LMOVE source destination LEFT|RIGHT LEFT|RIGHT',
|
||||
LPOP: 'LPOP key',
|
||||
LPUSH: 'LPUSH key value [value ...]',
|
||||
LPUSHX: 'LPUSHX key value',
|
||||
LREM: 'LREM key count value',
|
||||
LSET: 'LSET key index value',
|
||||
LTRIM: 'LTRIM key start stop',
|
||||
MIGRATE: 'MIGRATE host port key destination-db timeout',
|
||||
MOVE: 'MOVE key db',
|
||||
MSET: 'MSET key value [key value ...]',
|
||||
MSETNX: 'MSETNX key value [key value ...]',
|
||||
PERSIST: 'PERSIST key',
|
||||
PEXPIRE: 'PEXPIRE key milliseconds',
|
||||
PEXPIREAT: 'PEXPIREAT key milliseconds-timestamp',
|
||||
PSETEX: 'PSETEX key milliseconds value',
|
||||
PUBLISH: 'PUBLISH channel message',
|
||||
RENAME: 'RENAME key newkey',
|
||||
RENAMENX: 'RENAMENX key newkey',
|
||||
RESTORE: 'RESTORE key ttl serialized-value',
|
||||
RPOP: 'RPOP key',
|
||||
RPOPLPUSH: 'RPOPLPUSH source destination',
|
||||
RPUSH: 'RPUSH key value [value ...]',
|
||||
RPUSHX: 'RPUSHX key value',
|
||||
SADD: 'SADD key member [member ...]',
|
||||
SCRIPT: ['SCRIPT EXISTS script [script ...]', 'SCRIPT FLUSH', 'SCRIPT KILL', 'SCRIPT LOAD script'],
|
||||
SDIFFSTORE: 'SDIFFSTORE destination key [key ...]',
|
||||
SET: 'SET key value',
|
||||
SETBIT: 'SETBIT key offset value',
|
||||
SETEX: 'SETEX key seconds value',
|
||||
SETNX: 'SETNX key value',
|
||||
SETRANGE: 'SETRANGE key offset value',
|
||||
SINTERSTORE: 'SINTERSTORE destination key [key ...]',
|
||||
SMOVE: 'SMOVE source destination member',
|
||||
SORT: 'SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]',
|
||||
SPOP: 'SPOP key',
|
||||
SREM: 'SREM key member [member ...]',
|
||||
SUNIONSTORE: 'SUNIONSTORE destination key [key ...]',
|
||||
SWAPDB: 'SWAPDB index1 index2',
|
||||
UNLINK: 'UNLINK key [key ...]',
|
||||
XADD: 'XADD key ID field string [field string ...]',
|
||||
XDEL: 'XDEL key ID [ID ...]',
|
||||
XGROUP: [
|
||||
'XGROUP CREATE key groupname id|$ [MKSTREAM]',
|
||||
'XGROUP CREATECONSUMER key groupname consumername',
|
||||
'XGROUP DELCONSUMER key groupname consumername',
|
||||
'XGROUP DESTROY key groupname',
|
||||
'XGROUP SETID key groupname id|$',
|
||||
],
|
||||
XTRIM: 'XTRIM key MAXLEN [~] count',
|
||||
ZADD: 'ZADD key score member [score] [member]',
|
||||
ZDIFFSTORE: 'ZDIFFSTORE destination numkeys key [key ...]',
|
||||
ZINCRBY: 'ZINCRBY key increment member',
|
||||
ZINTERSTORE: 'ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
|
||||
ZPOPMAX: 'ZPOPMAX key [count]',
|
||||
ZPOPMIN: 'ZPOPMIN key [count]',
|
||||
ZRANGESTORE: 'ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]',
|
||||
ZREM: 'ZREM key member [member ...]',
|
||||
ZREMRANGEBYLEX: 'ZREMRANGEBYLEX key min max',
|
||||
ZREMRANGEBYRANK: 'ZREMRANGEBYRANK key start stop',
|
||||
ZREMRANGEBYSCORE: 'ZREMRANGEBYSCORE key min max',
|
||||
ZUNIONSTORE: 'ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export function keysToTree(keys: any, separator: string = ':', openStatus: any =
|
||||
return formatTreeData(tree, '', separator, openStatus, forceCut);
|
||||
}
|
||||
|
||||
export function keysToList(keys: any, separator: string = ':', openStatus: any = null, forceCut = 20000) {
|
||||
export function keysToList(keys: any) {
|
||||
return keys.map((x: string) => {
|
||||
return {
|
||||
key: x,
|
||||
|
||||
@@ -415,4 +415,3 @@ const removeDeafultExpandId = (id: any) => {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@/components/contextmenu
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover v-if="props.accountId" @show="getAccountInfo(props.accountId)" placement="top-start" title="账号信息" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-link type="primary">{{ props.username }}</el-link>
|
||||
</template>
|
||||
<el-descriptions v-loading="loading" :column="2" border>
|
||||
<el-descriptions-item label="username">{{ account.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">
|
||||
{{ account.name }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="角色">
|
||||
<el-tag v-for="role in account.roles" :key="role.code" class="ml5">
|
||||
{{ role.roleName }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-popover>
|
||||
<span v-else>{{ props.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { accountApi } from '../../api';
|
||||
const props = defineProps({
|
||||
accountId: {
|
||||
type: [Number],
|
||||
},
|
||||
username: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
account: {} as any,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { account, loading } = toRefs(state);
|
||||
|
||||
const getAccountInfo = async (id: number) => {
|
||||
try {
|
||||
state.loading = true;
|
||||
state.account = await accountApi.getAccountDetail.request({ id });
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user