Add batch download zip on homepage

This commit is contained in:
2025-12-20 18:05:47 +08:00
parent 8852ce6fc2
commit d17b9d2136
3 changed files with 83 additions and 0 deletions

View File

@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"fflate": "^0.8.2",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
@@ -1656,6 +1657,12 @@
} }
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"fflate": "^0.8.2",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { zipSync } from 'fflate'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { compressFile, getSubscription, getUsage, sendVerification, type CompressResponse } from '@/services/api' import { compressFile, getSubscription, getUsage, sendVerification, type CompressResponse } from '@/services/api'
@@ -32,6 +33,7 @@ const busy = computed(() => items.value.some((x) => x.status === 'compressing'))
const alert = ref<{ type: 'info' | 'success' | 'error'; message: string } | null>(null) const alert = ref<{ type: 'info' | 'success' | 'error'; message: string } | null>(null)
const sendingVerification = ref(false) const sendingVerification = ref(false)
const batchDownloading = ref(false)
const quotaLoading = ref(false) const quotaLoading = ref(false)
const quotaError = ref<string | null>(null) const quotaError = ref<string | null>(null)
const usage = ref<Awaited<ReturnType<typeof getUsage>> | null>(null) const usage = ref<Awaited<ReturnType<typeof getUsage>> | null>(null)
@@ -165,6 +167,71 @@ async function download(item: UploadItem) {
URL.revokeObjectURL(objectUrl) URL.revokeObjectURL(objectUrl)
} }
function sanitizeFileName(name: string) {
const trimmed = name.trim()
if (!trimmed) return 'image'
return trimmed.replace(/[\\/:*?"<>|\r\n]+/g, '_').slice(0, 120)
}
function uniqueFileName(base: string, ext: string, used: Set<string>) {
const safeBase = sanitizeFileName(base)
let candidate = `${safeBase}.${ext}`
if (!used.has(candidate)) {
used.add(candidate)
return candidate
}
let idx = 2
while (used.has(`${safeBase}-${idx}.${ext}`)) idx += 1
candidate = `${safeBase}-${idx}.${ext}`
used.add(candidate)
return candidate
}
async function downloadAll() {
if (batchDownloading.value) return
const readyItems = items.value.filter((item) => item.status === 'done' && item.result)
if (!readyItems.length) return
batchDownloading.value = true
alert.value = null
try {
const files: Record<string, Uint8Array> = {}
const usedNames = new Set<string>()
for (const item of readyItems) {
if (!item.result) continue
const url = item.result.download_url
const headers = auth.token ? { authorization: `Bearer ${auth.token}` } : undefined
const res = await fetch(url, { headers, credentials: 'include' })
if (!res.ok) {
throw new Error(`下载失败HTTP ${res.status}${item.file.name}`)
}
const data = new Uint8Array(await res.arrayBuffer())
const base = item.file.name.replace(/\.[^/.]+$/, '')
const ext = item.result.format_out === 'jpeg' ? 'jpg' : item.result.format_out
const name = uniqueFileName(base || 'image', ext, usedNames)
files[name] = data
}
const zipped = zipSync(files, { level: 6 })
const zipBytes = new Uint8Array(zipped)
const blob = new Blob([zipBytes], { type: 'application/zip' })
const objectUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
a.href = objectUrl
a.download = `imageforge-${stamp}.zip`
a.click()
URL.revokeObjectURL(objectUrl)
} catch (err) {
const message = err instanceof Error ? err.message : '批量下载失败,请稍后再试'
alert.value = { type: 'error', message }
} finally {
batchDownloading.value = false
}
}
async function resendVerification() { async function resendVerification() {
if (!auth.token) return if (!auth.token) return
sendingVerification.value = true sendingVerification.value = true
@@ -265,6 +332,14 @@ async function resendVerification() {
> >
开始压缩 开始压缩
</button> </button>
<button
type="button"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-50"
:disabled="busy || batchDownloading || items.every((x) => x.status !== 'done' || !x.result)"
@click="downloadAll"
>
{{ batchDownloading ? '打包中…' : '批量下载' }}
</button>
<button <button
type="button" type="button"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-50" class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 disabled:opacity-50"