Add batch download zip on homepage
This commit is contained in:
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"fflate": "^0.8.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.24",
|
||||
"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": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"fflate": "^0.8.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { zipSync } from 'fflate'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
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 sendingVerification = ref(false)
|
||||
const batchDownloading = ref(false)
|
||||
const quotaLoading = ref(false)
|
||||
const quotaError = ref<string | null>(null)
|
||||
const usage = ref<Awaited<ReturnType<typeof getUsage>> | null>(null)
|
||||
@@ -165,6 +167,71 @@ async function download(item: UploadItem) {
|
||||
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() {
|
||||
if (!auth.token) return
|
||||
sendingVerification.value = true
|
||||
@@ -265,6 +332,14 @@ async function resendVerification() {
|
||||
>
|
||||
开始压缩
|
||||
</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
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user