🐛 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

View File

@@ -84,7 +84,7 @@
<button
class="action-button primary"
:class="{ 'action-button': selectedItems.length === 0 }"
@click="handleBatchCopyLink(manageStore.config.settings.pasteFormat)"
@click="copyDropdownOpen = !copyDropdownOpen"
>
<CopyIcon class="action-icon" />
</button>
@@ -417,27 +417,34 @@
</button>
<!-- Copy Link Dropdown -->
<div class="file-actions-dropdown">
<button class="file-action-button" @click.stop="toggleCopyDropdown(index)">
<div class="file-actions-dropdown" :data-dropdown-index="index">
<button class="file-action-button" @click.stop="toggleCopyDropdown(index, $event)">
<CopyIcon class="action-icon" />
</button>
<div v-if="copyDropdownIndex === index" class="file-actions-dropdown-content">
<teleport to="body">
<div
v-for="format in linkFormatList"
:key="format"
class="file-actions-dropdown-item"
@click.stop="copyLink(item, format)"
v-if="copyDropdownIndex === index"
class="file-actions-dropdown-content floating"
:style="getDropdownStyle(index)"
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
v-if="isShowPresignedUrl"
class="file-actions-dropdown-item"
@click.stop="async () => copyToClipboard(await getPreSignedUrl(item))"
>
{{ t('pages.manage.bucket.linkFormat.presign') }}
</div>
</div>
</teleport>
</div>
<!-- File Info -->
@@ -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<number, { left: boolean; up: boolean }>())
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)

View File

@@ -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 */