mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
refactor: contextmenu组件优化、标签&资源替换为contextmenu操作
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
55
mayfly_go_web/src/components/contextmenu/index.ts
Normal file
55
mayfly_go_web/src/components/contextmenu/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 条件查询,点击列信息后显示输入对应的值
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user