Files
mayfly-go/mayfly_go_web/src/views/ops/db/SqlExec.vue

637 lines
23 KiB
Vue
Raw Normal View History

<template>
<div class="db-sql-exec">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
<template #prefix="{ data }">
2023-11-03 17:09:20 +08:00
<span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
<template #reference>
<SvgIcon :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
</template>
<template #default>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="名称">
{{ data.params.name }}
</el-descriptions-item>
<el-descriptions-item label="host">
{{ `${data.params.host}:${data.params.port}` }}
</el-descriptions-item>
<el-descriptions-item label="user">
{{ data.params.username }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ data.params.remark }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
</template>
<template #label="{ data }">
<el-tooltip placement="left" :show-after="1000" v-if="data.type.value == SqlExecNodeType.Table" :content="data.params.tableComment">
{{ data.label }}
</el-tooltip>
2023-03-28 15:22:05 +08:00
</template>
<template #suffix="{ data }">
2023-11-03 17:09:20 +08:00
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
` ${data.params.dbTableSize}`
}}</span>
</template>
</tag-tree>
</Pane>
<Pane>
2023-11-15 12:28:49 +08:00
<el-row>
<el-col :span="24" v-if="state.db">
<el-descriptions :column="4" size="small" border class="ml5">
<el-descriptions-item label-align="right" label="操作"
><el-button
:disabled="!state.db || !nowDbInst.id"
type="primary"
icon="Search"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases?.split(' ') }, state.db)"
size="small"
>新建查询</el-button
></el-descriptions-item
>
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
<el-descriptions-item label-align="right">
<template #label>
<div>
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
2023-11-15 12:28:49 +08:00
实例
</div>
</template>
{{ nowDbInst.id }}
<el-divider direction="vertical" border-style="dashed" />
{{ nowDbInst.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ nowDbInst.host }}
</el-descriptions-item>
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<div id="data-exec" class="mt5 ml5">
2023-11-13 17:41:03 +08:00
<el-tabs
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeName"
class="h100"
2023-11-13 17:41:03 +08:00
>
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
2023-11-13 17:41:03 +08:00
<template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference> {{ dt.label }} </template>
<template #default>
<el-descriptions :column="1" size="small">
2023-11-13 17:41:03 +08:00
<el-descriptions-item label="tagPath">
{{ dt.params.tagPath }}
</el-descriptions-item>
<el-descriptions-item label="名称">
{{ dt.params.name }}
</el-descriptions-item>
<el-descriptions-item label="host">
<SvgIcon :name="getDbDialect(dt.params.type).getInfo().icon" :size="18" />
2023-11-13 17:41:03 +08:00
{{ dt.params.host }}
</el-descriptions-item>
2023-11-13 17:41:03 +08:00
<el-descriptions-item label="库名">
{{ dt.params.dbName }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
2023-11-13 17:41:03 +08:00
</template>
<db-table-data-op
v-if="dt.type === TabType.TableData"
:db-id="dt.dbId"
:db-name="dt.db"
:table-name="dt.params.table"
:table-height="state.dataTabsTableHeight"
></db-table-data-op>
<db-sql-editor
v-if="dt.type === TabType.Query"
:db-id="dt.dbId"
:db-name="dt.db"
:sql-name="dt.params.sqlName"
@save-sql-success="reloadSqls"
>
</db-sql-editor>
<db-tables-op
v-if="dt.type == TabType.TablesOp"
:db-id="dt.params.id"
:db="dt.params.db"
:db-type="dt.params.type"
:height="state.tablesOpHeight"
/>
2023-02-06 17:14:16 +08:00
</el-tab-pane>
</el-tabs>
2023-11-15 12:28:49 +08:00
</div>
</Pane>
</Splitpanes>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, toRefs, onBeforeUnmount } from 'vue';
2023-11-24 17:03:08 +08:00
import { ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { DbInst, TabInfo, TabType, registerDbCompletionItemProvider } from './db';
2023-11-03 17:09:20 +08:00
import { TagTreeNode, NodeType } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
2023-11-15 12:28:49 +08:00
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
2023-11-13 17:41:03 +08:00
import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
2023-11-26 21:21:35 +08:00
import { getDbDialect } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Splitpanes, Pane } from 'splitpanes';
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
/**
* 树节点类型
*/
2023-11-03 17:09:20 +08:00
class SqlExecNodeType {
static DbInst = 1;
static Db = 2;
static TableMenu = 3;
static SqlMenu = 4;
static Table = 5;
static Sql = 6;
static PgSchemaMenu = 7;
static PgSchema = 8;
}
2023-11-03 17:09:20 +08:00
const DbIcon = {
name: 'Coin',
color: '#67c23a',
};
// pgsql schema icon
const SchemaIcon = {
name: 'List',
color: '#67c23a',
};
const TableIcon = {
name: 'Calendar',
color: '#409eff',
};
const SqlIcon = {
name: 'Files',
color: '#f56c6c',
};
2023-11-03 17:09:20 +08:00
// node节点点击时触发改变db事件
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
2023-11-03 17:09:20 +08:00
const params = nodeData.params;
2023-11-15 12:28:49 +08:00
if (params.db) {
changeDb(
{ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database },
params.db
);
}
2023-11-03 17:09:20 +08:00
};
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
2023-11-03 17:09:20 +08:00
if (!dbInfos) {
return [];
}
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
2023-11-03 17:09:20 +08:00
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
2023-11-03 17:09:20 +08:00
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
});
// 数据库实例节点类型
2023-11-15 12:28:49 +08:00
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
return dbs.map((x: any) => {
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
.withParams({
tagPath: params.tagPath,
id: params.id,
name: params.name,
type: params.type,
host: `${params.host}:${params.port}`,
dbs: dbs,
db: x,
})
.withIcon(DbIcon);
});
});
2023-11-03 17:09:20 +08:00
// 数据库节点
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
2023-12-06 14:50:02 +08:00
if (params.type == 'postgres' || params.type === 'dm') {
return [new TagTreeNode(`${params.id}.${params.db}.schema-menu`, 'schema', NodeTypePostgresScheamMenu).withParams(params).withIcon(SchemaIcon)];
}
2023-11-03 17:09:20 +08:00
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
2023-11-03 17:09:20 +08:00
];
})
.withNodeClickFunc(nodeClickChangeDb);
// postgres schema模式菜单
const NodeTypePostgresScheamMenu = new NodeType(SqlExecNodeType.PgSchemaMenu)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
// 将db变更为 db/schema;
const nParams = { ...params };
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
});
})
.withNodeClickFunc(nodeClickChangeDb);
// postgres schema模式
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
];
})
.withNodeClickFunc(nodeClickChangeDb);
2023-11-03 17:09:20 +08:00
// 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
.withContextMenuItems([
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadTables(data.key)),
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
const params = data.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
}),
])
2023-11-03 17:09:20 +08:00
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
let { id, db } = params;
2023-11-03 17:09:20 +08:00
// 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false;
let dbTableSize = 0;
const tablesNode = tables.map((x: any) => {
dbTableSize += x.dataLength + x.indexLength;
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
.withIsLeaf(true)
.withParams({
id,
db,
tableName: x.tableName,
tableComment: x.tableComment,
size: formatByteSize(x.dataLength + x.indexLength, 1),
})
.withIcon(TableIcon);
2023-11-03 17:09:20 +08:00
});
// 设置父节点参数的表大小
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
return tablesNode;
})
.withNodeClickFunc(nodeClickChangeDb);
2023-11-03 17:09:20 +08:00
// 数据库sql模板菜单节点
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const id = params.id;
const db = params.db;
const dbs = params.dbs;
// 加载用户保存的sql脚本
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
.withIsLeaf(true)
.withParams({
id,
db,
dbs,
sqlName: x.name,
})
.withIcon(SqlIcon);
2023-11-03 17:09:20 +08:00
});
})
.withNodeClickFunc(nodeClickChangeDb);
2023-11-03 17:09:20 +08:00
// 表节点类型
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
const params = nodeData.params;
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
});
// sql模板节点类型
2023-11-24 17:03:08 +08:00
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
.withNodeClickFunc((nodeData: TagTreeNode) => {
const params = nodeData.params;
addQueryTab({ id: params.id, nodeKey: nodeData.key, dbs: params.dbs }, params.db, params.sqlName);
})
.withContextMenuItems([
new ContextmenuItem('delSql', '删除').withIcon('delete').withOnClick((data: any) => deleteSql(data.params.id, data.params.db, data.params.sqlName)),
]);
2023-11-03 17:09:20 +08:00
const tagTreeRef: any = ref(null);
2023-01-31 10:37:40 +08:00
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
2023-02-15 21:28:01 +08:00
/**
* 当前操作的数据库实例
*/
nowDbInst: {} as DbInst,
db: '', // 当前操作的数据库
activeName: '',
2023-03-28 15:22:05 +08:00
reloadStatus: false,
tabs,
dataTabsTableHeight: '600px',
tablesOpHeight: '600',
});
const { nowDbInst } = toRefs(state);
2023-02-15 21:28:01 +08:00
onMounted(() => {
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
window.onresize = () => setHeight();
2022-11-05 22:18:59 +08:00
});
onBeforeUnmount(() => {
dispposeCompletionItemProvider('sql');
});
/**
* 设置editor高度和数据表高度
*/
const setHeight = () => {
state.dataTabsTableHeight = window.innerHeight - 255 + 'px';
2023-11-13 17:41:03 +08:00
state.tablesOpHeight = window.innerHeight - 220 + 'px';
2022-10-28 17:42:23 +08:00
};
2023-11-13 17:41:03 +08:00
// 选择数据库,改变当前正在操作的数据库信息
const changeDb = (db: any, dbName: string) => {
state.nowDbInst = DbInst.getOrNewInst(db);
state.db = dbName;
};
2022-10-28 17:42:23 +08:00
// 加载选中的表数据即新增表数据操作tab
const loadTableData = async (db: any, dbName: string, tableName: string) => {
if (tableName == '') {
return;
}
2023-11-15 12:28:49 +08:00
changeDb(db, dbName);
2022-11-07 21:57:51 +08:00
const key = `${db.id}:\`${dbName}\`.${tableName}`;
let tab = state.tabs.get(key);
state.activeName = key;
// 如果存在该表tab则直接返回
if (tab) {
return;
}
tab = new TabInfo();
tab.label = tableName;
tab.key = key;
tab.treeNodeKey = db.nodeKey;
tab.dbId = db.id;
tab.db = dbName;
tab.type = TabType.TableData;
2023-02-14 11:44:48 +08:00
tab.params = {
2023-11-13 17:41:03 +08:00
...getNowDbInfo(),
table: tableName,
};
state.tabs.set(key, tab);
};
// 新建查询tab
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
if (!dbName || !db.id) {
ElMessage.warning('请选择数据库实例及对应的schema');
return;
}
2023-11-15 12:28:49 +08:00
changeDb(db, dbName);
const dbId = db.id;
let label;
let key;
// 存在sql模板名则该模板名只允许一个tab
if (sqlName) {
label = `查询-${sqlName}`;
key = `查询:${dbId}:${dbName}.${sqlName}`;
} else {
let count = 1;
state.tabs.forEach((v) => {
2023-02-14 11:44:48 +08:00
if (v.type == TabType.Query && !v.params.sqlName) {
count++;
2022-11-07 21:57:51 +08:00
}
});
label = `新查询-${count}`;
key = `新查询${count}:${dbId}:${dbName}`;
}
state.activeName = key;
let tab = state.tabs.get(key);
if (tab) {
return;
}
tab = new TabInfo();
tab.key = key;
tab.label = label;
tab.treeNodeKey = db.nodeKey;
tab.dbId = dbId;
tab.db = dbName;
tab.type = TabType.Query;
2023-02-14 11:44:48 +08:00
tab.params = {
2023-11-13 17:41:03 +08:00
...getNowDbInfo(),
sqlName: sqlName,
dbs: db.dbs,
};
state.tabs.set(key, tab);
// 注册当前sql编辑框提示词
registerDbCompletionItemProvider('sql', tab.dbId, tab.db, tab.params.dbs);
};
/**
* 添加数据操作tab
* @param inst
*/
const addTablesOpTab = async (db: any) => {
const dbName = db.db;
if (!db || !db.id) {
ElMessage.warning('请选择数据库实例及对应的schema');
return;
}
2023-11-15 12:28:49 +08:00
changeDb(db, dbName);
const dbId = db.id;
let key = `表操作:${dbId}:${dbName}.tablesOp`;
state.activeName = key;
let tab = state.tabs.get(key);
if (tab) {
return;
}
tab = new TabInfo();
tab.key = key;
tab.label = `表操作-${dbName}`;
tab.treeNodeKey = db.nodeKey;
tab.dbId = dbId;
tab.db = dbName;
tab.type = TabType.TablesOp;
tab.params = {
2023-11-13 17:41:03 +08:00
...getNowDbInfo(),
id: db.id,
db: dbName,
type: db.type,
};
state.tabs.set(key, tab);
};
2023-02-15 21:28:01 +08:00
const onRemoveTab = (targetName: string) => {
let activeName = state.activeName;
const tabNames = [...state.tabs.keys()];
for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i];
if (tabName !== targetName) {
continue;
2022-11-07 21:57:51 +08:00
}
const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) {
activeName = nextTab;
2023-02-15 21:28:01 +08:00
} else {
activeName = '';
}
state.tabs.delete(targetName);
state.activeName = activeName;
onTabChange();
}
};
2023-02-15 21:28:01 +08:00
const onTabChange = () => {
if (!state.activeName) {
state.nowDbInst = {} as DbInst;
state.db = '';
return;
}
const nowTab = state.tabs.get(state.activeName);
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
state.db = nowTab?.db as string;
if (nowTab?.type == TabType.Query) {
// 注册sql提示
registerDbCompletionItemProvider('sql', nowTab.dbId, nowTab.db, nowTab.params.dbs);
}
};
2022-11-07 21:57:51 +08:00
const reloadSqls = (dbId: number, db: string) => {
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
};
2023-11-24 17:03:08 +08:00
const deleteSql = async (dbId: any, db: string, sqlName: string) => {
try {
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
ElMessage.success('删除成功');
reloadSqls(dbId, db);
} catch (err) {
//
}
};
const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu`;
};
2023-04-05 22:41:53 +08:00
const reloadTables = (nodeKey: string) => {
state.reloadStatus = true;
2023-04-05 22:41:53 +08:00
tagTreeRef.value.reloadNode(nodeKey);
};
2023-11-13 17:41:03 +08:00
/**
* 获取当前操作的数据库信息
*/
const getNowDbInfo = () => {
const di = state.nowDbInst;
return {
tagPath: di.tagPath,
id: di.id,
name: di.name,
type: di.type,
host: di.host,
dbName: state.db,
};
};
</script>
2022-03-15 22:27:39 +08:00
<style lang="scss">
.db-sql-exec {
.db-table-size {
color: #c4c9c4;
2023-11-03 17:09:20 +08:00
font-size: 9px;
}
2023-02-06 17:14:16 +08:00
#data-exec {
min-height: calc(100vh - 155px);
2023-02-06 17:14:16 +08:00
2023-11-13 17:41:03 +08:00
.el-tabs {
--el-tabs-header-height: 30px;
}
.el-tabs__header {
2023-11-13 17:41:03 +08:00
margin: 0 0 5px;
.el-tabs__item {
padding: 0 10px;
}
2023-02-06 17:14:16 +08:00
}
2023-11-13 17:41:03 +08:00
.el-tabs__nav-next,
.el-tabs__nav-prev {
line-height: 30px;
}
2023-01-31 10:37:40 +08:00
}
2022-11-22 17:15:08 +08:00
.update_field_active {
background-color: var(--el-color-success);
}
}
</style>