feat: File list supports right menu operation (#10056)

This commit is contained in:
2025-08-19 17:31:15 +08:00 committed by GitHub
parent fd1e0e72a0
commit 0637ca308b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 68 additions and 180 deletions

View file

@ -46,25 +46,22 @@
</slot>
</div>
<div
<ul
v-if="rightClick.visible"
class="custom-context-menu"
:style="{
left: rightClick.left + 'px',
top: rightClick.top + 'px',
}"
class="context-menu"
ref="menuRef"
:style="{ top: `${adjustedY}px`, left: `${adjustedX}px` }"
@click.stop
>
<el-button
class="no-border-button"
v-for="(btn, i) in rightButtons"
:disabled="disabled(btn)"
@click="rightButtonClick(btn)"
:key="i"
:command="btn"
<li
v-for="(btn, index) in rightButtons"
:key="index"
:class="[{ disabled: disabled(btn) }, { divided: btn.divided }]"
@click="!disabled(btn) && rightButtonClick(btn)"
>
{{ btn.label }}
</el-button>
</div>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
@ -105,6 +102,7 @@ const mobile = computed(() => {
});
const tableRef = ref();
const tableHeight = ref(0);
const menuRef = ref<HTMLElement | null>(null);
const rightClick = ref({
visible: false,
@ -164,6 +162,34 @@ function clearSort() {
tableRef.value.refElTable.clearSort();
}
const adjustedX = ref(rightClick.value.left);
const adjustedY = ref(rightClick.value.top);
watch(
() => [rightClick.value.left, rightClick.value.top],
async () => {
await nextTick();
if (!menuRef.value) return;
const menuRect = menuRef.value.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (rightClick.value.left + menuRect.width > windowWidth) {
adjustedX.value = windowWidth - menuRect.width - 4;
} else {
adjustedX.value = rightClick.value.left;
}
if (rightClick.value.top + menuRect.height > windowHeight) {
adjustedY.value = windowHeight - menuRect.height - 4;
} else {
adjustedY.value = rightClick.value.top;
}
},
{ immediate: true },
);
defineExpose({
clearSelects,
sort,
@ -197,7 +223,7 @@ onMounted(() => {
});
</script>
<style lang="scss">
<style scoped lang="scss">
@use '@/styles/mixins.scss' as *;
.complex-table {
@ -223,20 +249,32 @@ onMounted(() => {
@include flex-row(flex-end);
}
}
.custom-context-menu {
.context-menu {
position: fixed;
z-index: 9999;
border: 1px solid #dcdfe6;
background: var(--panel-main-bg-color-9);
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 0;
width: 80px;
color: var(--el-color-primary);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
list-style: none;
font-size: 14px;
padding: 4px 0;
margin: 0;
z-index: 9999;
min-width: 120px;
}
.no-border-button {
border: 0;
border-radius: 4;
margin: 0 !important;
width: 100%;
text-align: left;
.context-menu li {
padding: 6px 12px;
cursor: pointer;
}
.context-menu li:hover {
background-color: var(--panel-menu-bg-color);
}
.context-menu li.disabled {
color: var(--el-border-color);
cursor: not-allowed;
}
.context-menu li.divided {
border-top: 1px solid var(--el-border-color);
}
</style>

View file

@ -1,105 +0,0 @@
<template>
<ul class="context-menu" ref="menuRef" :style="{ top: `${adjustedY}px`, left: `${adjustedX}px` }" @click.stop>
<li
v-for="(btn, index) in buttons"
:key="index"
:class="[{ disabled: isDisabled(btn) }, { divided: btn.divided }]"
@click="!isDisabled(btn) && onClick(btn)"
>
{{ btn.label }}
</li>
</ul>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
x: number;
y: number;
row: any;
buttons: {
label: string;
click: (row: any) => void;
disabled?: boolean | ((row: any) => boolean);
divided?: boolean;
}[];
}>();
const emit = defineEmits(['close']);
const menuRef = ref<HTMLElement | null>(null);
const onClick = (btn: any) => {
btn.click?.(props.row);
emit('close');
};
const isDisabled = computed(() => {
return function (btn: any) {
return typeof btn.disabled === 'function' ? btn.disabled(props.row) : btn.disabled;
};
});
const adjustedX = ref(props.x);
const adjustedY = ref(props.y);
watch(
() => [props.x, props.y],
async () => {
await nextTick(); //
if (!menuRef.value) return;
const menuRect = menuRef.value.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
//
if (props.x + menuRect.width > windowWidth) {
adjustedX.value = windowWidth - menuRect.width - 4; //
} else {
adjustedX.value = props.x;
}
//
if (props.y + menuRect.height > windowHeight) {
adjustedY.value = windowHeight - menuRect.height - 4;
} else {
adjustedY.value = props.y;
}
},
{ immediate: true },
);
const handleClickOutside = () => emit('close');
onMounted(() => document.addEventListener('click', () => handleClickOutside));
onUnmounted(() => document.removeEventListener('click', () => handleClickOutside));
</script>
<style scoped>
.context-menu {
position: fixed;
background: var(--panel-main-bg-color-9);
border: 1px solid var(--el-border-color);
color: var(--el-color-primary);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
list-style: none;
font-size: 14px;
padding: 4px 0;
margin: 0;
z-index: 9999;
min-width: 120px;
}
.context-menu li {
padding: 6px 12px;
cursor: pointer;
}
.context-menu li:hover {
background-color: var(--panel-menu-bg-color);
}
.context-menu li.disabled {
color: var(--el-border-color);
cursor: not-allowed;
}
.context-menu li.divided {
border-top: 1px solid var(--el-border-color);
}
</style>

View file

@ -1,11 +1,5 @@
<template>
<div
@dragover="handleDragover"
@drop="handleDrop"
@dragleave="handleDragleave"
@contextmenu.prevent="onRightClick"
@click="onRightClick"
>
<div @dragover="handleDragover" @drop="handleDrop" @dragleave="handleDragleave">
<div class="flex sm:flex-row flex-col justify-start gap-y-2 items-center gap-x-4" ref="toolRef">
<div class="flex-shrink-0 flex sm:w-min w-full items-center justify-start">
<el-tooltip :content="$t('file.back')" placement="top">
@ -394,7 +388,7 @@
@cell-mouse-enter="showFavorite"
@cell-mouse-leave="hideFavorite"
:heightDiff="300"
@row-contextmenu="openRight"
:right-buttons="buttons"
>
<el-table-column type="selection" width="30" />
<el-table-column
@ -542,14 +536,6 @@
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
<Preview ref="previewRef" />
<TerminalDialog ref="dialogTerminalRef" />
<RightContextMenu
v-if="menuVisible"
:x="menuPosition.x"
:y="menuPosition.y"
:row="currentRow"
:buttons="buttons"
@close="hideRightMenu"
/>
</LayoutContent>
</div>
</template>
@ -880,7 +866,6 @@ const top = () => {
};
const jump = async (url: string) => {
hideRightMenu();
history.splice(pointer + 1);
history.push(url);
pointer = history.length - 1;
@ -1441,36 +1426,6 @@ const handleDragleave = (event: { preventDefault: () => void }) => {
event.preventDefault();
};
const menuVisible = ref(false);
const noMenuVisible = ref(false);
const menuPosition = reactive({ x: 0, y: 0 });
const currentRow = ref<File.File>({} as File.File);
function openRight(row: File.File, _column: any, event: MouseEvent) {
noMenuVisible.value = true;
if (row.name === currentRow.value?.name && menuVisible.value) {
menuVisible.value = false;
} else {
event.preventDefault();
currentRow.value = row;
menuPosition.x = event.clientX;
menuPosition.y = event.clientY;
menuVisible.value = true;
}
}
function hideRightMenu() {
menuVisible.value = false;
}
function onRightClick() {
if (noMenuVisible.value) {
noMenuVisible.value = false;
} else {
menuVisible.value = false;
}
}
onMounted(() => {
if (localStorage.getItem('show-hidden') === null) {
localStorage.setItem('show-hidden', 'true');