release: v1.2.5 — 图库页、图片详情弹层与列表体验优化
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
This commit is contained in:
1 parent
81dcc2c15e
commit
270af4e707
22 files changed
+587
-52
No files matched your search
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ImageItem } from '~/types/image'
|
||||
|
||||
interface StatsResponse {
|
||||
uploadToday: number
|
||||
uploadMonth: number
|
||||
@@ -53,6 +55,8 @@ const deleteTargetKeys = ref<string[]>([])
|
||||
const batchDeleting = ref(false)
|
||||
const showScrollTop = ref(false)
|
||||
const currentStorage = ref('all')
|
||||
const previewOpen = ref(false)
|
||||
const previewImage = ref<ImageItem | null>(null)
|
||||
|
||||
const storageSelectItems = computed(() => [
|
||||
{ label: t('stats.filterStorageAll'), value: 'all' },
|
||||
@@ -203,6 +207,8 @@ async function clearSearch() {
|
||||
|
||||
function requestDelete(key: string) {
|
||||
if (!isAuthenticated.value) return
|
||||
previewOpen.value = false
|
||||
previewImage.value = null
|
||||
deleteTargetKeys.value = [key]
|
||||
deleteModalOpen.value = true
|
||||
}
|
||||
@@ -217,6 +223,23 @@ function requestBatchDelete() {
|
||||
deleteModalOpen.value = true
|
||||
}
|
||||
|
||||
function selectAllOnPage() {
|
||||
selectedKeys.value = new Set(items.value.map(item => item.key))
|
||||
}
|
||||
|
||||
function invertSelectionOnPage() {
|
||||
const next = new Set(selectedKeys.value)
|
||||
for (const item of items.value) {
|
||||
if (next.has(item.key)) next.delete(item.key)
|
||||
else next.add(item.key)
|
||||
}
|
||||
selectedKeys.value = next
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedKeys.value = new Set()
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const keys = [...deleteTargetKeys.value]
|
||||
if (!keys.length || !isAuthenticated.value) return
|
||||
@@ -279,6 +302,8 @@ async function confirmDelete() {
|
||||
|
||||
async function handleGalleryPageChange(targetPage: number) {
|
||||
selectedKeys.value = new Set()
|
||||
previewOpen.value = false
|
||||
previewImage.value = null
|
||||
try {
|
||||
await goToPage(targetPage)
|
||||
} catch (error: unknown) {
|
||||
@@ -286,6 +311,16 @@ async function handleGalleryPageChange(targetPage: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(image: ImageItem) {
|
||||
previewImage.value = image
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
function requestPreviewDelete() {
|
||||
if (!previewImage.value) return
|
||||
requestDelete(previewImage.value.key)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('scroll', onWindowScroll, { passive: true })
|
||||
const status = await fetchStatus()
|
||||
@@ -422,7 +457,7 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-3">
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-lg font-medium">
|
||||
{{ t('stats.gallery') }}
|
||||
{{ t('stats.imagesSection') }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="!galleryLoading"
|
||||
@@ -483,6 +518,45 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
>
|
||||
<span class="hidden sm:inline">{{ t('stats.clear') }}</span>
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
icon="i-lucide-check-check"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="neutral"
|
||||
class="shrink-0"
|
||||
:disabled="!items.length || galleryLoading"
|
||||
:aria-label="t('stats.selectAll')"
|
||||
@click="selectAllOnPage"
|
||||
>
|
||||
<span class="hidden sm:inline">{{ t('stats.selectAll') }}</span>
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
icon="i-lucide-shuffle"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="neutral"
|
||||
class="shrink-0"
|
||||
:disabled="!items.length || galleryLoading"
|
||||
:aria-label="t('stats.invertSelection')"
|
||||
@click="invertSelectionOnPage"
|
||||
>
|
||||
<span class="hidden sm:inline">{{ t('stats.invertSelection') }}</span>
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="selectedCount"
|
||||
type="button"
|
||||
icon="i-lucide-x-circle"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
class="shrink-0"
|
||||
:aria-label="t('stats.clearSelection')"
|
||||
@click="clearSelection"
|
||||
>
|
||||
<span class="hidden sm:inline">{{ t('stats.clearSelection') }}</span>
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
icon="i-lucide-trash-2"
|
||||
@@ -516,9 +590,16 @@ watch(isAuthenticated, async (authed, prev) => {
|
||||
show-storage
|
||||
:items="items"
|
||||
:selected-keys="selectedKeys"
|
||||
:deleting-keys="deletingKeys"
|
||||
@update:selected-keys="selectedKeys = $event"
|
||||
@delete="requestDelete"
|
||||
@preview="openPreview"
|
||||
/>
|
||||
|
||||
<ImagePreviewModal
|
||||
v-model:open="previewOpen"
|
||||
:image="previewImage"
|
||||
show-storage
|
||||
:deleting="previewImage ? deletingKeys.has(previewImage.key) : false"
|
||||
@delete="requestPreviewDelete"
|
||||
/>
|
||||
|
||||
<PaginationBar
|
||||
+24
-2
@@ -15,6 +15,8 @@ const { t } = useI18n()
|
||||
const sessionItems = ref<ImageItem[]>([])
|
||||
const deletingKeys = ref<Set<string>>(new Set())
|
||||
const emptySelection = ref(new Set<string>())
|
||||
const previewOpen = ref(false)
|
||||
const previewImage = ref<ImageItem | null>(null)
|
||||
|
||||
const showProgress = computed(
|
||||
() =>
|
||||
@@ -123,6 +125,10 @@ async function handleDelete(key: string) {
|
||||
query: { key }
|
||||
})
|
||||
sessionItems.value = sessionItems.value.filter(item => item.key !== key)
|
||||
if (previewImage.value?.key === key) {
|
||||
previewOpen.value = false
|
||||
previewImage.value = null
|
||||
}
|
||||
toast.add({ title: t('stats.deletedSingle'), color: 'success' })
|
||||
} catch (error: unknown) {
|
||||
handleAuthError(error)
|
||||
@@ -132,6 +138,16 @@ async function handleDelete(key: string) {
|
||||
deletingKeys.value = new Set(deletingKeys.value)
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(image: ImageItem) {
|
||||
previewImage.value = image
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
function requestPreviewDelete() {
|
||||
if (!previewImage.value) return
|
||||
void handleDelete(previewImage.value.key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -178,14 +194,20 @@ async function handleDelete(key: string) {
|
||||
<ImageGrid
|
||||
:items="sessionItems"
|
||||
:selected-keys="emptySelection"
|
||||
:deleting-keys="deletingKeys"
|
||||
:selectable="false"
|
||||
:show-key="false"
|
||||
:empty-text="t('upload.sessionEmpty')"
|
||||
@update:selected-keys="() => {}"
|
||||
@delete="handleDelete"
|
||||
@preview="openPreview"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<ImagePreviewModal
|
||||
v-model:open="previewOpen"
|
||||
:image="previewImage"
|
||||
:deleting="previewImage ? deletingKeys.has(previewImage.key) : false"
|
||||
@delete="requestPreviewDelete"
|
||||
/>
|
||||
</AppShell>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user