fix: 手机保存图片加长按保存 fallback
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 53s

navigator.share 在部分手机浏览器不可用时,改为弹出全屏图片
让用户长按保存到相册。三层 fallback:
1. navigator.share({files}) → 系统分享面板
2. 图片弹窗 → 长按保存(手机通用)
3. 下载链接(桌面)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 09:08:22 +00:00
parent fc16436ebd
commit 26a47aaf23

View File

@@ -1,33 +1,40 @@
/** /**
* Save image — on mobile use native share (save to photos), * Save image utility — handles mobile (share/long-press) and desktop (download).
* on desktop trigger download.
*/ */
const isMobile = () => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) const isMobile = () => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
/** /**
* Save a data URL or blob as image. * Save from a data URL.
* Mobile: navigator.share → save to photos * Mobile: try navigator.share, fallback to showing image for long-press save.
* Desktop: download link * Desktop: download link.
*/ */
export async function saveImageFromUrl(dataUrl, filename) { export async function saveImageFromUrl(dataUrl, filename) {
if (isMobile() && navigator.canShare) { // Mobile: try native share first
if (isMobile() && navigator.share && navigator.canShare) {
try { try {
const res = await fetch(dataUrl) const res = await fetch(dataUrl)
const blob = await res.blob() const blob = await res.blob()
const file = new File([blob], filename + '.png', { type: 'image/png' }) const file = new File([blob], filename + '.png', { type: 'image/png' })
if (navigator.canShare({ files: [file] })) { if (navigator.canShare({ files: [file] })) {
await navigator.share({ files: [file] }) await navigator.share({ files: [file] })
return true return 'shared'
} }
} catch {} } catch {}
} }
// Desktop fallback
// Mobile fallback: show image popup for long-press save
if (isMobile()) {
showImagePopup(dataUrl, filename)
return 'popup'
}
// Desktop: direct download
const a = document.createElement('a') const a = document.createElement('a')
a.href = dataUrl a.href = dataUrl
a.download = filename + '.png' a.download = filename + '.png'
a.click() a.click()
return true return 'downloaded'
} }
/** /**
@@ -37,40 +44,36 @@ export async function captureAndSave(element, filename) {
const { default: html2canvas } = await import('html2canvas') const { default: html2canvas } = await import('html2canvas')
// Hide buttons during capture // Hide buttons during capture
const buttons = element.querySelectorAll('button') const buttons = element.querySelectorAll('button')
buttons.forEach(b => b.style.display = 'none') buttons.forEach(b => { b._prevDisplay = b.style.display; b.style.display = 'none' })
try { try {
const canvas = await html2canvas(element, { const canvas = await html2canvas(element, {
scale: 2, scale: 2,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
useCORS: true, useCORS: true,
}) })
buttons.forEach(b => b.style.display = '') buttons.forEach(b => b.style.display = b._prevDisplay || '')
return saveCanvasImage(canvas, filename) const dataUrl = canvas.toDataURL('image/png')
} catch { return saveImageFromUrl(dataUrl, filename)
buttons.forEach(b => b.style.display = '') } catch (e) {
buttons.forEach(b => b.style.display = b._prevDisplay || '')
console.error('captureAndSave failed:', e)
return false return false
} }
} }
/** /**
* Save a canvas element as image. * Show a fullscreen image popup for long-press saving (mobile fallback).
*/ */
export async function saveCanvasImage(canvas, filename) { function showImagePopup(dataUrl, filename) {
if (isMobile() && navigator.canShare) { const overlay = document.createElement('div')
try { overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.8);display:flex;flex-direction:column;align-items:center;padding:16px;overflow-y:auto'
const blob = await new Promise(r => canvas.toBlob(r, 'image/png')) overlay.onclick = () => overlay.remove()
const file = new File([blob], filename + '.png', { type: 'image/png' }) overlay.innerHTML = `
if (navigator.canShare({ files: [file] })) { <div style="color:white;font-size:13px;text-align:center;margin:12px 0;flex-shrink:0" onclick="event.stopPropagation()">
await navigator.share({ files: [file] }) 📱 长按下方图片 → 保存到相册
return true </div>
} <img src="${dataUrl}" style="max-width:100%;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.3)" onclick="event.stopPropagation()">
} catch {} <button onclick="this.parentElement.remove()" style="margin-top:12px;padding:8px 24px;border-radius:20px;border:1px solid rgba(255,255,255,0.3);background:transparent;color:white;cursor:pointer;font-size:14px;flex-shrink:0">关闭</button>
} `
// Desktop fallback document.body.appendChild(overlay)
const url = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = url
a.download = filename + '.png'
a.click()
return true
} }