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

View File

@@ -24,7 +24,7 @@
<slot v-else :node="node" :data="data" name="prefix"></slot> <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> <slot name="label" :data="data"> {{ data.label }}</slot>
</span> </span>

View File

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

View File

@@ -41,12 +41,6 @@
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" /> <SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
</template> </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 }"> <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.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{ <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, tableComment: x.tableComment,
size: formatByteSize(x.dataLength + x.indexLength, 1), size: formatByteSize(x.dataLength + x.indexLength, 1),
}) })
.withIcon(TableIcon); .withIcon(TableIcon)
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
}); });
// 设置父节点参数的表大小 // 设置父节点参数的表大小
parentNode.params.dbTableSize = formatByteSize(dbTableSize); parentNode.params.dbTableSize = formatByteSize(dbTableSize);

View File

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <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" /> <monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" /> <el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<template #footer> <template #footer>

View File

@@ -32,22 +32,19 @@
</div> </div>
<!-- 字段名列 --> <!-- 字段名列 -->
<div v-else style="position: relative"> <div v-else @contextmenu="headerContextmenuClick($event, column)" style="position: relative">
<!-- 字段列的数据类型 --> <!-- 字段列的数据类型 -->
<div class="column-type"> <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" /> <SvgIcon :size="10" name="Clock" style="cursor: unset" />
</span> </span>
<span class="font8" v-else>{{ ColumnTypeSubscript[dbDialect.getDataType(column.columnType)] }}</span> <span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
</div> </div>
<div v-if="showColumnTip" @mouseover="column.showSetting = true" @mouseleave="column.showSetting = false"> <div v-if="showColumnTip">
<el-tooltip :show-after="500" raw-content placement="top"> <el-text tag="b" :title="column.remark" style="cursor: pointer">
<template #content> {{ getColumnTip(column) }} </template> {{ column.title }}
<el-text tag="b" style="cursor: pointer"> </el-text>
{{ column.title }}
</el-text>
</el-tooltip>
<span> <span>
<SvgIcon <SvgIcon
@@ -63,20 +60,6 @@
{{ column.title }} {{ column.title }}
</el-text> </el-text>
</div> </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> </div>
</div> </div>
@@ -161,7 +144,7 @@ import { dateStrFormat } from '@/common/utils/date';
import { useIntervalFn } from '@vueuse/core'; import { useIntervalFn } from '@vueuse/core';
import { ColumnTypeSubscript, DataType, DbDialect, DbType, getDbDialect } from '../../dialect/index'; import { ColumnTypeSubscript, DataType, DbDialect, DbType, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue'; 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']); const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
@@ -236,9 +219,6 @@ const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', '取消固定')
.withOnClick((data: any) => (data.fixed = false)) .withOnClick((data: any) => (data.fixed = false))
.withHideFunc((data: any) => !data.fixed); .withHideFunc((data: any) => !data.fixed);
// 标头菜单
const tableHeadlerMenu = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
/** 表数据 contextmenu items **/ /** 表数据 contextmenu items **/
const cmDataCopyCell = new ContextmenuItem('copyValue', '复制') const cmDataCopyCell = new ContextmenuItem('copyValue', '复制')
@@ -465,6 +445,11 @@ const setTableData = (datas: any) => {
const setTableColumns = (columns: any) => { const setTableColumns = (columns: any) => {
state.columns = columns.map((x: any) => { state.columns = columns.map((x: any) => {
const columnName = x.columnName; const columnName = x.columnName;
// 数据类型
x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return { return {
...x, ...x,
key: columnName, 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) => { const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为 event.preventDefault(); // 阻止默认的右击菜单行为
@@ -681,18 +676,18 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
rowIndex: rowIndex, rowIndex: rowIndex,
colIndex: columnIndex, colIndex: columnIndex,
oldValue: rowData[column.dataKey], oldValue: rowData[column.dataKey],
dataType: dbDialect.getDataType(column.columnType), dataType: column.dataType,
}; };
// 编辑器语言json、html、string目前支持json、html使用MonacoEditor编辑器 // 编辑器语言json、html、string目前支持json、html使用MonacoEditor编辑器
let editorLang = getEditorLangByValue(rowData[column.dataKey]); let editorLang = getEditorLangByValue(rowData[column.dataKey]);
state.editorLang = editorLang; state.editorLang = editorLang;
if (editorLang === 'html' || editorLang === 'json') { if (editorLang === 'html' || editorLang === 'json') {
TableDataEditorBox({ MonacoEditorDialog({
content: rowData[column.dataKey], content: rowData[column.dataKey],
title: '编辑字段' + column.dataKey, title: `编辑字段 [${column.dataKey}]`,
language: editorLang, language: editorLang,
runSuccessCallback: (newVal: any) => { confirmFn: (newVal: any) => {
rowData[column.dataKey] = newVal; rowData[column.dataKey] = newVal;
onExitEditMode(rowData, column, rowIndex); 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; padding: 2px;
height: 12px; height: 12px;
} }
.column-header-op {
color: var(--el-color-primary);
position: absolute;
top: 6px;
right: 0px;
}
} }
</style> </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"> <el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
<template #content> <template #content>
1. 右击数据可显示操作菜单 <br /> 1. 右击数据/表头可显示操作菜单 <br />
2. 按住Ctrl点击数据则为多选 <br /> 2. 按住Ctrl点击数据则为多选 <br />
3. 双击单元格可编辑数据 3. 双击单元格可编辑数据 <br />
4. 鼠标悬停字段名或标签树的表名可提示相关备注
</template> </template>
<el-link icon="QuestionFilled" :underline="false"> </el-link> <el-link icon="QuestionFilled" :underline="false"> </el-link>
</el-tooltip> </el-tooltip>