🐛 Fix(custom): fix copy button dropdown ui

ISSUES CLOSED: #363
This commit is contained in:
Kuingsmile
2025-08-18 15:18:36 +08:00
parent 228b96537b
commit 49e139f835
2 changed files with 204 additions and 29 deletions
+95 -18
View File
@@ -84,7 +84,7 @@
<button <button
class="action-button primary" class="action-button primary"
:class="{ 'action-button': selectedItems.length === 0 }" :class="{ 'action-button': selectedItems.length === 0 }"
@click="handleBatchCopyLink(manageStore.config.settings.pasteFormat)" @click="copyDropdownOpen = !copyDropdownOpen"
> >
<CopyIcon class="action-icon" /> <CopyIcon class="action-icon" />
</button> </button>
@@ -417,27 +417,34 @@
</button> </button>
<!-- Copy Link Dropdown --> <!-- Copy Link Dropdown -->
<div class="file-actions-dropdown"> <div class="file-actions-dropdown" :data-dropdown-index="index">
<button class="file-action-button" @click.stop="toggleCopyDropdown(index)"> <button class="file-action-button" @click.stop="toggleCopyDropdown(index, $event)">
<CopyIcon class="action-icon" /> <CopyIcon class="action-icon" />
</button> </button>
<div v-if="copyDropdownIndex === index" class="file-actions-dropdown-content"> <teleport to="body">
<div <div
v-for="format in linkFormatList" v-if="copyDropdownIndex === index"
:key="format" class="file-actions-dropdown-content floating"
class="file-actions-dropdown-item" :style="getDropdownStyle(index)"
@click.stop="copyLink(item, format)" data-floating-dropdown
> >
{{ t(`pages.manage.bucket.linkFormat.${format}`) }} <div
v-for="format in linkFormatList"
:key="format"
class="file-actions-dropdown-item"
@click.stop="copyLink(item, format)"
>
{{ t(`pages.manage.bucket.linkFormat.${format}`) }}
</div>
<div
v-if="isShowPresignedUrl"
class="file-actions-dropdown-item"
@click.stop="async () => copyToClipboard(await getPreSignedUrl(item))"
>
{{ t('pages.manage.bucket.linkFormat.presign') }}
</div>
</div> </div>
<div </teleport>
v-if="isShowPresignedUrl"
class="file-actions-dropdown-item"
@click.stop="async () => copyToClipboard(await getPreSignedUrl(item))"
>
{{ t('pages.manage.bucket.linkFormat.presign') }}
</div>
</div>
</div> </div>
<!-- File Info --> <!-- File Info -->
@@ -1246,6 +1253,7 @@ const isContentFullscreen = ref(false)
const copyDropdownOpen = ref(false) const copyDropdownOpen = ref(false)
const sortDropdownOpen = ref(false) const sortDropdownOpen = ref(false)
const copyDropdownIndex = ref(-1) const copyDropdownIndex = ref(-1)
const dropdownPositions = ref(new Map<number, { left: boolean; up: boolean }>())
const gridBreakpoints = ref([ const gridBreakpoints = ref([
{ min: 0, cols: 1 }, { min: 0, cols: 1 },
{ min: 380, cols: 2 }, { min: 380, cols: 2 },
@@ -2492,11 +2500,13 @@ function handleBatchCopyInfo() {
async function copyLink(item: any, type: string) { async function copyLink(item: any, type: string) {
copyToClipboard(await formatLink(item.url, item.fileName, type, manageStore.config.settings.customPasteFormat)) copyToClipboard(await formatLink(item.url, item.fileName, type, manageStore.config.settings.customPasteFormat))
copyDropdownIndex.value = -1
} }
async function handleBatchCopyLink(type: string) { async function handleBatchCopyLink(type: string) {
if (!selectedItems.value.length) { if (!selectedItems.value.length) {
message.warning(t('pages.manage.bucket.selectFileMsg')) message.warning(t('pages.manage.bucket.selectFileMsg'))
copyDropdownOpen.value = false
return return
} }
const result = [] as string[] const result = [] as string[]
@@ -2514,6 +2524,7 @@ async function handleBatchCopyLink(type: string) {
} }
window.electron.clipboard.writeText(result.join('\n')) window.electron.clipboard.writeText(result.join('\n'))
message.success(`${t('pages.manage.bucket.copySuccess')}`) message.success(`${t('pages.manage.bucket.copySuccess')}`)
copyDropdownOpen.value = false
} }
async function cancelLoading() { async function cancelLoading() {
@@ -2860,13 +2871,77 @@ async function getPreSignedUrl(item: any) {
function copyToClipboard(text: string) { function copyToClipboard(text: string) {
window.electron.clipboard.writeText(text) window.electron.clipboard.writeText(text)
message.success(t('pages.manage.bucket.copySuccess')) message.success(t('pages.manage.bucket.copySuccess'))
copyDropdownIndex.value = -1
} }
function toggleCopyDropdown(index: number) { function toggleCopyDropdown(index: number, event?: MouseEvent) {
if (copyDropdownIndex.value === index) { if (copyDropdownIndex.value === index) {
copyDropdownIndex.value = -1 copyDropdownIndex.value = -1
} else { } else {
copyDropdownIndex.value = index 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 isShowLoadingPage.value = false
document.addEventListener('keydown', handleDetectShiftKey) document.addEventListener('keydown', handleDetectShiftKey)
document.addEventListener('keyup', handleDetectShiftKey) document.addEventListener('keyup', handleDetectShiftKey)
document.addEventListener('click', handleClickOutside)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('keydown', handleDetectShiftKey) document.removeEventListener('keydown', handleDetectShiftKey)
document.removeEventListener('keyup', handleDetectShiftKey) document.removeEventListener('keyup', handleDetectShiftKey)
document.removeEventListener('click', handleClickOutside)
fileTransferInterval && clearInterval(fileTransferInterval) fileTransferInterval && clearInterval(fileTransferInterval)
downloadInterval && clearInterval(downloadInterval) downloadInterval && clearInterval(downloadInterval)
refreshUploadTaskId.value && clearInterval(refreshUploadTaskId.value) refreshUploadTaskId.value && clearInterval(refreshUploadTaskId.value)
+109 -11
View File
@@ -7,6 +7,7 @@
padding: 1rem; padding: 1rem;
gap: 1rem; gap: 1rem;
overflow: hidden; overflow: hidden;
position: relative;
} }
/* Header Card */ /* Header Card */
@@ -359,7 +360,7 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
background: var(--color-surface); background: var(--color-background-primary);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
@@ -407,15 +408,18 @@
.content-area { .content-area {
flex: 1; flex: 1;
padding: 0.75rem; padding: 0.5rem;
overflow: hidden; overflow-x: visible;
overflow-y: hidden;
background: var(--color-background-secondary); background: var(--color-background-secondary);
position: relative;
} }
/* Virtual Scroller Container */ /* Virtual Scroller Container */
.virtual-scroller-container { .virtual-scroller-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: visible;
} }
/* File Grid Item */ /* File Grid Item */
@@ -425,21 +429,25 @@
background: var(--color-surface); background: var(--color-surface);
border: 1px solid var(--color-border-secondary); border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: visible;
transition: var(--transition-medium); transition: var(--transition-medium);
cursor: pointer; cursor: pointer;
height: 220px; height: 220px;
position: relative;
z-index: 1;
} }
.file-grid-item:hover { .file-grid-item:hover {
border-color: var(--color-border); border-color: var(--color-border);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
transform: translateY(-2px); transform: translateY(-2px);
z-index: 10;
} }
.file-grid-item.selected { .file-grid-item.selected {
border-color: var(--color-accent); border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
z-index: 10;
} }
.file-preview { .file-preview {
@@ -471,6 +479,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
position: relative;
z-index: 1;
} }
.file-name { .file-name {
@@ -501,6 +511,8 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: relative;
z-index: 10;
} }
.file-action-group { .file-action-group {
@@ -639,6 +651,7 @@
/* File Actions Dropdown */ /* File Actions Dropdown */
.file-actions-dropdown { .file-actions-dropdown {
position: relative; position: relative;
z-index: 100;
} }
.file-actions-dropdown-content { .file-actions-dropdown-content {
@@ -649,10 +662,56 @@
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
z-index: 1000; z-index: 3000;
min-width: 140px; min-width: 140px;
max-width: 200px;
margin-top: 0.25rem; 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 { .file-actions-dropdown-item {
@@ -1077,6 +1136,7 @@ input:checked + .switch-slider:before {
/* File Actions Dropdown */ /* File Actions Dropdown */
.file-actions-dropdown { .file-actions-dropdown {
position: relative; position: relative;
z-index: 100;
} }
.file-actions-dropdown-content { .file-actions-dropdown-content {
@@ -1087,14 +1147,52 @@ input:checked + .switch-slider:before {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
z-index: 1000; z-index: 3000;
min-width: 120px; min-width: 120px;
max-width: 200px;
margin-top: 0.25rem; 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 { .file-actions-dropdown-item {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
cursor: pointer; cursor: pointer;
background: var(--color-background-primary);
transition: var(--transition-fast); transition: var(--transition-fast);
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-primary); color: var(--color-text-primary);
@@ -1104,8 +1202,7 @@ input:checked + .switch-slider:before {
} }
.file-actions-dropdown-item:hover { .file-actions-dropdown-item:hover {
background: var(--color-surface-elevated); color: var(--color-blue-common);
color: var(--color-accent);
} }
/* Empty State */ /* Empty State */
@@ -1162,6 +1259,7 @@ input:checked + .switch-slider:before {
.file-grid-item { .file-grid-item {
height: 240px; height: 240px;
overflow: auto;
} }
.drawer-container { .drawer-container {
@@ -1246,12 +1344,12 @@ input:checked + .switch-slider:before {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: visible;
} }
.content-fullscreen .virtual-scroller-container { .content-fullscreen .virtual-scroller-container {
flex: 1; flex: 1;
overflow: hidden; overflow: visible;
} }
/* Responsive adjustments for fullscreen */ /* Responsive adjustments for fullscreen */