diff --git a/src/renderer/manage/pages/BucketPage.vue b/src/renderer/manage/pages/BucketPage.vue index 0ce787cb..5bb14288 100644 --- a/src/renderer/manage/pages/BucketPage.vue +++ b/src/renderer/manage/pages/BucketPage.vue @@ -84,7 +84,7 @@ @@ -417,27 +417,34 @@ -
- -
+
- {{ t(`pages.manage.bucket.linkFormat.${format}`) }} +
+ {{ t(`pages.manage.bucket.linkFormat.${format}`) }} +
+
+ {{ t('pages.manage.bucket.linkFormat.presign') }} +
-
- {{ t('pages.manage.bucket.linkFormat.presign') }} -
-
+
@@ -1246,6 +1253,7 @@ const isContentFullscreen = ref(false) const copyDropdownOpen = ref(false) const sortDropdownOpen = ref(false) const copyDropdownIndex = ref(-1) +const dropdownPositions = ref(new Map()) const gridBreakpoints = ref([ { min: 0, cols: 1 }, { min: 380, cols: 2 }, @@ -2492,11 +2500,13 @@ function handleBatchCopyInfo() { async function copyLink(item: any, type: string) { copyToClipboard(await formatLink(item.url, item.fileName, type, manageStore.config.settings.customPasteFormat)) + copyDropdownIndex.value = -1 } async function handleBatchCopyLink(type: string) { if (!selectedItems.value.length) { message.warning(t('pages.manage.bucket.selectFileMsg')) + copyDropdownOpen.value = false return } const result = [] as string[] @@ -2514,6 +2524,7 @@ async function handleBatchCopyLink(type: string) { } window.electron.clipboard.writeText(result.join('\n')) message.success(`${t('pages.manage.bucket.copySuccess')}`) + copyDropdownOpen.value = false } async function cancelLoading() { @@ -2860,13 +2871,77 @@ async function getPreSignedUrl(item: any) { function copyToClipboard(text: string) { window.electron.clipboard.writeText(text) message.success(t('pages.manage.bucket.copySuccess')) + copyDropdownIndex.value = -1 } -function toggleCopyDropdown(index: number) { +function toggleCopyDropdown(index: number, event?: MouseEvent) { if (copyDropdownIndex.value === index) { copyDropdownIndex.value = -1 } else { copyDropdownIndex.value = index + + if (event) { + const button = event.currentTarget as HTMLElement + const rect = button.getBoundingClientRect() + const viewportWidth = window.innerWidth + const viewportHeight = window.innerHeight + + const container = bucketContainerRef.value?.$el || bucketContainerRef.value + const containerRect = container?.getBoundingClientRect() + const dropdownWidth = 160 + const shouldShowLeft = + rect.right > viewportWidth - dropdownWidth || + (containerRect && rect.right > containerRect.right - dropdownWidth) + + const dropdownHeight = 200 + const shouldShowUp = + rect.bottom > viewportHeight - dropdownHeight || + (containerRect && rect.bottom > containerRect.bottom - dropdownHeight) + + dropdownPositions.value.set(index, { + left: shouldShowLeft, + up: shouldShowUp, + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + } as any) + } + } +} + +function getDropdownStyle(index: number) { + const pos: any = dropdownPositions.value.get(index) + if (!pos) return { display: 'none' as const } + const estWidth = 180 + const estHeight = 240 + let left = pos.left ? pos.x + pos.width - estWidth : pos.x + let top = pos.up ? pos.y - estHeight : pos.y + pos.height + const vw = window.innerWidth + const vh = window.innerHeight + if (left + estWidth > vw - 4) left = vw - estWidth - 4 + if (left < 4) left = 4 + if (top + estHeight > vh - 4) top = vh - estHeight - 4 + if (top < 4) top = 4 + return { + position: 'fixed' as const, + left: left + 'px', + top: top + 'px', + maxHeight: '240px', + zIndex: 4000, + minWidth: '140px', + maxWidth: '200px' + } +} + +function handleClickOutside(event: MouseEvent) { + const target = event.target as HTMLElement + if (!target.closest('.file-actions-dropdown') && !target.closest('[data-floating-dropdown]')) { + copyDropdownIndex.value = -1 + } + if (!target.closest('.dropdown')) { + copyDropdownOpen.value = false + sortDropdownOpen.value = false } } @@ -2910,11 +2985,13 @@ onBeforeMount(async () => { isShowLoadingPage.value = false document.addEventListener('keydown', handleDetectShiftKey) document.addEventListener('keyup', handleDetectShiftKey) + document.addEventListener('click', handleClickOutside) }) onBeforeUnmount(() => { document.removeEventListener('keydown', handleDetectShiftKey) document.removeEventListener('keyup', handleDetectShiftKey) + document.removeEventListener('click', handleClickOutside) fileTransferInterval && clearInterval(fileTransferInterval) downloadInterval && clearInterval(downloadInterval) refreshUploadTaskId.value && clearInterval(refreshUploadTaskId.value) diff --git a/src/renderer/manage/pages/css/BucketPage.css b/src/renderer/manage/pages/css/BucketPage.css index fa9a7afd..d539fabf 100644 --- a/src/renderer/manage/pages/css/BucketPage.css +++ b/src/renderer/manage/pages/css/BucketPage.css @@ -7,6 +7,7 @@ padding: 1rem; gap: 1rem; overflow: hidden; + position: relative; } /* Header Card */ @@ -359,7 +360,7 @@ position: absolute; top: 100%; left: 0; - background: var(--color-surface); + background: var(--color-background-primary); border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); @@ -407,15 +408,18 @@ .content-area { flex: 1; - padding: 0.75rem; - overflow: hidden; + padding: 0.5rem; + overflow-x: visible; + overflow-y: hidden; background: var(--color-background-secondary); + position: relative; } /* Virtual Scroller Container */ .virtual-scroller-container { height: 100%; width: 100%; + overflow: visible; } /* File Grid Item */ @@ -425,21 +429,25 @@ background: var(--color-surface); border: 1px solid var(--color-border-secondary); border-radius: var(--radius-lg); - overflow: hidden; + overflow: visible; transition: var(--transition-medium); cursor: pointer; height: 220px; + position: relative; + z-index: 1; } .file-grid-item:hover { border-color: var(--color-border); box-shadow: var(--shadow-md); transform: translateY(-2px); + z-index: 10; } .file-grid-item.selected { border-color: var(--color-accent); box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); + z-index: 10; } .file-preview { @@ -471,6 +479,8 @@ display: flex; flex-direction: column; justify-content: space-between; + position: relative; + z-index: 1; } .file-name { @@ -501,6 +511,8 @@ display: flex; justify-content: space-between; align-items: center; + position: relative; + z-index: 10; } .file-action-group { @@ -639,6 +651,7 @@ /* File Actions Dropdown */ .file-actions-dropdown { position: relative; + z-index: 100; } .file-actions-dropdown-content { @@ -649,10 +662,56 @@ border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); - z-index: 1000; + z-index: 3000; min-width: 140px; + max-width: 200px; margin-top: 0.25rem; - overflow: hidden; + overflow: visible; + white-space: nowrap; + transform-origin: top right; + animation: dropdown-appear 0.15s ease-out; + max-height: 300px; + overflow-y: auto; +} + +@keyframes dropdown-appear { + from { + opacity: 0; + transform: scale(0.95) translateY(-5px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.file-actions-dropdown-content.position-left { + right: auto; + left: 0; + transform-origin: top left; +} + +.file-actions-dropdown-content.position-up { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.25rem; + transform-origin: bottom right; +} + +.file-actions-dropdown-content.position-up.position-left { + transform-origin: bottom left; +} + +.content-fullscreen .file-actions-dropdown-content { + z-index: 4000; +} + +@media (max-width: 768px) { + .file-actions-dropdown-content { + min-width: 120px; + max-width: 150px; + } } .file-actions-dropdown-item { @@ -1077,6 +1136,7 @@ input:checked + .switch-slider:before { /* File Actions Dropdown */ .file-actions-dropdown { position: relative; + z-index: 100; } .file-actions-dropdown-content { @@ -1087,14 +1147,52 @@ input:checked + .switch-slider:before { border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); - z-index: 1000; + z-index: 3000; min-width: 120px; + max-width: 200px; margin-top: 0.25rem; + white-space: nowrap; + overflow: visible; + transform-origin: top right; + animation: dropdown-appear 0.15s ease-out; + /* Ensure dropdown is never clipped */ + max-height: 300px; + overflow-y: auto; +} + +.file-actions-dropdown-content.position-left { + right: auto; + left: 0; + transform-origin: top left; +} + +.file-actions-dropdown-content.position-up { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.25rem; + transform-origin: bottom right; +} + +.file-actions-dropdown-content.position-up.position-left { + transform-origin: bottom left; +} + +.content-fullscreen .file-actions-dropdown-content { + z-index: 4000; +} + +@media (max-width: 768px) { + .file-actions-dropdown-content { + min-width: 100px; + max-width: 140px; + } } .file-actions-dropdown-item { padding: 0.5rem 0.75rem; cursor: pointer; + background: var(--color-background-primary); transition: var(--transition-fast); font-size: 0.875rem; color: var(--color-text-primary); @@ -1104,8 +1202,7 @@ input:checked + .switch-slider:before { } .file-actions-dropdown-item:hover { - background: var(--color-surface-elevated); - color: var(--color-accent); + color: var(--color-blue-common); } /* Empty State */ @@ -1162,6 +1259,7 @@ input:checked + .switch-slider:before { .file-grid-item { height: 240px; + overflow: auto; } .drawer-container { @@ -1246,12 +1344,12 @@ input:checked + .switch-slider:before { flex: 1; display: flex; flex-direction: column; - overflow: hidden; + overflow: visible; } .content-fullscreen .virtual-scroller-container { flex: 1; - overflow: hidden; + overflow: visible; } /* Responsive adjustments for fullscreen */