refactor: contextmenu组件优化、标签&资源替换为contextmenu操作

This commit is contained in:
meilin.huang
2023-11-14 17:36:51 +08:00
parent f234c72514
commit 0ae99cdaf9
15 changed files with 337 additions and 366 deletions

View File

@@ -6,6 +6,9 @@ import { useUserInfo } from '@/store/userInfo';
* @returns
*/
export function hasPerm(code: string) {
if (!code) {
return true;
}
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
}

View File

@@ -0,0 +1,55 @@
export class ContextmenuItem {
clickId: any;
txt: string;
icon: string;
affix: boolean;
permission: string;
/**
* 是否隐藏回调函数
*/
hideFunc: (data: any) => boolean;
onClickFunc: (data: any) => void;
constructor(clickId: any, txt: string) {
this.clickId = clickId;
this.txt = txt;
}
withIcon(icon: string) {
this.icon = icon;
return this;
}
withPermission(permission: string) {
this.permission = permission;
return this;
}
withHideFunc(func: (data: any) => boolean) {
this.hideFunc = func;
return this;
}
withOnClick(func: (data: any) => void) {
this.onClickFunc = func;
return this;
}
/**
* 是否隐藏
* @param data 点击数据项
* @returns
*/
isHide(data: any) {
if (this.hideFunc) {
return this.hideFunc(data);
}
return false;
}
}

View File

@@ -7,17 +7,18 @@
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="state.isShow"
v-show="state.isShow && !allHide"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li
v-auth="v.permission"
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
v-if="!v.affix && !v.isHide(state.item)"
@click="onCurrentContextmenuClick(v)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
@@ -31,6 +32,7 @@
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { ContextmenuItem } from './index';
// 定义父组件传过来的值
const props = defineProps({
@@ -44,7 +46,7 @@ const props = defineProps({
},
},
items: {
type: Array,
type: Array<ContextmenuItem>,
default: () => [],
},
});
@@ -55,11 +57,20 @@ const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
dropdownList: [] as ContextmenuItem[],
item: {} as any,
arrowLeft: 10,
});
const allHide = computed(() => {
for (let item of state.dropdownList) {
if (!item.isHide(state.item)) {
return false;
}
}
return true;
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
@@ -73,8 +84,12 @@ const dropdowns = computed(() => {
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, item: state.item });
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
// 存在点击事件,则触发该事件函数
if (ci.onClickFunc) {
ci.onClickFunc(state.item);
}
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
@@ -91,6 +106,7 @@ const closeContextmenu = () => {
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
state.dropdownList = props.items;
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
@@ -140,3 +156,4 @@ defineExpose({
}
}
</style>
.

View File

@@ -1,138 +0,0 @@
<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,
};
},
},
});
// 定义子组件向父组件传值/事件
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' },
{
contextMenuClickId: 4,
txt: '当前页全屏',
affix: false,
icon: 'full-screen',
},
],
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, path: state.item.path });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
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,
}
);
// 暴露变量
defineExpose({
openContextmenu,
});
</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

@@ -42,19 +42,20 @@
</li>
</ul>
</el-scrollbar>
<Contextmenu :dropdown="state.dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
<Contextmenu :items="state.contextmenu.items" :dropdown="state.contextmenu.dropdown" ref="contextmenuRef" />
</div>
</template>
<script lang="ts" setup name="layoutTagsView">
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance, watch } from 'vue';
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import mittBus from '@/common/utils/mitt';
import Sortable from 'sortablejs';
import Contextmenu from '@/layout/navBars/tagsView/contextmenu.vue';
import Contextmenu from '@/components/contextmenu/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
import { useTagsViews } from '@/store/tagsViews';
import { useKeepALiveNames } from '@/store/keepAliveNames';
@@ -73,11 +74,38 @@ const keepAliveNamesStores = useKeepALiveNames();
const route = useRoute();
const router = useRouter();
const contextmenuItems = [
new ContextmenuItem(0, '刷新').withIcon('RefreshRight').withOnClick((data: any) => {
// path为fullPath
let { path } = data;
let currentTag = tagsViews.value.find((v: any) => v.path === path);
refreshCurrentTagsView(path);
router.push({ path, query: currentTag?.query });
}),
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => closeCurrentTagsView(data.path)),
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
let { path } = data;
let currentTag = tagsViews.value.find((v: any) => v.path === path);
router.push({ path, query: currentTag?.query });
closeOtherTagsView(path);
}),
new ContextmenuItem(3, '关闭所有').withIcon('FolderDelete').withOnClick((data: any) => closeAllTagsView(data.path)),
new ContextmenuItem(4, '当前页全屏').withIcon('full-screen').withOnClick((data: any) => openCurrenFullscreen(data.path)),
];
const state = reactive({
routePath: route.fullPath,
dropdown: { x: '', y: '' },
// dropdown: { x: '', y: '' },
tagsRefsIndex: 0,
sortable: '' as any,
contextmenu: {
items: contextmenuItems,
dropdown: { x: '', y: '' },
},
});
// 动态设置 tagsView 风格样式
@@ -239,31 +267,7 @@ const openCurrenFullscreen = (path: string) => {
screenfulls.request(element);
});
};
// 当前项右键菜单点击
const onCurrentContextmenuClick = (data: any) => {
// path为fullPath
let { id, path } = data;
let currentTag = tagsViews.value.find((v: any) => v.path === path);
switch (id) {
case 0:
refreshCurrentTagsView(path);
router.push({ path, query: currentTag?.query });
break;
case 1:
closeCurrentTagsView(path);
break;
case 2:
router.push({ path, query: currentTag?.query });
closeOtherTagsView(path);
break;
case 3:
closeAllTagsView(path);
break;
case 4:
openCurrenFullscreen(path);
break;
}
};
// 判断页面高亮
const isActive = (tagView: TagsView) => {
return tagView.path === state.routePath;
@@ -271,8 +275,8 @@ const isActive = (tagView: TagsView) => {
// 右键点击时:传 x,y 坐标值到子组件中props
const onContextmenu = (v: any, e: any) => {
const { clientX, clientY } = e;
state.dropdown.x = clientX;
state.dropdown.y = clientY;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(v);
};
// 当前的 tagsView 项点击时
@@ -371,10 +375,6 @@ const initSortable = () => {
// 页面加载前
onBeforeMount(() => {
// 监听非本页面调用 0 刷新当前1 关闭当前2 关闭其它3 关闭全部 4 当前页全屏
mittBus.on('onCurrentContextmenuClick', (data: object) => {
onCurrentContextmenuClick(data);
});
// 监听布局配置界面开启/关闭拖拽
mittBus.on('openOrCloseSortable', () => {
initSortable();
@@ -382,8 +382,6 @@ onBeforeMount(() => {
});
// 页面卸载时
onUnmounted(() => {
// 取消非本页面调用监听
mittBus.off('onCurrentContextmenuClick');
// 取消监听布局配置界面开启/关闭拖拽
mittBus.off('openOrCloseSortable');
});
@@ -523,8 +521,14 @@ onBeforeRouteUpdate((to) => {
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
-webkit-mask-size: 18px 30px, 20px 30px, calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position: right bottom, left bottom, center top;
-webkit-mask-size:
18px 30px,
20px 30px,
calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position:
right bottom,
left bottom,
center top;
-webkit-mask-repeat: no-repeat;
}
@@ -545,14 +549,14 @@ onBeforeRouteUpdate((to) => {
&:hover {
@extend .tgs-style-three-svg;
background: var(--el-color-primary-light-9);
background: var(--tagsview3-active-background-color);
color: unset;
}
}
.is-active {
@extend .tgs-style-three-svg;
background: var(--el-color-primary-light-9) !important;
background: var(--tagsview3-active-background-color) !important;
color: var(--el-color-primary) !important;
z-index: 1;
}
@@ -563,3 +567,4 @@ onBeforeRouteUpdate((to) => {
box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px;
}
</style>
@/components/contextmenu

View File

@@ -23,6 +23,8 @@
--color-seting-main: #e9eef3;
--color-seting-aside: #d3dce6;
--color-seting-header: #b3c0d1;
--tagsview3-active-background-color: var(--el-color-primary-light-9);
}
html,

View File

@@ -24,4 +24,6 @@ html.dark {
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
--bg-columnsMenuBar: var(--next-color-disabled) !important;
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
--tagsview3-active-background-color: var(--next-color-hover);
}

View File

@@ -345,4 +345,5 @@
.el-dialog {
border-radius: 6px; /* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
border: 1px solid var(--el-border-color-lighter);
}

View File

@@ -1,3 +1,5 @@
import { ContextmenuItem } from '@/components/contextmenu';
export class TagTreeNode {
/**
* 节点id
@@ -73,7 +75,7 @@ export class NodeType {
*/
value: number;
contextMenuItems: [];
contextMenuItems: ContextmenuItem[];
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
@@ -108,7 +110,7 @@ export class NodeType {
* @param contextMenuItems 右击菜单按钮选项
* @returns this
*/
withContextMenuItems(contextMenuItems: []) {
withContextMenuItems(contextMenuItems: ContextmenuItem[]) {
this.contextMenuItems = contextMenuItems;
return this;
}

View File

@@ -37,7 +37,7 @@
<el-row type="flex">
<el-col :span="4">
<tag-tree ref="tagTreeRef" :loadTags="loadTags" @current-contextmenu-click="onCurrentContextmenuClick" :height="state.tagTreeHeight">
<tag-tree ref="tagTreeRef" :loadTags="loadTags" :height="state.tagTreeHeight">
<template #prefix="{ data }">
<span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
@@ -159,6 +159,7 @@ import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '../../../components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
@@ -199,11 +200,6 @@ const SqlIcon = {
color: '#f56c6c',
};
class ContextmenuClickId {
static ReloadTable = 0;
static TableOp = 1;
}
// node节点点击时触发改变db事件
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
const params = nodeData.params;
@@ -290,9 +286,13 @@ const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
// 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
.withContextMenuItems([
{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' },
{ contextMenuClickId: ContextmenuClickId.TableOp, txt: '表操作', icon: 'Setting' },
] as any)
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadTables(data.key)),
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
const params = data.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
}),
])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
let { id, db } = params;
@@ -423,19 +423,6 @@ const loadTags = async () => {
return tagNodes;
};
// 当前右击菜单点击事件
const onCurrentContextmenuClick = (clickData: any) => {
const clickId = clickData.id;
if (clickId == ContextmenuClickId.ReloadTable) {
reloadTables(clickData.item.key);
return;
}
if (clickId == ContextmenuClickId.TableOp) {
const params = clickData.item.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: clickData.item.key });
}
};
// 选择数据库,改变当前正在操作的数据库信息
const changeDb = (db: any, dbName: string) => {
state.nowDbInst = DbInst.getOrNewInst(db);
@@ -655,3 +642,4 @@ const getNowDbInfo = () => {
}
}
</style>
../../../components/contextmenu

View File

@@ -68,10 +68,10 @@
<template #prepend>
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right">
<template #reference>
<el-link @click.stop="state.condPopVisible = !state.condPopVisible" type="success" :underline="false">选择列</el-link>
<el-link @click.stop="chooseCondColumnName" type="success" :underline="false">选择列</el-link>
</template>
<el-table
:data="columns"
:data="filterCondColumns"
max-height="500"
size="small"
@row-click="
@@ -81,7 +81,17 @@
"
style="cursor: pointer"
>
<el-table-column property="columnName" label="列名" show-overflow-tooltip> </el-table-column>
<el-table-column property="columnName" label="列名" show-overflow-tooltip>
<template #header>
<el-input
ref="columnNameSearchInputRef"
v-model="state.columnNameSearch"
size="small"
placeholder="列名: 输入可过滤"
clearable
/>
</template>
</el-table-column>
<el-table-column property="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table>
</el-popover>
@@ -185,7 +195,7 @@
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
import { onMounted, computed, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
import { isTrue, notEmpty } from '@/common/assert';
import { ElMessage } from 'element-plus';
@@ -217,6 +227,7 @@ const props = defineProps({
});
const dbTableRef = ref(null) as Ref;
const columnNameSearchInputRef = ref(null) as Ref;
const state = reactive({
datas: [],
@@ -231,6 +242,7 @@ const state = reactive({
count: 0,
selectionDatas: [] as any,
condPopVisible: false,
columnNameSearch: '',
conditionDialog: {
title: '',
placeholder: '',
@@ -351,6 +363,35 @@ const exportData = () => {
exportCsv(`数据导出-${props.tableName}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
};
/**
* 选择条件列
*/
const chooseCondColumnName = () => {
state.condPopVisible = !state.condPopVisible;
if (state.condPopVisible) {
columnNameSearchInputRef.value.clear();
columnNameSearchInputRef.value.focus();
}
};
/**
* 过滤条件列名
*/
const filterCondColumns = computed(() => {
const columns = state.columns;
const columnNameSearch = state.columnNameSearch;
if (!columnNameSearch) {
return columns;
}
return columns.filter((data: any) => {
let tnMatch = true;
if (columnNameSearch) {
tnMatch = data.columnName.toLowerCase().includes(columnNameSearch.toLowerCase());
}
return tnMatch;
});
});
/**
* 条件查询,点击列信息后显示输入对应的值
*/

View File

@@ -128,23 +128,7 @@
</el-tree>
<!-- right context menu -->
<div ref="rightMenuRef" class="key-list-right-menu">
<!-- folder right menu -->
<div v-if="!state.rightClickNode?.isLeaf"></div>
<!-- key right menu -->
<div v-else>
<el-row>
<el-link @click="showKeyDetail(state.rightClickNode.key, true)" type="primary" icon="plus" :underline="false"
>新tab打开</el-link
>
</el-row>
<el-row class="mt5">
<el-link @click="delKey(state.rightClickNode.key)" v-auth="'redis:data:del'" type="danger" icon="delete" :underline="false"
>删除</el-link
>
</el-row>
</div>
</div>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</el-col>
@@ -195,9 +179,24 @@ import { isTrue, notBlank, notNull } from '@/common/assert';
import { TagTreeNode, NodeType } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import Contextmenu from '@/components/contextmenu/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
const contextmenuRef = ref();
const cmNewTabOpen = new ContextmenuItem('newTabOpenKey', '新tab打开')
.withIcon('plus')
.withHideFunc((data: any) => !data.isLeaf)
.withOnClick((data: any) => showKeyDetail(data.key, true));
const cmDelKey = new ContextmenuItem('delKey', '删除')
.withIcon('delete')
.withPermission('redis:data:del')
.withHideFunc((data: any) => !data.isLeaf)
.withOnClick((data: any) => delKey(data.key));
/**
* 树节点类型
*/
@@ -265,7 +264,6 @@ const treeProps = {
const defaultCount = 250;
const keyTreeRef: any = ref(null);
const rightMenuRef: any = ref(null);
const state = reactive({
tags: [],
@@ -297,6 +295,13 @@ const state = reactive({
},
},
dbsize: 0,
contextmenu: {
dropdown: {
x: 0,
y: 0,
},
items: [cmNewTabOpen, cmDelKey],
},
});
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
@@ -406,7 +411,8 @@ const expandAllKeyNode = (nodes: any) => {
};
const handleKeyTreeNodeClick = async (data: any) => {
hideAllMenus();
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
// 目录则不做处理
if (data.type == 1) {
return;
@@ -479,40 +485,11 @@ const keyTreeNodeCollapse = (data: any) => {
};
const rightClickNode = (event: any, data: any, node: any) => {
hideAllMenus();
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(node);
keyTreeRef.value.setCurrentKey(node.key);
state.rightClickNode = node;
// nextTick for dom render
nextTick(() => {
let top = event.clientY;
const menu = rightMenuRef.value;
menu.style.display = 'block';
// position in bottom
if (document.body.clientHeight - top < menu.clientHeight) {
top -= menu.clientHeight;
}
menu.style.left = `${event.clientX}px`;
menu.style.top = `${top}px`;
document.addEventListener('click', hideAllMenus, { once: true });
});
};
const hideAllMenus = () => {
let menus: any = document.querySelectorAll('.key-list-right-menu');
if (menus.length === 0) {
return;
}
state.rightClickNode = null;
for (const menu of menus) {
menu.style.display = 'none';
}
};
const searchKey = async () => {
@@ -643,21 +620,4 @@ const delKey = (key: string) => {
height: 22px;
line-height: 22px;
}
/* right menu style start */
.key-list-right-menu {
display: none;
position: fixed;
top: 0;
left: 0;
padding: 5px;
z-index: 99999;
overflow: hidden;
border-radius: 3px;
border: 2px solid lightgrey;
background: #fafafa;
}
.dark-mode .key-list-right-menu {
background: #263238;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<div class="menu">
<div class="toolbar">
<el-input v-model="filterTag" placeholder="输入标签关键字过滤" style="width: 200px; margin-right: 10px" />
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="showSaveTabDialog(null)">添加</el-button>
<el-input v-model="filterTag" placeholder="输入关键字过滤(右击进行操作)" style="width: 220px; margin-right: 10px" />
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="showSaveTagDialog(null)">添加</el-button>
<div style="float: right">
<el-tooltip effect="dark" placement="top">
<template #content>
@@ -26,8 +26,10 @@
:data="data"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
@node-contextmenu="nodeContextmenu"
@node-click="treeNodeClick"
:default-expanded-keys="defaultExpandedKeys"
:expand-on-click-node="false"
:expand-on-click-node="true"
:filter-node-method="filterNode"
>
<template #default="{ data }">
@@ -39,44 +41,6 @@
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
</span>
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" :underline="false" />
<el-link v-auth="'tag:save'" @click.prevent="showEditTagDialog(data)" class="ml5" type="primary" icon="edit" :underline="false" />
<el-link v-auth="'tag:save'" @click.prevent="showSaveTabDialog(data)" icon="circle-plus" :underline="false" type="success" class="ml5" />
<!-- <el-link
v-auth="'resource:changeStatus'"
@click.prevent="changeStatus(data, -1)"
v-if="data.status === 1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
icon="circle-close"
:underline="false"
type="warning"
class="ml5"
/>
<el-link
v-auth="'resource:changeStatus'"
@click.prevent="changeStatus(data, 1)"
v-if="data.status === -1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
type="success"
icon="circle-check"
:underline="false"
plain
class="ml5"
/> -->
<el-link
v-auth="'tag:del'"
@click.prevent="deleteTag(data)"
v-if="data.children == null"
type="danger"
icon="delete"
:underline="false"
plain
class="ml5"
/>
</span>
</template>
</el-tree>
@@ -114,6 +78,8 @@
<el-descriptions-item label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</template>
@@ -122,6 +88,8 @@ import { toRefs, ref, watch, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from './api';
import { dateFormat } from '@/common/utils/date';
import Contextmenu from '@/components/contextmenu/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
interface Tree {
id: number;
@@ -133,6 +101,28 @@ interface Tree {
const tagForm: any = ref(null);
const tagTreeRef: any = ref(null);
const filterTag = ref('');
const contextmenuRef = ref();
const contextmenuInfo = new ContextmenuItem('info', '详情').withIcon('view').withOnClick((data: any) => info(data));
const contextmenuAdd = new ContextmenuItem('addTag', '添加子标签')
.withIcon('circle-plus')
.withPermission('tag:save')
.withOnClick((data: any) => showSaveTagDialog(data));
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
.withIcon('edit')
.withPermission('tag:save')
.withOnClick((data: any) => showEditTagDialog(data));
const contextmenuDel = new ContextmenuItem('delete', '删除')
.withIcon('delete')
.withPermission('tag:del')
.withHideFunc((data: any) => {
// 存在子标签,则不允许删除
return data.children;
})
.withOnClick((data: any) => deleteTag(data));
const state = reactive({
data: [],
@@ -149,6 +139,13 @@ const state = reactive({
},
// 展开的节点
defaultExpandedKeys: [] as any,
contextmenu: {
dropdown: {
x: 0,
y: 0,
},
items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel],
},
});
const { data, saveTabDialog, infoDialog, defaultExpandedKeys } = toRefs(state);
@@ -188,12 +185,25 @@ const search = async () => {
state.data = res;
};
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data);
};
const treeNodeClick = () => {
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
};
const info = async (data: any) => {
state.infoDialog.data = data;
state.infoDialog.visible = true;
};
const showSaveTabDialog = (data: any) => {
const showSaveTagDialog = (data: any) => {
if (data) {
state.saveTabDialog.form.pid = data.id;
state.saveTabDialog.title = `新增 [${data.codePath}] 子标签信息`;
@@ -300,3 +310,4 @@ const removeDeafultExpandId = (id: any) => {
user-select: none;
}
</style>
@/components/contextmenu

View File

@@ -2,7 +2,7 @@
<div class="menu">
<div class="toolbar">
<div>
<span style="font-size: 14px"> <SvgIcon name="info-filled" />红色橙色字体表示禁用状态 </span>
<span style="font-size: 14px"> <SvgIcon name="info-filled" />红色橙色字体表示禁用状态 (右击资源进行操作) </span>
</div>
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="addResource(false)">添加</el-button>
</div>
@@ -14,8 +14,10 @@
:data="data"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
@node-contextmenu="nodeContextmenu"
@node-click="treeNodeClick"
:default-expanded-keys="defaultExpandedKeys"
:expand-on-click-node="false"
:expand-on-click-node="true"
draggable
:allow-drop="allowDrop"
@node-drop="handleDrop"
@@ -34,43 +36,6 @@
<span :style="data.status == 1 ? 'color: #67c23a;' : 'color: #f67c6c;'">{{ data.name }}</span>
<span style="color: #3c8dbc"></span>
</span>
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" :underline="false" />
<el-link v-auth="perms.updateResource" @click.prevent="editResource(data)" class="ml5" type="primary" icon="edit" :underline="false" />
<el-link
v-auth="perms.addResource"
@click.prevent="addResource(data)"
v-if="data.type === menuTypeValue"
icon="circle-plus"
:underline="false"
type="success"
class="ml5"
/>
<el-link
v-auth="perms.changeStatus"
@click.prevent="changeStatus(data, -1)"
v-if="data.status === 1"
icon="circle-close"
:underline="false"
type="warning"
class="ml5"
/>
<el-link
v-auth="perms.changeStatus"
@click.prevent="changeStatus(data, 1)"
v-if="data.status === -1"
type="success"
icon="circle-check"
:underline="false"
plain
class="ml5"
/>
<el-link v-auth="perms.delResource" @click.prevent="deleteMenu(data)" type="danger" icon="delete" :underline="false" plain class="ml5" />
</span>
</template>
</el-tree>
@@ -123,17 +88,21 @@
<el-descriptions-item label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
</el-descriptions>
</el-dialog>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { ref, toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ResourceEdit from './ResourceEdit.vue';
import { ResourceTypeEnum } from '../enums';
import { resourceApi } from '../api';
import { dateFormat } from '@/common/utils/date';
import EnumValue from '@/common/Enum';
import Contextmenu from '@/components/contextmenu/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
const menuTypeValue = ResourceTypeEnum.Menu.value;
const permissionTypeValue = ResourceTypeEnum.Permission.value;
@@ -150,7 +119,46 @@ const props = {
children: 'children',
};
const contextmenuRef = ref();
const contextmenuInfo = new ContextmenuItem('info', '详情').withIcon('View').withOnClick((data: any) => info(data));
const contextmenuAdd = new ContextmenuItem('add', '添加子资源')
.withIcon('circle-plus')
.withPermission(perms.addResource)
.withHideFunc((data: any) => data.type !== menuTypeValue)
.withOnClick((data: any) => addResource(data));
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
.withIcon('edit')
.withPermission(perms.updateResource)
.withOnClick((data: any) => editResource(data));
const contextmenuEnable = new ContextmenuItem('enable', '启用')
.withIcon('circle-check')
.withPermission(perms.updateResource)
.withHideFunc((data: any) => data.status === 1)
.withOnClick((data: any) => changeStatus(data, 1));
const contextmenuDisable = new ContextmenuItem('disable', '禁用')
.withIcon('circle-close')
.withPermission(perms.updateResource)
.withHideFunc((data: any) => data.status === -1)
.withOnClick((data: any) => changeStatus(data, -1));
const contextmenuDel = new ContextmenuItem('delete', '删除')
.withIcon('delete')
.withPermission(perms.delResource)
.withOnClick((data: any) => deleteMenu(data));
const state = reactive({
contextmenu: {
dropdown: {
x: 0,
y: 0,
},
items: [contextmenuInfo, contextmenuAdd, contextmenuEdit, contextmenuEnable, contextmenuDisable, contextmenuDel],
},
//弹出框对象
dialogForm: {
type: null,
@@ -193,6 +201,19 @@ const search = async () => {
state.data = res;
};
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data);
};
const treeNodeClick = () => {
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
};
const deleteMenu = (data: any) => {
ElMessageBox.confirm(`此操作将删除 [${data.name}], 是否继续?`, '提示', {
confirmButtonText: '确定',
@@ -378,3 +399,4 @@ const info = async (data: any) => {
user-select: none;
}
</style>
@/components/contextmenu

View File

@@ -150,7 +150,7 @@ func (r *resourceAppImpl) checkCode(code string) error {
if strings.Contains(code, ",") {
return errorx.NewBiz("code不能包含','")
}
if gormx.CountBy(&entity.Resource{Code: code}) == 0 {
if gormx.CountBy(&entity.Resource{Code: code}) != 0 {
return errorx.NewBiz("该code已存在")
}
return nil