refactor: 界面优化

This commit is contained in:
meilin.huang
2023-04-05 22:41:53 +08:00
parent f6e9076a40
commit 58fb11b78f
7 changed files with 349 additions and 131 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,135 @@
<template>
<transition name="el-zoom-in-center">
<div aria-hidden="true" class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu" role="tooltip"
data-popper-placement="bottom" :style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`" :key="Math.random()"
v-show="state.isShow">
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li class="el-dropdown-menu__item" aria-disabled="false" tabindex="-1" :key="k" v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)">
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
// 定义父组件传过来的值
const props = defineProps({
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
items: {
type: Array,
default: [],
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
],
item: {} as any,
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, item: state.item });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
watch(
() => props.items,
(x: any) => {
state.dropdownList = x
},
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
closeContextmenu,
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

View File

@@ -7,91 +7,118 @@
data-popper-placement="bottom" data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`" :style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()" :key="Math.random()"
v-show="isShow" v-show="state.isShow"
> >
<ul class="el-dropdown-menu"> <ul class="el-dropdown-menu">
<template v-for="(v, k) in dropdownList"> <template v-for="(v, k) in state.dropdownList">
<li <li
class="el-dropdown-menu__item" class="el-dropdown-menu__item"
aria-disabled="false" aria-disabled="false"
tabindex="-1" tabindex="-1"
:key="k" :key="k"
v-if="!v.affix" v-if="!v.affix"
@click="onCurrentContextmenuClick(v.id)" @click="onCurrentContextmenuClick(v.contextMenuClickId)"
> >
<i :class="v.icon"></i> <SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span> <span>{{ v.txt }}</span>
</li> </li>
</template> </template>
</ul> </ul>
<div class="el-popper__arrow" style="left: 10px"></div> <div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted } from 'vue'; import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
export default defineComponent({
name: 'layoutTagsViewContextmenu', // 定义父组件传过来的值
props: { const props = defineProps({
dropdown: { dropdown: {
type: Object, type: Object,
default: () => {
return {
x: 0,
y: 0,
};
}, },
}, },
setup(props, { emit }) { });
const state = reactive({
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false, isShow: false,
dropdownList: [ dropdownList: [
{ id: 0, txt: '刷新', affix: false, icon: 'el-icon-refresh-right' }, { contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ id: 1, txt: '关闭', affix: false, icon: 'el-icon-close' }, { contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ id: 2, txt: '关闭其他', affix: false, icon: 'el-icon-circle-close' }, { contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ id: 3, txt: '关闭所有', affix: false, icon: 'el-icon-folder-delete' }, { contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
{ {
id: 4, contextMenuClickId: 4,
txt: '当前页全屏', txt: '当前页全屏',
affix: false, affix: false,
icon: 'el-icon-full-screen', icon: 'full-screen',
}, },
], ],
path: {}, item: {} as any,
}); arrowLeft: 10,
// 父级传过来的坐标 x,y 值 });
const dropdowns = computed(() => {
return props.dropdown; // 父级传过来的坐标 x,y 值
}); const dropdowns = computed(() => {
// 当前项菜单点击 // 117 为 `Dropdown 下拉菜单` 的宽度
const onCurrentContextmenuClick = (id: number) => { if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
emit('currentContextmenuClick', { id, path: state.path }); return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
}; };
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮 } else {
const openContextmenu = (item: any) => { return props.dropdown;
state.path = item.fullPath; }
item.meta.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false); });
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.fullPath });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.meta?.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu(); closeContextmenu();
setTimeout(() => { setTimeout(() => {
state.isShow = true; state.isShow = true;
}, 10); }, 10);
}; };
// 关闭右键菜单 // 关闭右键菜单
const closeContextmenu = () => { const closeContextmenu = () => {
state.isShow = false; state.isShow = false;
}; };
// 监听页面监听进行右键菜单的关闭 // 监听页面监听进行右键菜单的关闭
onMounted(() => { onMounted(() => {
document.body.addEventListener('click', closeContextmenu); document.body.addEventListener('click', closeContextmenu);
}); });
// 页面卸载时,移除右键菜单监听事件 // 页面卸载时,移除右键菜单监听事件
onUnmounted(() => { onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu); document.body.removeEventListener('click', closeContextmenu);
}); });
return { // 监听下拉菜单位置
dropdowns, watch(
openContextmenu, () => props.dropdown,
closeContextmenu, ({ x }) => {
onCurrentContextmenuClick, if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
...toRefs(state), else state.arrowLeft = 10;
};
}, },
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
}); });
</script> </script>
@@ -102,6 +129,7 @@ export default defineComponent({
position: fixed; position: fixed;
.el-dropdown-menu__item { .el-dropdown-menu__item {
font-size: 12px !important; font-size: 12px !important;
white-space: nowrap;
i { i {
font-size: 12px !important; font-size: 12px !important;
} }

View File

@@ -9,8 +9,8 @@
if (el) tagsRefs[k] = el; if (el) tagsRefs[k] = el;
} }
"> ">
<i class="iconfont icon-webicon318 layout-navbars-tagsview-ul-li-iconfont font14" <SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14"
v-if="isActive(v)"></i> v-if="isActive(v)" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" <SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont"
v-if="!isActive(v) && themeConfig.isTagsviewIcon" /> v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.meta.title }}</span> <span>{{ v.meta.title }}</span>
@@ -523,4 +523,5 @@ onBeforeRouteUpdate((to) => {
.layout-navbars-tagsview-shadow { .layout-navbars-tagsview-shadow {
box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px; box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px;
}</style> }
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="instances-box layout-aside"> <div class="instances-box">
<el-row type="flex" justify="space-between"> <el-row type="flex" justify="space-between">
<el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto"> <el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" /> <el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" />
@@ -7,7 +7,7 @@
<el-tree ref="treeRef" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }" <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" :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" :expand-on-click-node="true" :filter-node-method="filterNode" @node-click="treeNodeClick"
@node-expand="treeNodeClick"> @node-expand="treeNodeClick" @node-contextmenu="nodeContextmenu">
<template #default="{ node, data }"> <template #default="{ node, data }">
<span> <span>
<span v-if="data.type == TagTreeNode.TagPath"> <span v-if="data.type == TagTreeNode.TagPath">
@@ -19,16 +19,13 @@
<span class="ml3"> <span class="ml3">
<slot name="label" :data="data"> {{ data.label }}</slot> <slot name="label" :data="data"> {{ data.label }}</slot>
</span> </span>
<span class="ml3">
<slot name="option" :data="data"></slot>
</span>
</span> </span>
</template> </template>
</el-tree> </el-tree>
</el-col> </el-col>
</el-row> </el-row>
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef"
@currentContextmenuClick="onCurrentContextmenuClick" />
</div> </div>
</template> </template>
@@ -36,6 +33,7 @@
import { onMounted, reactive, ref, watch, toRefs } from 'vue'; import { onMounted, reactive, ref, watch, toRefs } from 'vue';
import { TagTreeNode } from './tag'; import { TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue'; import TagInfo from './TagInfo.vue';
import Contextmenu from '@/components/contextmenu/index.vue';
const props = defineProps({ const props = defineProps({
height: { height: {
@@ -45,6 +43,10 @@ const props = defineProps({
load: { load: {
type: Function, type: Function,
required: true, required: true,
},
loadContextmenuItems: {
type: Function,
required: false,
} }
}) })
@@ -54,12 +56,18 @@ const treeProps = {
isLeaf: 'isLeaf', isLeaf: 'isLeaf',
} }
const emit = defineEmits(['nodeClick']) const emit = defineEmits(['nodeClick', 'currentContextmenuClick'])
const treeRef: any = ref(null) const treeRef: any = ref(null)
const contextmenuRef = ref();
const state = reactive({ const state = reactive({
height: 600 as any, height: 600 as any,
filterText: '', filterText: '',
dropdown: {
x: 0,
y: 0,
},
contextmenuItems: [],
opend: {}, opend: {},
}) })
const { filterText } = toRefs(state) const { filterText } = toRefs(state)
@@ -101,6 +109,29 @@ const loadNode = async (node: any, resolve: any) => {
const treeNodeClick = (data: any) => { const treeNodeClick = (data: any) => {
emit('nodeClick', data); emit('nodeClick', data);
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
}
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
if (!props.loadContextmenuItems) {
return;
}
// 加载当前节点是否需要显示右击菜单
const items = props.loadContextmenuItems(data)
if (!items || items.length == 0) {
return;
}
state.contextmenuItems = items;
const { clientX, clientY } = event;
state.dropdown.x = clientX;
state.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data);
}
const onCurrentContextmenuClick = (clickData: any) => {
emit('currentContextmenuClick', clickData);
} }
const reloadNode = (nodeKey: any) => { const reloadNode = (nodeKey: any) => {
@@ -125,6 +156,7 @@ defineExpose({
<style lang="scss"> <style lang="scss">
.instances-box { .instances-box {
overflow: 'auto'; overflow: 'auto';
position: relative;
.el-tree { .el-tree {
display: inline-block; display: inline-block;

View File

@@ -23,13 +23,15 @@
</el-row> </el-row>
<el-row type="flex"> <el-row type="flex">
<el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px"> <el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px">
<tag-tree ref="tagTreeRef" @node-click="nodeClick" :load="loadNode" :height="state.tagTreeHeight"> <tag-tree ref="tagTreeRef" @node-click="nodeClick" :load="loadNode" :load-contextmenu-items="getContextmenuItems" @current-contextmenu-click="onCurrentContextmenuClick"
:height="state.tagTreeHeight">
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type == NodeType.DbInst"> <span v-if="data.type == NodeType.DbInst">
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210"> <el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
<template #reference> <template #reference>
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" /> <SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" /> <SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres"
:size="18" />
<SvgIcon name="InfoFilled" v-else /> <SvgIcon name="InfoFilled" v-else />
</template> </template>
@@ -57,12 +59,8 @@
<SvgIcon name="Calendar" color="#409eff" /> <SvgIcon name="Calendar" color="#409eff" />
</el-tooltip> </el-tooltip>
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" color="#f56c6c" /> <SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql"
</template> color="#f56c6c" />
<template #option="{data}">
<span v-if="data.type == NodeType.TableMenu">
<el-link @click="reloadTables(data.key)" icon="refresh" :underline="false"></el-link>
</span>
</template> </template>
</tag-tree> </tag-tree>
</el-col> </el-col>
@@ -119,6 +117,9 @@ class NodeType {
static Table = 5; static Table = 5;
static Sql = 6; static Sql = 6;
} }
class ContextmenuClickId {
static ReloadTable = 0
}
const tagTreeRef: any = ref(null) const tagTreeRef: any = ref(null)
@@ -134,7 +135,7 @@ const state = reactive({
tabs, tabs,
dataTabsTableHeight: '600', dataTabsTableHeight: '600',
editorHeight: '600', editorHeight: '600',
tagTreeHeight: window.innerHeight - 178 + 'px', tagTreeHeight: window.innerHeight - 173 + 'px',
genSqlDialog: { genSqlDialog: {
visible: false, visible: false,
sql: '', sql: '',
@@ -171,9 +172,9 @@ const getInsts = async () => {
if (!res.total) return if (!res.total) return
for (const db of res.list) { for (const db of res.list) {
const tagPath = db.tagPath; const tagPath = db.tagPath;
let redisInsts = instMap.get(tagPath) || []; let dbInsts = instMap.get(tagPath) || [];
redisInsts.push(db); dbInsts.push(db);
instMap.set(tagPath, redisInsts); instMap.set(tagPath, dbInsts?.sort());
} }
} }
@@ -261,10 +262,28 @@ const nodeClick = async (data: any) => {
} }
} }
const getContextmenuItems = (data: any) => {
const dataType = data.type;
if (dataType === NodeType.TableMenu) {
return [
{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }
]
}
return [];
}
// 当前右击菜单点击事件
const onCurrentContextmenuClick = (clickData: any) => {
const clickId = clickData.id;
if (clickId == ContextmenuClickId.ReloadTable) {
reloadTables(clickData.item.key)
}
}
const getTables = async (params: any) => { const getTables = async (params: any) => {
const { id, db } = params; const { id, db } = params;
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus); let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus=false state.reloadStatus = false
return tables.map((x: any) => { return tables.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({ return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
id, id,
@@ -413,8 +432,8 @@ const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu` return `${dbId}.${db}.sql-menu`
} }
const reloadTables = (nodeKey:string) => { const reloadTables = (nodeKey: string) => {
state.reloadStatus=true state.reloadStatus = true
tagTreeRef.value.reloadNode(nodeKey); tagTreeRef.value.reloadNode(nodeKey);
} }

View File

@@ -68,6 +68,9 @@ export class DbInst {
if (!reload && tables) { if (!reload && tables) {
return tables; return tables;
} }
// 重置列信息缓存与表提示信息
db.columnsMap?.clear();
db.tableHints = null;
console.log(`load tables -> dbName: ${dbName}`); console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName }); tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName });
db.tables = tables; db.tables = tables;