feat: 常用操作界面支持Splitpane等

This commit is contained in:
meilin.huang
2023-12-07 23:57:11 +08:00
parent 172c729535
commit 59a7ff9ac7
13 changed files with 382 additions and 329 deletions

View File

@@ -29,6 +29,7 @@
"qrcode.vue": "^3.4.1",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.0",
"splitpanes": "^3.1.5",
"sql-formatter": "^14.0.0",
"uuid": "^9.0.1",
"vue": "^3.3.10",

View File

@@ -1,5 +1,5 @@
<template>
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5)">
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>

View File

@@ -13,6 +13,8 @@ import 'element-plus/theme-chalk/dark/css-vars.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { ElMessage } from 'element-plus';
import 'splitpanes/dist/splitpanes.css';
import '@/theme/index.scss';
import '@/assets/font/font.css';
import '@/assets/iconfont/iconfont.js';

View File

@@ -8,21 +8,21 @@
}
:root {
--color-white: #ffffff;
--bg-main-color: #f8f8f8;
--bg-color: #f5f5ff;
--bg-menuBarActiveColor: #0000000a; // 菜单栏激活时的背景色
--border-color-light: #f1f2f3;
--el-color-primary-lighter: #ecf5ff;
--color-success-lighter: #f0f9eb;
--color-warning-lighter: #fdf6ec;
--color-danger-lighter: #fef0f0;
--color-dark-hover: #0000001a;
--color-menu-hover: rgba(0, 0, 0, 0.2);
--color-user-hover: rgba(0, 0, 0, 0.04);
--color-seting-main: #e9eef3;
--color-seting-aside: #d3dce6;
--color-seting-header: #b3c0d1;
--color-white: #ffffff;
--bg-main-color: #f8f8f8;
--bg-color: #f5f5ff;
--bg-menuBarActiveColor: #0000000a; // 菜单栏激活时的背景色
--border-color-light: #f1f2f3;
--el-color-primary-lighter: #ecf5ff;
--color-success-lighter: #f0f9eb;
--color-warning-lighter: #fdf6ec;
--color-danger-lighter: #fef0f0;
--color-dark-hover: #0000001a;
--color-menu-hover: rgba(0, 0, 0, 0.2);
--color-user-hover: rgba(0, 0, 0, 0.04);
--color-seting-main: #e9eef3;
--color-seting-aside: #d3dce6;
--color-seting-header: #b3c0d1;
--tagsview3-active-background-color: var(--el-color-primary-light-9);
}
@@ -373,6 +373,45 @@ body,
cursor: pointer;
transition: color 0.3s;
}
.pointer-icon:hover {
color: var(--el-color-primary); /* 鼠标移动到图标时的颜色 */
color: var(--el-color-primary);
/* 鼠标移动到图标时的颜色 */
}
/** splitpanes **/
.splitpanes.default-theme .splitpanes {
background: transparent !important;
}
.splitpanes.default-theme .splitpanes__pane {
background: transparent !important;
}
.default-theme.splitpanes--vertical>.splitpanes__splitter {
border-left: 1px solid var(--bg-main-color);
}
.default-theme.splitpanes--horizontal>.splitpanes__splitter {
border-top: 1px solid var(--bg-main-color);
}
// 竖线样式
.splitpanes.default-theme .splitpanes__splitter::before,
.splitpanes.default-theme .splitpanes__splitter::after {
background-color: var(--el-color-info-light-5);
}
.splitpanes.default-theme .splitpanes__splitter:hover::before,
.splitpanes.default-theme .splitpanes__splitter:hover::after {
background-color: var(--el-color-success);
}
.splitpanes.default-theme .splitpanes__splitter {
min-width: 6px;
background: var(--el-color-info-light-8) !important;
}
.splitpanes.default-theme .splitpanes__splitter:hover {
background: var(--el-color-success-light-8) !important;
}

View File

@@ -3,3 +3,4 @@ declare module 'sql-formatter';
declare module 'jsoneditor';
declare module 'asciinema-player';
declare module 'vue-grid-layout';
declare module 'splitpanes';

View File

@@ -98,7 +98,7 @@ onMounted(async () => {
});
const setHeight = () => {
state.height = vh.value - 148 + 'px';
state.height = vh.value - 150 + 'px';
};
onUnmounted(() => {

View File

@@ -1,7 +1,7 @@
<template>
<div class="db-sql-exec">
<el-row>
<el-col :span="5">
<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 }">
<span v-if="data.type.value == SqlExecNodeType.DbInst">
@@ -44,9 +44,9 @@
}}</span>
</template>
</tag-tree>
</el-col>
</Pane>
<el-col :span="19">
<Pane>
<el-row>
<el-col :span="24" v-if="state.db">
<el-descriptions :column="4" size="small" border class="ml5">
@@ -90,8 +90,9 @@
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeName"
class="h100"
>
<el-tab-pane closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<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-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference> {{ dt.label }} </template>
@@ -129,7 +130,6 @@
:db-name="dt.db"
:sql-name="dt.params.sqlName"
@save-sql-success="reloadSqls"
:editor-height="state.editorHeight"
>
</db-sql-editor>
@@ -143,8 +143,8 @@
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</Pane>
</Splitpanes>
</div>
</template>
@@ -162,6 +162,7 @@ import { ContextmenuItem } from '@/components/contextmenu';
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'));
@@ -377,8 +378,7 @@ const state = reactive({
activeName: '',
reloadStatus: false,
tabs,
dataTabsTableHeight: 600,
editorHeight: '600',
dataTabsTableHeight: '600px',
tablesOpHeight: '600',
});
@@ -398,8 +398,7 @@ onBeforeUnmount(() => {
* 设置editor高度和数据表高度
*/
const setHeight = () => {
state.editorHeight = window.innerHeight - 525 + 'px';
state.dataTabsTableHeight = window.innerHeight - 255;
state.dataTabsTableHeight = window.innerHeight - 255 + 'px';
state.tablesOpHeight = window.innerHeight - 220 + 'px';
};

View File

@@ -39,86 +39,96 @@
</div>
</div>
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="state.editorHeight" :id="'MonacoTextarea-' + getKey()" />
<Splitpanes
@pane-maximize="resizeTableHeight([{ size: 0 }])"
@resize="resizeTableHeight"
horizontal
class="default-theme"
style="height: calc(100vh - 225px)"
>
<Pane :size="state.editorSize" max-size="80">
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
</Pane>
<div class="editor-move-resize" @mousedown="onDragSetHeight">
<el-icon>
<Minus />
</el-icon>
</div>
<Pane :size="100 - state.editorSize">
<div class="mt5 sql-exec-res h100">
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab">
<el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label>
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
<template #reference>
<div>
<span>
<span v-if="dt.loading">
<SvgIcon class="mb2 is-loading" name="Loading" color="var(--el-color-primary)" />
</span>
<span v-else>
<SvgIcon class="mb2" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
<SvgIcon class="mb2" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
</span>
</span>
<div class="mt5 sql-exec-res">
<el-tabs v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTab">
<el-tab-pane closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label>
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
<template #reference>
<div>
<span>
<span v-if="dt.loading">
<SvgIcon class="mb2 is-loading" name="Loading" color="var(--el-color-primary)" />
</span>
<span v-else>
<SvgIcon class="mb2" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
<SvgIcon class="mb2" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
</span>
</span>
<span> 结果{{ dt.id }} </span>
</div>
</template>
<template #default>
<el-descriptions v-if="dt.sql" :column="1" size="small">
<el-descriptions-item>
<div style="width: 280px">
<el-text size="small" truncated :title="dt.sql"> {{ dt.sql }} </el-text>
<span> 结果{{ dt.id }} </span>
</div>
</el-descriptions-item>
<el-descriptions-item label="耗时 :"> {{ dt.execTime }}ms </el-descriptions-item>
<el-descriptions-item label="结果集 :">
{{ dt.data?.length }}
</el-descriptions-item>
</el-descriptions>
</template>
<template #default>
<el-descriptions v-if="dt.sql" :column="1" size="small">
<el-descriptions-item>
<div style="width: 280px">
<el-text size="small" truncated :title="dt.sql"> {{ dt.sql }} </el-text>
</div>
</el-descriptions-item>
<el-descriptions-item label="耗时 :"> {{ dt.execTime }}ms </el-descriptions-item>
<el-descriptions-item label="结果集 :">
{{ dt.data?.length }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</template>
</el-popover>
</template>
<el-row>
<span v-if="dt.hasUpdatedFileds" class="mt5">
<span>
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"><span style="font-size: 12px">提交</span></el-link>
</span>
<span>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"><span style="font-size: 12px">取消</span></el-link>
</span>
</span>
</el-row>
<db-table-data
v-if="!dt.errorMsg"
:ref="(el) => (dt.dbTableRef = el)"
:db-id="dbId"
:db="dbName"
:data="dt.data"
:table="dt.table"
:columns="dt.tableColumn"
:loading="dt.loading"
:loading-key="dt.loadingKey"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@change-updated-field="changeUpdatedField($event, dt)"
@data-delete="onDeleteData($event, dt)"
></db-table-data>
<el-row>
<span v-if="dt.hasUpdatedFileds" class="mt5">
<span>
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"
><span style="font-size: 12px">提交</span></el-link
>
</span>
<span>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"
><span style="font-size: 12px">取消</span></el-link
>
</span>
</span>
</el-row>
<db-table-data
v-if="!dt.errorMsg"
:ref="(el) => (dt.dbTableRef = el)"
:db-id="dbId"
:db="dbName"
:data="dt.data"
:table="dt.table"
:columns="dt.tableColumn"
:loading="dt.loading"
:loading-key="dt.loadingKey"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@change-updated-field="changeUpdatedField($event, dt)"
@data-delete="onDeleteData($event, dt)"
></db-table-data>
<el-result v-else icon="error" title="执行失败" :sub-title="dt.errorMsg"> </el-result>
</el-tab-pane>
</el-tabs>
</div>
<el-result v-else icon="error" title="执行失败" :sub-title="dt.errorMsg"> </el-result>
</el-tab-pane>
</el-tabs>
</div>
</Pane>
</Splitpanes>
</div>
</template>
<script lang="ts" setup>
import { h, nextTick, watch, onMounted, reactive, toRefs, ref } from 'vue';
import { h, nextTick, onMounted, reactive, toRefs, ref } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
@@ -141,6 +151,7 @@ import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from '../../dialect';
import { randomUuid } from '@/common/utils/string';
import { Splitpanes, Pane } from 'splitpanes';
const emits = defineEmits(['saveSqlSuccess']);
@@ -157,10 +168,6 @@ const props = defineProps({
sqlName: {
type: String,
},
editorHeight: {
type: String,
default: '600',
},
});
class ExecResTab {
@@ -206,31 +213,30 @@ const monacoEditorRef: any = ref(null);
let monacoEditor: editor.IStandaloneCodeEditor;
const state = reactive({
editorSize: 50, // editor高度比例
token,
sql: '', // 当前编辑器的sql内容s
sqlName: '' as any, // sql模板名称
execResTabs: [] as ExecResTab[],
activeTab: 1,
editorHeight: '500',
tableDataHeight: 255 as any,
tableDataHeight: '250px',
});
const { tableDataHeight } = toRefs(state);
watch(
() => props.editorHeight,
(newValue: any) => {
state.editorHeight = newValue;
}
);
const getNowDbInst = () => {
return DbInst.getInst(props.dbId);
};
onMounted(async () => {
console.log('in query mounted');
state.editorHeight = props.editorHeight;
// 第一个pane为sql editor
resizeTableHeight([{ size: state.editorSize }]);
window.onresize = () => {
resizeTableHeight([{ size: state.editorSize }]);
};
// 默认新建一个结果集tab
state.execResTabs.push(new ExecResTab(1));
@@ -265,19 +271,11 @@ const onRemoveTab = (targetId: number) => {
}
};
/**
* 拖拽改变sql编辑区和查询结果区高度
*/
const onDragSetHeight = () => {
document.onmousemove = (e) => {
e.preventDefault();
//得到鼠标拖动的宽高距离:取绝对值
state.editorHeight = `${document.getElementById('MonacoTextarea-' + getKey())!.clientHeight + e.movementY}px`;
state.tableDataHeight -= e.movementY;
};
document.onmouseup = () => {
document.onmousemove = null;
};
const resizeTableHeight = (e: any) => {
const vh = window.innerHeight;
state.editorSize = e[0].size;
const editorHeight = (vh - 225) * (state.editorSize / 100);
state.tableDataHeight = vh - 225 - 40 - editorHeight + 'px';
};
const getKey = () => {
@@ -335,6 +333,10 @@ const onRunSql = async (newTab = false) => {
// 不是新建tab执行则在当前激活的tab上执行sql
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
execRes = state.execResTabs[i];
if (execRes.loading) {
ElMessage.error('当前结果集tab正在执行, 请使用新标签执行');
return;
}
id = execRes.id;
}
@@ -445,7 +447,7 @@ const formatSql = () => {
return;
}
const formatDialect = getDbDialect(getNowDbInst().type).getFormatDialect();
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
let sql = monacoEditor.getModel()?.getValueInRange(selection);
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容

View File

@@ -1,5 +1,5 @@
<template>
<div class="db-table-data mt5" :style="{ height: `${tableHeight}px` }">
<div class="db-table-data mt5" :style="{ height: tableHeight }">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
@@ -93,7 +93,8 @@
<template v-if="state.loading" #overlay>
<div class="el-loading-mask" style="display: flex; flex-direction: column; align-items: center; justify-content: center">
<div>
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="42" />
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
<el-text class="ml5" tag="b">执行时间 - {{ state.execTime.toFixed(1) }}s</el-text>
</div>
<div v-if="loadingKey" class="mt10">
<el-button @click="cancelLoading" type="info" size="small" plain> </el-button>
@@ -103,7 +104,7 @@
<template #empty>
<div style="text-align: center">
<el-empty :style="{ height: `${tableHeight}px` }" :description="state.emptyText" :image-size="100" />
<el-empty class="h100" :description="state.emptyText" :image-size="100" />
</div>
</template>
</el-table-v2>
@@ -124,7 +125,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
import { ref, onMounted, watch, reactive, toRefs, onBeforeUnmount } from 'vue';
import { ElInput } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string';
import { DbInst } from '@/views/ops/db/db';
@@ -137,9 +138,6 @@ import { dbApi } from '../../api';
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
const props = defineProps({
loadingKey: {
type: String,
},
dbId: {
type: Number,
required: true,
@@ -166,6 +164,9 @@ const props = defineProps({
type: Boolean,
default: false,
},
loadingKey: {
type: String,
},
emptyText: {
type: String,
default: '暂无数据',
@@ -175,8 +176,8 @@ const props = defineProps({
default: false,
},
height: {
type: Number,
default: 600,
type: String,
default: '600px',
},
});
@@ -283,6 +284,9 @@ const selectionRowsMap: Map<number, any> = new Map();
// 更新单元格 key-> rowIndex value -> 更新行
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
// 数据加载时间计时器
let execTimeInterval: any = null;
const state = reactive({
dbId: 0, // 当前选中操作的数据库实例
dbType: '',
@@ -291,9 +295,10 @@ const state = reactive({
datas: [],
columns: [] as any,
loading: false,
tableHeight: 600,
tableHeight: '600px',
emptyText: '',
execTime: 0,
contextmenu: {
dropdown: {
x: 0,
@@ -365,6 +370,11 @@ watch(
() => props.loading,
(newValue: any) => {
state.loading = newValue;
if (newValue) {
startLoading();
} else {
endLoading();
}
}
);
@@ -379,13 +389,15 @@ onMounted(async () => {
state.db = props.db;
state.table = props.table;
setTableData(props.data);
if (state.loading) {
startLoading();
}
});
const cancelLoading = async () => {
if (props.loadingKey) {
await dbApi.sqlExecCancel.request({ id: state.dbId, execId: props.loadingKey });
}
};
onBeforeUnmount(() => {
endLoading();
});
const setTableData = (datas: any) => {
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
@@ -416,6 +428,27 @@ const setTableColumns = (columns: any) => {
}
};
const startLoading = () => {
if (execTimeInterval) {
endLoading();
}
execTimeInterval = setInterval(() => {
state.execTime += 0.1; // 每秒递增执行时间
}, 100);
};
const endLoading = () => {
state.execTime = 0;
clearInterval(execTimeInterval);
};
const cancelLoading = async () => {
if (props.loadingKey) {
await dbApi.sqlExecCancel.request({ id: state.dbId, execId: props.loadingKey });
endLoading();
}
};
/**
* 当前单元格是否允许编辑
* @param rowIndex ri

View File

@@ -218,8 +218,8 @@ const props = defineProps({
required: true,
},
tableHeight: {
type: [Number],
default: 600,
type: [String],
default: '600px',
},
});
@@ -268,7 +268,7 @@ const state = reactive({
placeholder: '',
visible: false,
},
tableHeight: 600,
tableHeight: '600px',
hasUpdatedFileds: false,
});

View File

@@ -1,124 +1,128 @@
<template>
<div>
<el-row>
<el-col :span="5">
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }">
<span v-if="data.type.value == MongoNodeType.Mongo">
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
<template #reference>
<SvgIcon name="iconfont icon-op-mongo" :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="链接">
{{ data.params.uri }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }">
<span v-if="data.type.value == MongoNodeType.Mongo">
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
<template #reference>
<SvgIcon name="iconfont icon-op-mongo" :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="链接">
{{ data.params.uri }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
<SvgIcon v-if="data.type.value == MongoNodeType.Dbs" name="Coin" color="#67c23a" />
<SvgIcon v-if="data.type.value == MongoNodeType.Dbs" name="Coin" color="#67c23a" />
<SvgIcon
v-if="data.type.value == MongoNodeType.Coll || data.type.value == MongoNodeType.CollMenu"
name="Document"
class="color-primary"
/>
</template>
<SvgIcon
v-if="data.type.value == MongoNodeType.Coll || data.type.value == MongoNodeType.CollMenu"
name="Document"
class="color-primary"
/>
</template>
<template #label="{ data }">
<span v-if="data.type.value == MongoNodeType.Dbs">
{{ data.params.database }}
<span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
</span>
<template #label="{ data }">
<span v-if="data.type.value == MongoNodeType.Dbs">
{{ data.params.database }}
<span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
</span>
<span v-else>{{ data.label }}</span>
</template>
</tag-tree>
</el-col>
<span v-else>{{ data.label }}</span>
</template>
</tag-tree>
</Pane>
<el-col :span="19">
<div id="mongo-tab" class="ml5" style="border: 1px solid var(--el-border-color-light, #ebeef5); margin-top: 1px">
<el-row v-if="nowColl">
<el-descriptions :column="10" size="small" border>
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
<Pane>
<div id="mongo-tab" class="ml5" style="border: 1px solid var(--el-border-color-light, #ebeef5); margin-top: 1px">
<el-row v-if="nowColl">
<el-descriptions :column="10" size="small" border>
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
<el-descriptions-item label="ns" label-align="right">
{{ nowColl.stats?.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right">
{{ nowColl.stats?.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right">
{{ formatByteSize(nowColl.stats?.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right">
{{ formatByteSize(nowColl.stats?.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right">
{{ formatByteSize(nowColl.stats?.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right">
{{ formatByteSize(nowColl.stats?.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right">
{{ formatByteSize(nowColl.stats?.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-row>
<el-descriptions-item label="ns" label-align="right">
{{ nowColl.stats?.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right">
{{ nowColl.stats?.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right">
{{ formatByteSize(nowColl.stats?.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right">
{{ formatByteSize(nowColl.stats?.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right">
{{ formatByteSize(nowColl.stats?.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right">
{{ formatByteSize(nowColl.stats?.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right">
{{ formatByteSize(nowColl.stats?.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-row>
<el-row type="flex">
<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" :name="dt.key">
<el-row>
<el-col :span="2">
<div class="mt5">
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" :underline="false"> </el-link>
</div>
</el-col>
<el-col :span="22">
<el-input
ref="findParamInputRef"
v-model="dt.findParamStr"
placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)"
>
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row :style="`height: ${dataHeight}; overflow: auto;`">
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?" width="160">
<template #reference>
<el-link v-auth="perms.delData" :underline="false" type="danger" icon="DocumentDelete"> </el-link>
</template>
</el-popconfirm>
</div>
<el-row type="flex">
<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" :name="dt.key">
<el-row>
<el-col :span="2">
<div class="mt5">
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" :underline="false">
</el-link>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-row>
</div>
</el-col>
</el-col>
<el-col :span="22">
<el-input
ref="findParamInputRef"
v-model="dt.findParamStr"
placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)"
>
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row :style="`height: ${dataHeight}; overflow: auto;`">
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?" width="160">
<template #reference>
<el-link v-auth="perms.delData" :underline="false" type="danger" icon="DocumentDelete">
</el-link>
</template>
</el-popconfirm>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-row>
</div>
</Pane>
</Splitpanes>
</el-row>
<el-dialog width="600px" title="find参数" v-model="findDialog.visible">
@@ -174,6 +178,7 @@ import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { sleep } from '@/common/utils/loading';
import { Splitpanes, Pane } from 'splitpanes';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));

View File

@@ -1,43 +1,39 @@
<template>
<div>
<el-row>
<el-col :span="5">
<el-row type="flex" justify="space-between">
<el-col :span="24" class="flex-auto">
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }">
<span v-if="data.type.value == RedisNodeType.Redis">
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
<template #reference>
<SvgIcon name="iconfont icon-op-redis" :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="模式">
{{ data.params.mode }}
</el-descriptions-item>
<el-descriptions-item label="host">
{{ data.params.host }}
</el-descriptions-item>
<el-descriptions-item label="备注" label-align="right">
{{ data.params.remark }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }">
<span v-if="data.type.value == RedisNodeType.Redis">
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
<template #reference>
<SvgIcon name="iconfont icon-op-redis" :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="模式">
{{ data.params.mode }}
</el-descriptions-item>
<el-descriptions-item label="host">
{{ data.params.host }}
</el-descriptions-item>
<el-descriptions-item label="备注" label-align="right">
{{ data.params.remark }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
</template>
</tag-tree>
</el-col>
</el-row>
</el-col>
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
</template>
</tag-tree>
</Pane>
<el-col v-loading="state.loadingKeyTree" :span="7">
<Pane min-size="20" size="30">
<div class="key-list-vtree">
<el-row>
<el-col :span="2">
@@ -98,7 +94,6 @@
height: state.keyTreeHeight,
overflow: 'auto',
border: '1px solid var(--el-border-color-light, #ebeef5)',
marginLeft: '5px',
}"
ref="keyTreeRef"
:highlight-current="true"
@@ -127,21 +122,20 @@
</template>
</el-tree>
<!-- right context menu -->
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</el-col>
</Pane>
<el-col :span="12" style="border-left: 1px solid var(--el-card-border-color)">
<div class="ml5">
<el-tabs @tab-remove="removeDataTab" style="width: 100%" v-model="state.activeName">
<Pane min-size="40">
<div class="">
<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" />
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</Pane>
</Splitpanes>
<div style="text-align: center; margin-top: 10px"></div>
@@ -183,6 +177,7 @@ import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { sleep } from '../../../common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Splitpanes, Pane } from 'splitpanes';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
@@ -327,35 +322,6 @@ const setHeight = () => {
state.keyTreeHeight = window.innerHeight - 174 + 'px';
};
// /**
// * instmap; tagPaht -> redis info[]
// */
// const instMap: Map<string, any[]> = new Map();
// const getInsts = async () => {
// const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
// 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);
// }
// };
// /**
// * 加载标签树节点
// */
// const loadTags = async () => {
// await getInsts();
// const tagPaths = instMap.keys();
// const tagNodes = [];
// for (let tagPath of tagPaths) {
// tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
// }
// return tagNodes;
// };
const scan = async (appendKey = false) => {
isTrue(state.scanParam.id != null, '请先选择redis');
notBlank(state.scanParam.db, '请先选择库');
@@ -612,7 +578,7 @@ const delKey = (key: string) => {
<style lang="scss">
.key-list-vtree {
height: calc(100vh - 250px);
height: 100%;
}
.key-list-vtree .folder-label {

View File

@@ -1765,6 +1765,11 @@ sortablejs@^1.15.0:
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
splitpanes@^3.1.5:
version "3.1.5"
resolved "https://registry.npmmirror.com/splitpanes/-/splitpanes-3.1.5.tgz#de81da25681c252d131747a9cb48a17156e2b210"
integrity sha512-r3Mq2ITFQ5a2VXLOy4/Sb2Ptp7OfEO8YIbhVJqJXoFc9hc5nTXXkCvtVDjIGbvC0vdE7tse+xTM9BMjsszP6bw==
sql-formatter@^14.0.0:
version "14.0.0"
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-14.0.0.tgz#07a1714c49d7d280ff2f6f09c64eebfba82b7053"