refactor: 数据库虚拟table卡顿优化

This commit is contained in:
meilin.huang
2023-12-22 00:47:01 +08:00
parent 86bccc3b3d
commit 54a0f0b3c7
11 changed files with 139 additions and 174 deletions

View File

@@ -185,7 +185,7 @@ defineExpose({
position: fixed;
.el-dropdown-menu__item {
padding: 5px 10px;
padding: 5px 12px;
}
.el-dropdown-menu__item {

View File

@@ -0,0 +1,37 @@
import { VNode, h, render } from 'vue';
import MonacoEditorDialogComp from './MonacoEditorDialogComp.vue';
export type MonacoEditorDialogProps = {
content: string;
title: string;
language: string;
height?: string;
width?: string;
confirmFn?: Function;
cancelFn?: Function;
};
const boxId = 'monaco-editor-dialog-id';
let boxInstance: VNode;
const MonacoEditorDialog = (props: MonacoEditorDialogProps): void => {
if (!boxInstance) {
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
boxInstance = h(MonacoEditorDialogComp);
// 将虚拟dom渲染到 container dom 上
render(boxInstance, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container);
}
const boxVue = boxInstance.component;
if (boxVue) {
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed?.open(props);
}
};
export default MonacoEditorDialog;

View File

@@ -1,11 +1,11 @@
<template>
<div>
<el-dialog :destroy-on-close="true" :title="state.title" v-model="dialogVisible" :show-close="false" width="800px" @close="cancel">
<monaco-editor height="600px" class="codesql" :language="state.language" v-model="contentValue" />
<el-dialog :title="state.title" v-model="state.dialogVisible" :width="state.width" @close="cancel">
<monaco-editor ref="editorRef" :height="state.height" class="editor" :language="state.language" v-model="contentValue" />
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button @click="submit" type="primary">确定</el-button>
<el-button @click="confirm" type="primary">确定</el-button>
</span>
</template>
</el-dialog>
@@ -13,26 +13,27 @@
</template>
<script lang="ts" setup>
import { toRefs, ref, nextTick, reactive } from 'vue';
import { ElDialog, ElButton, InputInstance, ElMessage } from 'element-plus';
import { toRefs, ref, reactive } from 'vue';
import { ElDialog, ElButton, ElMessage } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { MonacoEditorDialogProps } from './MonacoEditorDialog';
import { TableDataEditorProps } from './DbTableDataEditorDialog';
const editorRef: any = ref(null);
const remarkInputRef = ref<InputInstance>();
const state = reactive({
dialogVisible: false,
height: '450px',
width: '800px',
contentValue: '',
title: '',
language: '',
});
const { dialogVisible, contentValue } = toRefs(state);
let confirmFn: any;
let cancelFn: any;
let cancelCallback: any;
let runSuccessCallback: any;
let runSuccess: boolean = false;
const { contentValue } = toRefs(state);
function compressHTML(html: string) {
return (
@@ -45,11 +46,10 @@ function compressHTML(html: string) {
}
/**
* 执行sql
* 确认按钮
*/
const submit = async () => {
runSuccess = true;
if (runSuccessCallback) {
const confirm = async () => {
if (confirmFn) {
if (state.language === 'json') {
let val;
try {
@@ -64,34 +64,28 @@ const submit = async () => {
}
// json
runSuccessCallback(JSON.stringify(val));
confirmFn(JSON.stringify(val));
} else if (state.language === 'html') {
// html
runSuccessCallback(compressHTML(contentValue.value));
confirmFn(compressHTML(contentValue.value));
} else {
runSuccessCallback(contentValue.value);
confirmFn(contentValue.value);
}
}
state.dialogVisible = false;
setTimeout(() => {
state.contentValue = '';
state.title = '';
runSuccessCallback = null;
runSuccess = false;
}, 200);
};
const cancel = () => {
state.dialogVisible = false;
//
if (!runSuccess && cancelCallback) {
cancelCallback();
}
cancelFn && cancelFn();
setTimeout(() => {
state.contentValue = '';
state.title = '';
cancelCallback = null;
runSuccess = false;
}, 200);
};
@@ -107,35 +101,34 @@ const formatXML = function (xml: string, tab?: string) {
return formatted.substring(1, formatted.length - 3);
};
const open = (props: TableDataEditorProps) => {
cancelCallback = props.cancelCallback;
runSuccessCallback = props.runSuccessCallback;
// json
if (props.language === 'json') {
try {
state.contentValue = JSON.stringify(JSON.parse(props.content), null, '\t');
} catch (e) {
state.contentValue = 'json格式字符串错误: ' + props.content;
const open = (optionProps: MonacoEditorDialogProps) => {
confirmFn = optionProps.confirmFn;
cancelFn = optionProps.cancelFn;
const language = optionProps.language;
state.language = language;
state.title = optionProps.title;
if (optionProps.height) {
state.height = optionProps.height;
}
state.contentValue = optionProps.content;
// html;
if (language === 'html' || language == 'xml') {
state.contentValue = formatXML(optionProps.content);
}
// html
if (props.language === 'html') {
state.contentValue = formatXML(props.content);
}
state.title = props.title;
state.language = props.language;
state.dialogVisible = true;
nextTick(() => {
setTimeout(() => {
remarkInputRef.value?.focus();
});
});
editorRef.value?.format();
}, 300);
state.dialogVisible = true;
};
defineExpose({ open });
</script>
<style lang="scss">
.codesql {
<style lang="scss" scoped>
.editor {
font-size: 9pt;
font-weight: 600;
}

View File

@@ -24,7 +24,7 @@
<slot v-else :node="node" :data="data" name="prefix"></slot>
<span class="ml3">
<span class="ml3" :title="data.labelRemark">
<slot name="label" :data="data"> {{ data.label }}</slot>
</span>

View File

@@ -13,6 +13,11 @@ export class TagTreeNode {
*/
label: string;
/**
* 节点名称备注用于元素title属性
*/
labelRemark: string;
/**
* 树节点类型
*/
@@ -38,6 +43,11 @@ export class TagTreeNode {
this.type = type || new NodeType(TagTreeNode.TagPath);
}
withLabelRemark(labelRemark: any) {
this.labelRemark = labelRemark;
return this;
}
withIsLeaf(isLeaf: boolean) {
this.isLeaf = isLeaf;
return this;

View File

@@ -41,12 +41,6 @@
<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>
</template>
<template #suffix="{ data }">
<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">{{
@@ -323,7 +317,8 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
tableComment: x.tableComment,
size: formatByteSize(x.dataLength + x.indexLength, 1),
})
.withIcon(TableIcon);
.withIcon(TableIcon)
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
});
// 设置父节点参数的表大小
parentNode.params.dbTableSize = formatByteSize(dbTableSize);

View File

@@ -1,6 +1,6 @@
import { h, render, VNode } from 'vue';
import SqlExecDialog from './SqlExecDialog.vue';
import {SqlLanguage} from 'sql-formatter/lib/src/sqlFormatter'
import { SqlLanguage } from 'sql-formatter/lib/src/sqlFormatter';
export type SqlExecProps = {
sql: string;
@@ -11,35 +11,26 @@ export type SqlExecProps = {
cancelCallback?: Function;
};
const boxId = 'sql-exec-id';
const boxId = 'sql-exec-dialog-id';
const renderBox = (): VNode => {
const props: SqlExecProps = {
sql: '',
dbId: 0,
} as any;
let boxInstance: VNode;
const SqlExecBox = (props: SqlExecProps): void => {
if (!boxInstance) {
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
const boxVNode = h(SqlExecDialog, props);
boxInstance = h(SqlExecDialog);
// 将虚拟dom渲染到 container dom 上
render(boxVNode, container);
render(boxInstance, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container);
}
return boxVNode;
};
let boxInstance: any;
const SqlExecBox = (props: SqlExecProps): void => {
if (boxInstance) {
const boxVue = boxInstance.component;
if (boxVue) {
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed.open(props);
} else {
boxInstance = renderBox();
SqlExecBox(props);
boxVue.exposed?.open(props);
}
};

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-dialog :destroy-on-close="true" title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<template #footer>

View File

@@ -32,22 +32,19 @@
</div>
<!-- 字段名列 -->
<div v-else style="position: relative">
<div v-else @contextmenu="headerContextmenuClick($event, column)" style="position: relative">
<!-- 字段列的数据类型 -->
<div class="column-type">
<span v-if="ColumnTypeSubscript[dbDialect.getDataType(column.columnType)] === 'icon-clock'">
<span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="10" name="Clock" style="cursor: unset" />
</span>
<span class="font8" v-else>{{ ColumnTypeSubscript[dbDialect.getDataType(column.columnType)] }}</span>
<span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
</div>
<div v-if="showColumnTip" @mouseover="column.showSetting = true" @mouseleave="column.showSetting = false">
<el-tooltip :show-after="500" raw-content placement="top">
<template #content> {{ getColumnTip(column) }} </template>
<el-text tag="b" style="cursor: pointer">
<div v-if="showColumnTip">
<el-text tag="b" :title="column.remark" style="cursor: pointer">
{{ column.title }}
</el-text>
</el-tooltip>
<span>
<SvgIcon
@@ -63,20 +60,6 @@
{{ column.title }}
</el-text>
</div>
<el-dropdown trigger="click" class="column-header-op" size="small">
<SvgIcon :size="16" name="CaretBottom" />
<template #dropdown>
<el-dropdown-menu>
<template v-for="menu in tableHeadlerMenu">
<el-dropdown-item :key="menu.clickId" v-if="!menu.isHide(column)" @click="menu?.onClickFunc(column)">
<SvgIcon v-if="menu.icon" :name="menu.icon" />
{{ menu.txt }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
@@ -161,7 +144,7 @@ import { dateStrFormat } from '@/common/utils/date';
import { useIntervalFn } from '@vueuse/core';
import { ColumnTypeSubscript, DataType, DbDialect, DbType, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
import TableDataEditorBox from '@/views/ops/db/component/table/DbTableDataEditorDialog';
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
@@ -236,9 +219,6 @@ const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', '取消固定')
.withOnClick((data: any) => (data.fixed = false))
.withHideFunc((data: any) => !data.fixed);
// 标头菜单
const tableHeadlerMenu = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
/** 表数据 contextmenu items **/
const cmDataCopyCell = new ContextmenuItem('copyValue', '复制')
@@ -465,6 +445,11 @@ const setTableData = (datas: any) => {
const setTableColumns = (columns: any) => {
state.columns = columns.map((x: any) => {
const columnName = x.columnName;
// 数据类型
x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return {
...x,
key: columnName,
@@ -561,6 +546,16 @@ const rowEventHandlers = {
},
};
const headerContextmenuClick = (event: any, data: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
contextmenuRef.value.openContextmenu(data);
};
const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
@@ -681,18 +676,18 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
rowIndex: rowIndex,
colIndex: columnIndex,
oldValue: rowData[column.dataKey],
dataType: dbDialect.getDataType(column.columnType),
dataType: column.dataType,
};
// 编辑器语言json、html、string目前支持json、html使用MonacoEditor编辑器
let editorLang = getEditorLangByValue(rowData[column.dataKey]);
state.editorLang = editorLang;
if (editorLang === 'html' || editorLang === 'json') {
TableDataEditorBox({
MonacoEditorDialog({
content: rowData[column.dataKey],
title: '编辑字段' + column.dataKey,
title: `编辑字段 [${column.dataKey}]`,
language: editorLang,
runSuccessCallback: (newVal: any) => {
confirmFn: (newVal: any) => {
rowData[column.dataKey] = newVal;
onExitEditMode(rowData, column, rowIndex);
},
@@ -835,11 +830,6 @@ const getFormatTimeValue = (dataType: DataType, originValue: string): string =>
}
};
const getColumnTip = (column: any) => {
const comment = column.columnComment;
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
};
/**
* 触发响应式实时刷新,否则需要滑动或移动才能使样式实时生效
*/
@@ -902,12 +892,5 @@ defineExpose({
padding: 2px;
height: 12px;
}
.column-header-op {
color: var(--el-color-primary);
position: absolute;
top: 6px;
right: 0px;
}
}
</style>

View File

@@ -1,45 +0,0 @@
import { h, render, VNode } from 'vue';
import SqlExecDialog from './DbTableDataEditorDialog.vue';
export type TableDataEditorProps = {
content: string;
title: string;
language: string;
runSuccessCallback?: Function;
cancelCallback?: Function;
};
const boxId = 'table-data-editor-id';
const renderBox = (): VNode => {
const props: TableDataEditorProps = {
content: '',
title: '',
language: '',
};
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
const boxVNode = h(SqlExecDialog, props);
// 将虚拟dom渲染到 container dom 上
render(boxVNode, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container);
return boxVNode;
};
let boxInstance: any;
const TableDataEditorBox = (props: TableDataEditorProps): void => {
if (boxInstance) {
const boxVue = boxInstance.component;
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed.open(props);
} else {
boxInstance = renderBox();
TableDataEditorBox(props);
}
};
export default TableDataEditorBox;

View File

@@ -38,9 +38,10 @@
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
<template #content>
1. 右击数据可显示操作菜单 <br />
1. 右击数据/表头可显示操作菜单 <br />
2. 按住Ctrl点击数据则为多选 <br />
3. 双击单元格可编辑数据
3. 双击单元格可编辑数据 <br />
4. 鼠标悬停字段名或标签树的表名可提示相关备注
</template>
<el-link icon="QuestionFilled" :underline="false"> </el-link>
</el-tooltip>