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

This commit is contained in:
O96u committed 2026-08-31 16:05:53 +08:00
1 parent 81dcc2c15e
commit 270af4e707
22 files changed
+587 -52

No files matched your search

+84 -3
View File
@@ -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
View File
@@ -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>