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",
|
"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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user