From e49ce32fc212dc9850e79af793950820d47509b4 Mon Sep 17 00:00:00 2001 From: XianYuDaXian Date: Tue, 24 Mar 2026 14:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=89=B9=E9=87=8F=E4=B8=8B=E8=BD=BD=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/admin/js/cache.js | 111 ++++++++++++++++++++++++++++++ app/static/admin/pages/cache.html | 4 ++ 2 files changed, 115 insertions(+) diff --git a/app/static/admin/js/cache.js b/app/static/admin/js/cache.js index e89f8ec..0d5baf5 100644 --- a/app/static/admin/js/cache.js +++ b/app/static/admin/js/cache.js @@ -35,6 +35,7 @@ const UI_MAP = { selectedCount: 'selected-count', batchActions: 'batch-actions', loadBtn: 'btn-load-stats', + downloadBtn: 'btn-download-assets', deleteBtn: 'btn-delete-assets', localCacheLists: 'local-cache-lists', localImageList: 'local-image-list', @@ -804,6 +805,19 @@ function updateLoadButton() { } } +function updateDownloadButton() { + const btn = ui.downloadBtn; + if (!btn) return; + if (currentSection === 'online') { + btn.classList.add('hidden'); + btn.title = ''; + } else { + btn.classList.remove('hidden'); + btn.textContent = '下载'; + btn.title = ''; + } +} + function updateDeleteButton() { const btn = ui.deleteBtn; if (!btn) return; @@ -819,6 +833,7 @@ function updateDeleteButton() { function setActionButtonsState() { const loadBtn = ui.loadBtn; + const downloadBtn = ui.downloadBtn; const deleteBtn = ui.deleteBtn; const disabled = isBatchLoading || isBatchDeleting || isLocalDeleting; const noSelection = getActiveSelectedSet().size === 0; @@ -836,6 +851,9 @@ function setActionButtonsState() { deleteBtn.disabled = disabled || noSelection; } } + if (downloadBtn) { + downloadBtn.disabled = currentSection === 'online' || disabled || noSelection; + } } function updateBatchProgress() { @@ -889,6 +907,7 @@ function getActiveSelectedSet() { function updateToolbarForSection() { updateLoadButton(); + updateDownloadButton(); updateDeleteButton(); updateSelectedCount(); updateBatchProgress(); @@ -1108,6 +1127,85 @@ function viewLocalFile(type, name) { window.open(url, '_blank'); } +function buildLocalFileUrl(type, name) { + const safeName = encodeURIComponent(name); + return type === 'image' ? `/v1/files/image/${safeName}` : `/v1/files/video/${safeName}`; +} + +function isIosBrowser() { + const ua = String(navigator.userAgent || ''); + const platform = String(navigator.platform || ''); + return /iPad|iPhone|iPod/.test(ua) || (platform === 'MacIntel' && navigator.maxTouchPoints > 1); +} + +function inferMimeFromName(type, name) { + const lower = String(name || '').toLowerCase(); + if (type === 'video') return 'video/mp4'; + if (lower.endsWith('.png')) return 'image/png'; + if (lower.endsWith('.webp')) return 'image/webp'; + return 'image/jpeg'; +} + +async function fetchLocalFileAsShareFile(type, name) { + const url = buildLocalFileUrl(type, name); + const res = await fetch(url, { + headers: buildAuthHeaders(apiKey) + }); + if (!res.ok) { + throw new Error('download_fetch_failed'); + } + const blob = await res.blob(); + const mime = blob.type || inferMimeFromName(type, name); + return new File([blob], name, { type: mime }); +} + +function triggerBrowserDownload(url, filename) { + const link = document.createElement('a'); + link.href = url; + link.download = filename || ''; + link.rel = 'noopener'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +async function downloadSelectedLocal(type) { + const selected = selectedLocal[type]; + const names = selected ? Array.from(selected) : []; + if (names.length === 0) { + showToast('未选择文件', 'info'); + return; + } + if (isIosBrowser()) { + try { + const files = await Promise.all(names.map(name => fetchLocalFileAsShareFile(type, name))); + if (navigator.share && navigator.canShare && navigator.canShare({ files })) { + await navigator.share({ + files, + title: names.length > 1 ? '缓存文件' : names[0] + }); + showToast(`已调用系统分享:${names.length} 个文件`, 'success'); + return; + } + } catch (error) { + console.warn('iOS share download failed:', error); + } + if (names.length === 1) { + window.location.href = buildLocalFileUrl(type, names[0]); + showToast('已打开文件,请使用系统分享保存', 'info'); + } else { + showToast('当前 iOS 浏览器不支持直接批量下载,请改为单选下载或使用分享保存', 'info'); + } + return; + } + names.forEach((name, index) => { + window.setTimeout(() => { + triggerBrowserDownload(buildLocalFileUrl(type, name), name); + }, index * 120); + }); + showToast(`已发起 ${names.length} 个文件下载`, 'success'); +} + async function deleteLocalFile(type, name) { const ok = await confirmAction(`确定要删除该文件吗?`, { okText: '删除' }); if (!ok) return; @@ -1209,6 +1307,19 @@ function handleDeleteClick() { } } +function handleDownloadClick() { + ensureUI(); + if (isBatchLoading || isBatchDeleting || isLocalDeleting) { + showToast('当前有任务进行中', 'info'); + return; + } + if (currentSection === 'online') { + showToast('在线资产暂不支持批量下载', 'info'); + return; + } + downloadSelectedLocal(currentSection); +} + function stopBatchLoad(options = {}) { if (!isBatchLoading) return; isBatchLoading = false; diff --git a/app/static/admin/pages/cache.html b/app/static/admin/pages/cache.html index 2167b2e..8512158 100644 --- a/app/static/admin/pages/cache.html +++ b/app/static/admin/pages/cache.html @@ -163,6 +163,10 @@ class="geist-button-outline text-xs px-3 gap-1 border-0 hover:bg-gray-100"> 加载 +