feat: 资源操作相关标签树调整为el-tree

This commit is contained in:
meilin.huang
2023-02-18 23:02:14 +08:00
parent 7b5f963ec4
commit edf29976dd
13 changed files with 928 additions and 852 deletions

View File

@@ -1,225 +1,261 @@
/* 初始化样式
------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
}
html,
body,
#app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
}
/* 主布局样式
------------------------------- */
.layout-container {
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
}
/* element plus 全局样式
------------------------------- */
.layout-breadcrumb-seting {
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
}
/* nprogress 进度条跟随主题颜色
------------------------------- */
#nprogress {
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
}
/* flex 弹性布局
------------------------------- */
.flex {
display: flex;
display: flex;
}
.flex-auto {
flex: 1;
flex: 1;
}
.flex-center {
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
}
.flex-margin {
margin: auto;
margin: auto;
}
.flex-warp {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
}
/* 宽高 100%
------------------------------- */
.w100 {
width: 100% !important;
width: 100% !important;
}
.h100 {
height: 100% !important;
height: 100% !important;
}
.vh100 {
height: 100vh !important;
height: 100vh !important;
}
.max100vh {
max-height: 100vh !important;
max-height: 100vh !important;
}
.min100vh {
min-height: 100vh !important;
min-height: 100vh !important;
}
/* 颜色值
------------------------------- */
.color-primary {
color: var(--color-primary);
color: var(--color-primary);
}
.color-success {
color: var(--color-success);
color: var(--color-success);
}
.color-warning {
color: var(--color-warning);
color: var(--color-warning);
}
.color-danger {
color: var(--color-danger);
color: var(--color-danger);
}
.color-info {
color: var(--color-info);
color: var(--color-info);
}
/* 字体大小全局样式
------------------------------- */
@for $i from 10 through 32 {
.font#{$i} {
font-size: #{$i}px !important;
}
.font#{$i} {
font-size: #{$i}px !important;
}
}
/* 外边距、内边距全局样式
------------------------------- */
@for $i from 1 through 35 {
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
}
@@ -249,15 +285,22 @@ body,
.el-menu .fa:not(.is-children) {
font-size: 14px;
}
.gray-mode{
.gray-mode {
filter: grayscale(100%);
}
.fade-enter-active, .fade-leave-active {
.fade-enter-active,
.fade-leave-active {
transition: opacity .2s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
.fade-enter,
.fade-leave-to
/* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
@@ -270,7 +313,7 @@ body,
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
.toolbar {
width: 100%;
@@ -298,3 +341,13 @@ body,
.f12 {
font-size: 12px
}
.icon-middle {
.el-icon {
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
vertical-align: middle;
}
}

View File

@@ -1,124 +0,0 @@
<template>
<div class="instances-box layout-aside">
<el-row type="flex" justify="space-between">
<el-col :span="24"
:style="{ maxHeight: instanceMenuMaxHeight, height: instanceMenuMaxHeight, overflow: 'auto' }"
class="el-scrollbar flex-auto">
<el-menu background-color="transparent" :collapse-transition="false" ref="menuRef">
<!-- 第一级tag -->
<el-sub-menu v-for="tag of tags" :index="tag.tagPath" :key="tag.tagPath"
@click.stop="clickTag(tag.tagPath)">
<template #title>
<tag-info :tag-path="tag.tagPath" />
<el-icon>
<FolderOpened v-if="opend[tag.tagPath]" color="#e6a23c" />
<Folder v-else />
</el-icon>
<span>{{ tag.tagPath }}</span>
</template>
<slot :tag="tag" name="submenu"></slot>
</el-sub-menu>
</el-menu>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, Ref, toRefs } from 'vue';
import TagInfo from './TagInfo.vue';
const props = defineProps({
instanceMenuMaxHeight: {
type: [Number, String],
},
tags: {
type: Object, required: true
},
})
const menuRef = ref(null) as Ref
const state = reactive({
instanceMenuMaxHeight: props.instanceMenuMaxHeight,
tags: props.tags,
opend: {},
})
const {
opend,
} = toRefs(state)
const clickTag = (tagPath: string) => {
if (state.opend[tagPath] === undefined) {
state.opend[tagPath] = true;
return;
}
const opend = state.opend[tagPath]
state.opend[tagPath] = !opend
}
const open = (index: string, isTag: boolean = false) => {
if (!index) {
return;
}
menuRef.value.open(index)
if (isTag) {
clickTag(index)
}
}
defineExpose({
open
})
</script>
<style lang="scss">
.instances-box {
.el-menu {
width: 100%;
}
.el-sub-menu {
.checked {
.checked-schema {
color: var(--el-color-primary);
}
}
}
.el-sub-menu__title {
padding-left: 0 !important;
height: 30px !important;
line-height: 30px !important;
}
.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-sub-menu__title {
padding-right: 10px;
}
.el-menu-item {
padding-left: 0 !important;
height: 20px !important;
line-height: 20px !important;
}
.el-icon {
margin: 0;
}
.el-sub-menu__icon-arrow {
top: inherit;
right: 10px;
}
}
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="instances-box layout-aside">
<el-row type="flex" justify="space-between">
<el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" />
<el-tree ref="treeRef" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
:highlight-current="true" :indent="7" :load="loadNode" :props="treeProps" lazy node-key="key"
:expand-on-click-node="true" :filter-node-method="filterNode" @node-click="treeNodeClick">
<template #default="{ node, data }">
<span class="icon-middle ">
<span v-if="data.type == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" />
</span>
<slot v-else :node="node" :data="data" name="prefix"></slot>
<span class="ml3">
<slot name="label" :data="data"> {{ data.label }}</slot>
</span>
</span>
</template>
</el-tree>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
import { TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
const props = defineProps({
height: {
type: [Number, String],
default: 0
},
load: {
type: Function,
required: true,
}
})
const treeProps = {
label: 'name',
children: 'zones',
isLeaf: 'isLeaf',
}
const emit = defineEmits(['nodeClick'])
const treeRef: any = ref(null)
const state = reactive({
height: 600 as any,
filterText: '',
opend: {},
})
const { filterText } = toRefs(state)
onMounted(async () => {
if (!props.height) {
state.height = window.innerHeight - 145 + 'px';
} else {
state.height = props.height;
}
})
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
/**
* 加载树节点
* @param { Object } node
* @param { Object } resolve
*/
const loadNode = async (node: any, resolve: any) => {
if (typeof resolve !== 'function') {
return;
}
return resolve(await props.load(node));
};
const treeNodeClick = (data: any) => {
emit('nodeClick', data);
}
const reloadNode = (nodeKey: any) => {
let node = getNode(nodeKey);
node.loaded = false;
node.expand();
}
const getNode = (nodeKey: any) => {
let node = treeRef.value.getNode(nodeKey);
if (!node) {
throw new Error('未找到节点: ' + nodeKey);
}
return node;
}
defineExpose({
reloadNode,
})
</script>
<style lang="scss">
.instances-box {
overflow: 'auto';
.el-tree {
display: inline-block;
min-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,38 @@
export class TagTreeNode {
/**
* 节点id
*/
key: any
/**
* 节点名称
*/
label: string
/**
* 树节点类型
*/
type: any
isLeaf: boolean = false;
params: any;
static TagPath = -1;
constructor(key: any, label: string, type?: any) {
this.key = key;
this.label = label;
this.type = type || TagTreeNode.TagPath;
}
withIsLeaf(isLeaf: boolean) {
this.isLeaf = isLeaf;
return this;
}
withParams(params: any) {
this.params = params;
return this;
}
}

View File

@@ -23,8 +23,51 @@
</el-row>
<el-row type="flex">
<el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px">
<InstanceTree ref="instanceTreeRef" @change-instance="changeInstance" @change-schema="changeSchema"
@clickSqlName="onClickSqlName" @clickSchemaTable="loadTableData" />
<tag-tree ref="tagTreeRef" @node-click="nodeClick" :load="loadNode" :height="state.tagTreeHeight">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.DbInst">
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="类型:">{{ data.params.type }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}:{{
data.params.port
}}</el-form-item>
<el-form-item label="用户:">{{ data.params.username }}</el-form-item>
<el-form-item v-if="data.params.remark" label="备注:">{{
data.params.remark
}}</el-form-item>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Db">
<Coin color="#67c23a" />
</el-icon>
<el-icon v-if="data.type == NodeType.TableMenu">
<Calendar color="#409eff" />
</el-icon>
<el-tooltip v-if="data.type == NodeType.Table" effect="customized"
:content="data.params.tableComment" placement="top-end">
<el-icon>
<Calendar color="#409eff" />
</el-icon>
</el-tooltip>
<el-icon v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql">
<Files color="#f56c6c" />
</el-icon>
</template>
</tag-tree>
</el-col>
<el-col :span="20">
<el-container id="data-exec" style="border-left: 1px solid #eee; margin-top: 10px">
@@ -53,19 +96,33 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { onMounted, reactive, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import * as monaco from 'monaco-editor';
import { editor, languages, Position } from 'monaco-editor';
import InstanceTree from '@/views/ops/db/component/InstanceTree.vue';
import { DbInst, TabInfo, TabType } from './db'
import TableData from './component/tab/TableData.vue'
import Query from './component/tab/Query.vue'
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
const instanceTreeRef = ref(null) as Ref;
/**
* 树节点类型
*/
class NodeType {
static DbInst = 1
static Db = 2
static TableMenu = 3;
static SqlMenu = 4;
static Table = 5;
static Sql = 6;
}
const tagTreeRef: any = ref(null)
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
@@ -74,10 +131,11 @@ const state = reactive({
*/
nowDbInst: {} as DbInst,
db: '', // 当前操作的数据库
activeName: 'Query',
activeName: '',
tabs,
dataTabsTableHeight: '600',
editorHeight: '600',
tagTreeHeight: window.innerHeight - 178 + 'px',
genSqlDialog: {
visible: false,
sql: '',
@@ -99,15 +157,140 @@ onMounted(() => {
* 设置editor高度和数据表高度
*/
const setHeight = () => {
// 默认300px
// state.monacoOptions.height = window.innerHeight - 518 + 'px'
state.editorHeight = window.innerHeight - 518 + 'px';
state.dataTabsTableHeight = window.innerHeight - 219 - 36 + 'px';
state.tagTreeHeight = window.innerHeight - 165 + 'px';
};
// 选择数据库实例
const changeInstance = (inst: any, fn?: Function) => {
fn && fn()
/**
* instmap; tagPaht -> redis info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000, })
if (!res.total) return
for (const db of res.list) {
const tagPath = db.tagPath;
let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(db);
instMap.set(tagPath, redisInsts);
}
}
/**
* 加载树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
const nodeType = data.type;
const params = data.params;
// 点击tagPath -> 加载数据库实例信息列表
if (nodeType === TagTreeNode.TagPath) {
const dbInfos = instMap.get(data.key)
return dbInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x);
});
}
// 点击数据库实例 -> 加载库列表
if (nodeType === NodeType.DbInst) {
const dbs = params.database.split(' ');
return dbs.map((x: any) => {
return new TagTreeNode(`${data.key}.${x}`, x, NodeType.Db).withParams({
tagPath: params.tagPath,
id: params.id,
name: params.name,
type: params.type,
dbs: dbs,
db: x
})
})
}
// 点击数据库 -> 加载 表&Sql 菜单
if (nodeType === NodeType.Db) {
return [new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeType.TableMenu).withParams(params),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params)];
}
// 点击表菜单 -> 加载表列表
if (nodeType === NodeType.TableMenu) {
return await getTables(params);
}
if (nodeType === NodeType.SqlMenu) {
return await loadSqls(params.id, params.db, params.dbs);
}
return [];
};
const nodeClick = async (data: any) => {
const params = data.params;
const nodeKey = data.key;
const dataType = data.type;
// 点击数据库,修改当前数据库信息
if (dataType === NodeType.Db || dataType === NodeType.SqlMenu || dataType === NodeType.TableMenu) {
changeSchema({ id: params.id, name: params.name, type: params.type, tagPath: params.tagPath }, params.db);
return;
}
// 点击表加载表数据tab
if (dataType === NodeType.Table) {
await loadTableData({ id: params.id, nodeKey: nodeKey }, params.db, params.tableName);
return;
}
// 点击表加载表数据tab
if (dataType === NodeType.Sql) {
await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName);
}
}
const getTables = async (params: any) => {
const { id, db } = params;
let tables = await DbInst.getInst(id).loadTables(db);
return tables.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
id,
db,
tableName: x.tableName,
tableComment: x.tableComment,
});
})
}
/**
* 加载用户保存的sql脚本
*
* @param inst
* @param schema
*/
const loadSqls = async (id: any, db: string, dbs: any) => {
const sqls = await dbApi.getSqlNames.request({ id: id, db: db, })
return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({
id,
db,
dbs,
sqlName: x.name,
});
});
}
// 选择数据库
@@ -132,6 +315,7 @@ const loadTableData = async (inst: any, schema: string, tableName: string) => {
}
tab = new TabInfo();
tab.key = label;
tab.treeNodeKey = inst.nodeKey;
tab.dbId = inst.id;
tab.db = schema;
tab.type = TabType.TableData;
@@ -147,6 +331,7 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
ElMessage.warning('请选择数据库实例及对应的schema')
return
}
const dbId = inst.id;
let label;
// 存在sql模板名则该模板名只允许一个tab
@@ -168,12 +353,13 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
}
tab = new TabInfo();
tab.key = label;
tab.treeNodeKey = inst.nodeKey;
tab.dbId = dbId;
tab.db = db;
tab.type = TabType.Query;
tab.params = {
sqlName: sqlName,
dbs: instanceTreeRef.value.getSchemas(dbId)
dbs: inst.dbs,
}
state.tabs.set(label, tab)
registerSqlCompletionItemProvider();
@@ -204,7 +390,9 @@ const onTabChange = () => {
state.db = '';
return;
}
state.nowDbInst = DbInst.getInst(state.tabs.get(state.activeName)?.dbId);
const nowTab = state.tabs.get(state.activeName);
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
state.db = nowTab?.db as string;
}
const onGenerateInsertSql = async (sql: string) => {
@@ -212,19 +400,19 @@ const onGenerateInsertSql = async (sql: string) => {
state.genSqlDialog.visible = true;
};
const onClickSqlName = (inst: any, schema: string, sqlName: string) => {
addQueryTab(inst, schema, sqlName);
}
const reloadSqls = (dbId: number, db: string) => {
instanceTreeRef.value.reloadSqls({ id: dbId }, db);
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
}
const deleteSqlScript = (ti: TabInfo) => {
instanceTreeRef.value.reloadSqls({ id: ti.dbId }, ti.db);
reloadSqls(ti.dbId, ti.db);
onRemoveTab(ti.key);
}
const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu`
}
const registerSqlCompletionItemProvider = () => {
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
self.completionItemProvider = self.completionItemProvider || monaco.languages.registerCompletionItemProvider('sql', {
@@ -288,7 +476,7 @@ const registerSqlCompletionItemProvider = () => {
let str = lastToken.substring(0, lastToken.lastIndexOf('.'))
// 库.表名联想
if (dbs.filter((a: any) => a.name === str)?.length > 0) {
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
let tables = await dbInst.loadTables(str)
let suggestions: languages.CompletionItem[] = []
for (let item of tables) {
@@ -390,17 +578,19 @@ const registerSqlCompletionItemProvider = () => {
})
// 库名提示
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a.name,
description: 'schema'
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a.name,
range
});
})
if (dbs) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema'
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a,
range
});
})
}
const tables = await dbInst.loadTables(db);
// 表名联想
@@ -518,4 +708,10 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(
.update_field_active {
background-color: var(--el-color-success)
}
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -1,346 +0,0 @@
<template>
<tag-menu :instanceMenuMaxHeight="instanceMenuMaxHeight" :tags="tags" ref="menuRef">
<template #submenu="props">
<!-- 第二级数据库实例 -->
<el-sub-menu v-for="inst in tree[props.tag.tagId]" :index="'instance-' + inst.id"
:key="'instance-' + inst.id" @click.stop="changeInstance(inst, () => { })">
<template #title>
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
<template #reference>
<span class="ml10">
<el-icon>
<MostlyCloudy color="#409eff" />
</el-icon>{{ inst.name }}
</span>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="类型:">{{ inst.type }}</el-form-item>
<el-form-item label="链接:">{{ inst.host }}:{{ inst.port }}</el-form-item>
<el-form-item label="用户:">{{ inst.username }}</el-form-item>
<el-form-item v-if="inst.remark" label="备注:">{{ inst.remark }}</el-form-item>
</el-form>
</template>
</el-popover>
</template>
<el-menu-item v-if="dbs[inst.id]?.length > 20" :index="'schema-filter-' + inst.id"
:key="'schema-filter-' + inst.id">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<el-input size="small" placeholder="过滤数据库" clearable @change="filterSchemaName(inst.id)"
@keyup="(e: any) => filterSchemaName(inst.id, e)"
v-model="state.schemaFilterParam[inst.id]" />
</template>
</el-menu-item>
<!-- 第三级数据库 -->
<el-sub-menu v-show="schema.show" v-for="schema in dbs[inst.id]" :index="inst.id + schema.name"
:key="inst.id + schema.name" :class="state.nowSchema === (inst.id + schema.name) && 'checked'"
@click.stop="changeSchema(inst, schema.name)">
<template #title>
<span class="checked-schema ml20">
<el-icon>
<Coin color="#67c23a" />
</el-icon>
<span v-html="schema.showName || schema.name"></span>
</span>
</template>
<!-- 第四级 01 -->
<el-sub-menu :index="inst.id + schema.name + '-table'">
<template #title>
<div class="ml30" style="width: 100%" @click="loadSchemaTables(inst, schema.name)">
<el-icon>
<Calendar color="#409eff" />
</el-icon>
<span></span>
<el-icon v-show="state.loading[inst.id + schema.name]" class="is-loading">
<Loading />
</el-icon>
</div>
</template>
<el-menu-item v-if="tables[inst.id + schema.name]?.length > 20"
:index="inst.id + schema.name + '-tableSearch'"
:key="inst.id + schema.name + '-tableSearch'">
<template #title>
<span class="ml35">
<el-input size="small" placeholder="表名、备注过滤表" clearable
@change="filterTableName(inst.id, schema.name)"
@keyup="(e: any) => filterTableName(inst.id, schema.name, e)"
v-model="state.filterParam[inst.id + schema.name]" />
</span>
</template>
</el-menu-item>
<template v-for="tb in tables[inst.id + schema.name]">
<el-menu-item :index="inst.id + schema.name + tb.tableName"
:key="inst.id + schema.name + tb.tableName" v-if="tb.show"
@click="clickSchemaTable(inst, schema.name, tb.tableName)">
<template #title>
<div class="ml35" style="width: 100%">
<el-icon>
<Calendar color="#409eff" />
</el-icon>
<el-tooltip v-if="tb.tableComment" effect="customized"
:content="tb.tableComment" placement="right">
<span v-html="tb.showName || tb.tableName"></span>
</el-tooltip>
<span v-else v-html="tb.showName || tb.tableName"></span>
</div>
</template>
</el-menu-item>
</template>
</el-sub-menu>
<!-- 第四级 02sql -->
<el-sub-menu @click.stop="loadSqls(inst, schema.name)" :index="inst.id + schema.name + '-sql'">
<template #title>
<span class="ml30">
<el-icon>
<List color="#f56c6c" />
</el-icon>
<span>sql</span>
</span>
</template>
<template v-for="sql in sqls[inst.id + schema.name]">
<el-menu-item v-if="sql.show" :index="inst.id + schema.name + sql.name"
:key="inst.id + schema.name + sql.name"
@click="clickSqlName(inst, schema.name, sql.name)">
<template #title>
<div class="ml35" style="width: 100%">
<el-icon>
<Document />
</el-icon>
<span>{{ sql.name }}</span>
</div>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</el-sub-menu>
</el-sub-menu>
</template>
</tag-menu>
</template>
<script lang="ts" setup>
import { onBeforeMount, reactive, toRefs } from 'vue';
import TagMenu from '../../component/TagMenu.vue';
import { dbApi } from '../api';
import { DbInst } from '../db';
const emits = defineEmits(['changeInstance', 'clickSqlName', 'clickSchemaTable', 'changeSchema', 'loadSqlNames'])
onBeforeMount(async () => {
await loadInstances();
state.instanceMenuMaxHeight = window.innerHeight - 140 + 'px';
})
const state = reactive({
tags: {},
tree: {},
dbs: {},
tables: {},
sqls: {},
nowSchema: '',
filterParam: {},
schemaFilterParam: {},
loading: {},
instanceMenuMaxHeight: '850px',
})
const {
instanceMenuMaxHeight,
tags,
tree,
dbs,
sqls,
tables,
} = toRefs(state)
// 加载实例数据
const loadInstances = async () => {
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000, })
if (!res.total) return
// state.instances = { tags: {}, tree: {}, dbs: {}, tables: {}, sqls: {} }; // 初始化变量
for (const db of res.list) {
let arr = state.tree[db.tagId] || []
const { tagId, tagPath } = db
// tags
state.tags[db.tagId] = { tagId, tagPath }
// tree
arr.push(db)
state.tree[db.tagId] = arr;
// dbs
let databases = db.database.split(' ')
let dbs = [] as any[];
databases.forEach((a: string) => dbs.push({ name: a, show: true }))
state.dbs[db.id] = dbs
}
}
/**
* 改变选中的数据库实例
* @param inst 选中的实例对象
* @param fn 选中的实例对象后的回调函数
*/
const changeInstance = (inst: any, fn: Function) => {
emits('changeInstance', inst, fn)
}
/**
* 改变选中的数据库schema
* @param inst 选中的实例对象
* @param schema 选中的数据库schema
*/
const changeSchema = (inst: any, schema: string) => {
state.nowSchema = inst.id + schema
emits('changeSchema', inst, schema)
}
/** 加载schema下所有表
*
* @param inst 数据库实例
* @param schema database名
*/
const loadSchemaTables = async (inst: any, schema: string) => {
const key = getSchemaKey(inst.id, schema);
state.loading[key] = true
try {
let { id } = inst
let tables = await DbInst.getInst(id).loadTables(schema);
tables && tables.forEach((a: any) => a.show = true)
state.tables[key] = tables;
changeSchema(inst, schema);
} finally {
state.loading[key] = false
}
}
/**
* 加载选中表数据
* @param inst 数据库实例
* @param schema database名
* @param tableName 表名
*/
const clickSchemaTable = (inst: any, schema: string, tableName: string) => {
emits('clickSchemaTable', inst, schema, tableName)
}
const filterTableName = (instId: number, schema: string, event?: any) => {
const key = getSchemaKey(instId, schema)
if (event) {
state.filterParam[key] = event.target.value
}
let param = state.filterParam[key] as string
state.tables[key].forEach((a: any) => {
let { match, showName } = matchAndHighLight(param, a.tableName + a.tableComment, a.tableName)
a.show = match;
a.showName = showName
})
}
const filterSchemaName = (instId: number, event?: any) => {
if (event) {
state.schemaFilterParam[instId] = event.target.value
}
let param = state.schemaFilterParam[instId] as string
param = param?.replace('/', '\/')
state.dbs[instId].forEach((a: any) => {
let { match, showName } = matchAndHighLight(param, a.name, a.name)
a.show = match
a.showName = showName
})
}
const matchAndHighLight = (searchParam: string, param: string, title: string): { match: boolean, showName: string } => {
if (!searchParam) {
return { match: true, showName: '' }
}
let str = '';
for (let c of searchParam?.replace('/', '\/')) {
str += `(${c}).*`
}
let regex = eval(`/${str}/i`)
let res = param.match(regex);
if (res?.length) {
if (res?.length) {
let tmp = '', showName = '';
for (let i = 1; i <= res.length - 1; i++) {
let head = (tmp || title).replace(res[i], `###${res[i]}!!!`);
let idx = head.lastIndexOf('!!!') + 3;
tmp = head.substring(idx);
showName += head.substring(0, idx)
if (!tmp) {
break
}
}
showName += tmp;
showName = showName.replaceAll('###', '<span style="color: red">')
showName = showName.replaceAll('!!!', '</span>')
return { match: true, showName }
}
}
return { match: false, showName: '' }
}
/**
* 加载用户保存的sql脚本
*
* @param inst
* @param schema
*/
const loadSqls = async (inst: any, schema: string) => {
const key = getSchemaKey(inst.id, schema)
let sqls = state.sqls[key];
if (!sqls) {
const sqls = await dbApi.getSqlNames.request({ id: inst.id, db: schema, })
sqls && sqls.forEach((a: any) => a.show = true)
state.sqls[key] = sqls;
} else {
sqls.forEach((a: any) => a.show = true);
}
}
const reloadSqls = async (inst: any, schema: string) => {
const sqls = await dbApi.getSqlNames.request({ id: inst.id, db: schema, })
sqls && sqls.forEach((a: any) => a.show = true)
state.sqls[getSchemaKey(inst.id, schema)] = sqls;
}
/**
* 点击sql模板名称时间加载用户保存的指定名称的sql内容并回调子组件指定事件
*/
const clickSqlName = async (inst: any, schema: string, sqlName: string) => {
emits('clickSqlName', inst, schema, sqlName)
changeSchema(inst, schema);
}
/**
* 根据实例以及库获取对应的唯一id
*
* @param inst 数据库实例
* @param schema 数据库
*/
const getSchemaKey = (instId: any, schema: string) => {
return instId + schema;
}
const getSchemas = (dbId: any) => {
return state.dbs[dbId] || []
}
defineExpose({
getSchemas,
reloadSqls,
})
</script>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -411,6 +411,11 @@ export class TabInfo {
*/
key: string
/**
* 菜单树节点key
*/
treeNodeKey: string
/**
* 数据库实例id
*/

View File

@@ -446,7 +446,6 @@ const showCreateFileDialog = (node: any) => {
const createFile = async () => {
const node = state.createFileDialog.node;
console.log(node.data);
const name = state.createFileDialog.name;
const type = state.createFileDialog.type;
const path = node.data.path + '/' + name;

View File

@@ -2,12 +2,50 @@
<div>
<el-row>
<el-col :span="4">
<mongo-instance-tree @init-load-instances="loadInstances" @change-instance="changeInstance"
@load-table-names="loadTableNames" @load-table-data="changeCollection"
:instances="state.instances" />
<tag-tree @node-click="nodeClick" :load="loadNode">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.Mongo">
<el-popover placement="right-start" title="mongo实例信息" trigger="hover" :width="210">
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="链接:">{{ data.params.uri }}</el-form-item>
</el-form>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Dbs">
<Coin color="#67c23a" />
</el-icon>
<el-icon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu">
<Document class="color-primary" />
</el-icon>
</template>
<template #label="{ data }">
<span v-if="data.type == NodeType.Dbs">
{{ data.params.dbName }}
<span style="color: #8492a6;font-size: 13px">
[{{ formatByteSize(data.params.size) }}]
</span>
</span>
<span v-else>{{ data.label }}</span>
</template>
</tag-tree>
</el-col>
<el-col :span="20">
<el-container id="data-exec" style="border: 1px solid #eee; margin-top: 1px">
<el-container id="mongo-tab" style="border: 1px solid #eee; margin-top: 1px">
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px"
v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label"
@@ -67,8 +105,8 @@
<el-dialog width="600px" title="find参数" v-model="findDialog.visible">
<el-form label-width="70px">
<el-form-item label="filter">
<el-input v-model="findDialog.findParam.filter" type="textarea" :rows="6" clearable
auto-complete="off"></el-input>
<monaco-editor style="width: 100%;" height="150px" ref="monacoEditorRef"
v-model="findDialog.findParam.filter" language="json" />
</el-form-item>
<el-form-item label="sort">
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable
@@ -116,7 +154,19 @@ import { ElMessage } from 'element-plus';
import { isTrue, notBlank } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import MongoInstanceTree from '@/views/ops/mongo/MongoInstanceTree.vue';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format';
/**
* 树节点类型
*/
class NodeType {
static Mongo = 1
static Dbs = 2
static CollMenu = 3
static Coll = 4
}
const findParamInputRef: any = ref(null);
const state = reactive({
@@ -142,7 +192,6 @@ const state = reactive({
doc: '',
item: {} as any,
},
instances: { tags: {}, tree: {}, dbs: {}, tables: {} }
});
const {
@@ -151,28 +200,110 @@ const {
jsonEditorDialog,
} = toRefs(state)
const changeInstance = async (inst: any, fn: Function) => {
if (inst) {
if (!state.instances.dbs[inst.id]) {
const res = await mongoApi.databases.request({ id: inst.id });
state.instances.dbs[inst.id] = res.Databases;
fn && fn(res.Databases)
/**
* instmap; tagPaht -> mongo info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, });
if (!res.total) return
for (const mongoInfo of res.list) {
const tagPath = mongoInfo.tagPath;
let mongoInsts = instMap.get(tagPath) || [];
mongoInsts.push(mongoInfo);
instMap.set(tagPath, mongoInsts);
}
}
/**
* 加载文件树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
const params = data.params;
const nodeType = data.type;
// 点击标签 -> 显示mongo信息列表
if (nodeType === TagTreeNode.TagPath) {
const mongoInfos = instMap.get(data.key)
return mongoInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x);
});
}
// 点击mongo -> 加载mongo数据库列表
if (nodeType === NodeType.Mongo) {
return await getDatabases(params);
}
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
if (nodeType === NodeType.Dbs) {
return [new TagTreeNode(`${params.id}.${params.dbName}.mongo-coll`, '集合', NodeType.CollMenu).withParams(params)];
}
// 点击数据库集合节点 -> 加载集合列表
if (nodeType === NodeType.CollMenu) {
return await getCollections(params.id, params.dbName)
}
return [];
};
/**
* 获取实例的所有库信息
* @param inst 实例信息
*/
const getDatabases = async (inst: any) => {
const res = await mongoApi.databases.request({ id: inst.id });
return res.Databases.map((x: any) => {
const dbName = x.Name;
return new TagTreeNode(`${inst.id}.${dbName}`, dbName, NodeType.Dbs).withParams({
id: inst.id,
dbName,
size: x.SizeOnDisk,
})
})
}
/**
* 获取集合列表信息
* @param inst
*/
const getCollections = async (id: any, database: string) => {
const colls = await mongoApi.collections.request({ id, database });
return colls.map((x: any) => {
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeType.Coll).withIsLeaf(true).withParams({
id,
database,
collection: x,
});
});
}
const nodeClick = (data: any) => {
// 点击集合
if (data.type === NodeType.Coll) {
const { id, database, collection } = data.params;
changeCollection(id, database, collection);
}
}
const loadTableNames = async (inst: any, database: string, fn: Function) => {
let tbs = await mongoApi.collections.request({ id: inst.id, database });
let tables = [];
for (let tb of tbs) {
tables.push({ tableName: tb, show: true })
}
state.instances.tables[inst.id + database] = tables
fn(tables)
}
const changeCollection = (inst: any, schema: string, collection: string) => {
const label = `${inst.id}:\`${schema}\`.${collection}`;
const changeCollection = (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label];
if (!dataTab) {
// 默认查询参数
@@ -186,7 +317,7 @@ const changeCollection = (inst: any, schema: string, collection: string) => {
key: label,
label: label,
name: label,
mongoId: inst.id,
mongoId: id,
database: schema,
collection,
datas: [],
@@ -359,28 +490,13 @@ const removeDataTab = (targetName: string) => {
delete state.dataTabs[targetName];
};
const loadInstances = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, });
if (!res.total) return
state.instances = { tags: {}, tree: {}, dbs: {}, tables: {} }; // 初始化变量
for (const db of res.list) {
let arr = state.instances.tree[db.tagId] || []
const { tagId, tagPath } = db
// tags
state.instances.tags[db.tagId] = { tagId, tagPath }
// 实例
arr.push(db)
state.instances.tree[db.tagId] = arr;
}
}
const getNowDataTab = () => {
return state.dataTabs[state.activeName]
}
</script>
<style>
<style lang="scss">
.mongo-doc-btns {
position: absolute;
z-index: 2;
@@ -388,4 +504,14 @@ const getNowDataTab = () => {
top: 2px;
max-width: 120px;
}
#mongo-tab {
.el-tabs__header {
margin: 0 0 5px;
.el-tabs__item {
padding: 0 5px;
}
}
}
</style>

View File

@@ -2,9 +2,38 @@
<div>
<el-row>
<el-col :span="4">
<redis-instance-tree @init-load-instances="initLoadInstances" @change-instance="changeInstance"
@change-schema="loadInitSchema" :instances="state.instances" />
<el-row type="flex" justify="space-between">
<el-col :span="24" class="el-scrollbar flex-auto">
<tag-tree @node-click="nodeClick" :load="loadNode">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.Redis">
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
<el-form-item label="备注:">{{
data.params.remark
}}</el-form-item>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Db">
<Coin color="#67c23a" />
</el-icon>
</template>
</tag-tree>
</el-col>
</el-row>
</el-col>
<el-col :span="20" style="border-left: 1px solid var(--el-card-border-color);">
<div class="mt10 ml5">
<el-col>
@@ -87,17 +116,26 @@
<script lang="ts" setup>
import { redisApi } from './api';
import { toRefs, reactive } from 'vue';
import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import HashValue from './HashValue.vue';
import StringValue from './StringValue.vue';
import SetValue from './SetValue.vue';
import ListValue from './ListValue.vue';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import RedisInstanceTree from '@/views/ops/redis/RedisInstanceTree.vue';
/**
* 树节点类型
*/
class NodeType {
static Redis = 1
static Db = 2
}
const state = reactive({
instanceMenuMaxHeight: '600',
loading: false,
tags: [],
redisList: [] as any,
@@ -137,7 +175,6 @@ const state = reactive({
},
keys: [],
dbsize: 0,
instances: { tags: {}, tree: {}, dbs: {}, tables: {} }
});
const {
@@ -149,6 +186,103 @@ const {
listValueDialog,
} = toRefs(state)
onMounted(async () => {
setHeight();
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
/**
* instmap; tagPaht -> redis info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await redisApi.redisList.request({});
if (!res.total) return
for (const redisInfo of res.list) {
const tagPath = redisInfo.tagPath;
let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(redisInfo);
instMap.set(tagPath, redisInsts);
}
}
/**
* 加载文件树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
// 点击tagPath -> 加载数据库信息列表
if (data.type === TagTreeNode.TagPath) {
const redisInfos = instMap.get(data.key)
return redisInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x);
});
}
// 点击redis实例 -> 加载库列表
if (data.type === NodeType.Redis) {
return await getDbs(data.params);
}
return [];
};
const nodeClick = (data: any) => {
// 点击库事件
if (data.type === NodeType.Db) {
resetScanParam();
state.scanParam.id = data.params.id;
state.scanParam.db = data.params.db;
scan();
}
}
/**
* 获取所有库信息
* @param redisInfo redis信息
*/
const getDbs = async (redisInfo: any) => {
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, x, NodeType.Db).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
})
})
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: "Keyspace" });
for (let db in res.Keyspace) {
for (let d of dbs) {
if (db == d.params.name) {
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0
}
}
}
// 替换label
dbs.forEach((e: any) => {
e.label = `${e.params.name} [${e.params.keys}]`
});
return dbs;
}
const scan = async () => {
isTrue(state.scanParam.id != null, '请先选择redis');
notBlank(state.scanParam.count, 'count不能为空');
@@ -312,49 +446,12 @@ const getTypeColor = (type: string) => {
return '#A8DEE0';
}
};
const initLoadInstances = async () => {
const res = await redisApi.redisList.request({});
if (!res.total) return
state.instances = { tags: {}, tree: {}, dbs: {}, tables: {} }; // 初始化变量
for (const db of res.list) {
let arr = state.instances.tree[db.tagId] || []
const { tagId, tagPath } = db
// tags
state.instances.tags[db.tagId] = { tagId, tagPath }
// 实例
arr.push(db)
state.instances.tree[db.tagId] = arr;
}
}
const changeInstance = async (inst: any, fn: Function) => {
let dbs = inst.db.split(',').map((x: string) => {
return { name: `db${x}`, keys: 0 }
})
const res = await redisApi.redisInfo.request({ id: inst.id, host: inst.host, section: "Keyspace" });
for (let db in res.Keyspace) {
for (let d of dbs) {
if (db == d.name) {
d.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0
}
}
}
state.instances.dbs[inst.id] = dbs
fn && fn(dbs)
}
/** 初始化加载db数据 */
const loadInitSchema = (inst: any, schema: string) => {
state.scanParam.id = inst.id
state.scanParam.db = schema.replace('db', '')
scan()
}
</script>
<style>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -32,6 +32,13 @@
</el-popover>
</template></el-input>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-select @change="changeDb" v-model="dbList" multiple allow-create filterable
placeholder="请选择可操作库号" style="width: 100%">
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db"
:label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>

View File

@@ -1,100 +0,0 @@
<template>
<tag-menu :instanceMenuMaxHeight="state.instanceMenuMaxHeight" :tags="instances.tags" ref="menuRef">
<template #submenu="props">
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'redis-' + inst.id"
:key="'redis-' + inst.id" @click.stop="changeInstance(inst)">
<template #title>
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
<template #reference>
<span>&nbsp;&nbsp;<el-icon>
<MostlyCloudy color="#409eff" />
</el-icon>{{ inst.name }}</span>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ inst.name }}</el-form-item>
<el-form-item label="链接:">{{ inst.host }}</el-form-item>
<el-form-item label="备注:">{{ inst.remark }}</el-form-item>
</el-form>
</template>
</el-popover>
</template>
<!-- 第三级数据库 -->
<el-menu-item v-for="db in instances.dbs[inst.id]" :index="inst.id + db.name" :key="inst.id + db.name"
:class="state.nowSchema === (inst.id + db.name) && 'checked'" @click="changeSchema(inst, db.name)">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Coin color="#67c23a" />
</el-icon>
<span class="checked-schema">
{{ db.name }} [{{ db.keys }}]
</span>
</template>
</el-menu-item>
</el-sub-menu>
</template>
</tag-menu>
</template>
<script lang="ts" setup>
import { onBeforeMount, reactive, Ref, ref } from 'vue';
import TagMenu from '../component/TagMenu.vue';
defineProps({
instances: {
type: Object, required: true
},
})
const emits = defineEmits(['initLoadInstances', 'changeInstance', 'changeSchema'])
onBeforeMount(async () => {
await initLoadInstances();
setHeight();
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
const state = reactive({
instanceMenuMaxHeight: '800px',
nowSchema: '',
filterParam: {},
loading: {},
})
/**
* 初始化加载实例数据
*/
const initLoadInstances = () => {
emits('initLoadInstances')
}
/**
* 改变选中的数据库实例
* @param inst 选中的实例对象
* @param fn 选中的实例后的回调函数
*/
const changeInstance = (inst: any, fn?: Function) => {
emits('changeInstance', inst, fn);
}
/**
* 改变选中的数据库schema
* @param inst 选中的实例对象
* @param schema 选中的数据库schema
*/
const changeSchema = (inst: any, schema: string) => {
state.nowSchema = inst.id + schema;
emits('changeSchema', inst, schema);
}
</script>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -155,12 +155,15 @@ func doSelect(selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExe
selectExprsStr := sqlparser.String(selectStmt.SelectExprs)
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
len(strings.Split(selectExprsStr, ",")) > 1 {
limit := selectStmt.Limit
biz.NotNil(limit, "请完善分页信息后执行")
count, err := strconv.Atoi(sqlparser.String(limit.Rowcount))
biz.ErrIsNil(err, "分页参数有误")
// 如果配置为0则不校验分页参数
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
biz.IsTrue(count <= maxCount, fmt.Sprintf("查询结果集数需小于系统配置的%d条", maxCount))
if maxCount != 0 {
limit := selectStmt.Limit
biz.NotNil(limit, "请完善分页信息后执行")
count, err := strconv.Atoi(sqlparser.String(limit.Rowcount))
biz.ErrIsNil(err, "分页参数有误")
biz.IsTrue(count <= maxCount, fmt.Sprintf("查询结果集数需小于系统配置的%d条", maxCount))
}
}
return doRead(execSqlReq)