fix: 知识卡保存图片参数与配方卡完全一致,去掉长按fallback
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Failing after 53s
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Failing after 53s
- html2canvas 参数: backgroundColor=null, scale=3, allowTaint=false (与 RecipeDetailOverlay.generateCardImage 完全一致) - useSaveImage 精简为只有 share + download 两条路径 - 截图失败时显示具体错误信息便于调试 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Save image utility — handles mobile (share/long-press) and desktop (download).
|
* Save image — on mobile use navigator.share (same as recipe card),
|
||||||
|
* 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 from a data URL.
|
* Save from a data URL.
|
||||||
* Mobile: try navigator.share, fallback to showing image for long-press save.
|
* Mobile: navigator.share({files}) → system share sheet (save to photos / AirDrop etc)
|
||||||
* Desktop: download link.
|
* Desktop: download link.
|
||||||
*/
|
*/
|
||||||
export async function saveImageFromUrl(dataUrl, filename) {
|
export async function saveImageFromUrl(dataUrl, filename) {
|
||||||
// Mobile: try native share first
|
// Try navigator.share with files (works on iOS Safari, Chrome mobile)
|
||||||
if (isMobile() && navigator.share && navigator.canShare) {
|
if (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()
|
||||||
@@ -20,60 +21,18 @@ export async function saveImageFromUrl(dataUrl, filename) {
|
|||||||
await navigator.share({ files: [file] })
|
await navigator.share({ files: [file] })
|
||||||
return 'shared'
|
return 'shared'
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
// User cancelled share or share failed, fall through to download
|
||||||
|
if (e.name === 'AbortError') return 'cancelled'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile fallback: show image popup for long-press save
|
// Fallback: direct download
|
||||||
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'
|
||||||
|
document.body.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
|
setTimeout(() => a.remove(), 100)
|
||||||
return 'downloaded'
|
return 'downloaded'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture a DOM element as image and save it.
|
|
||||||
*/
|
|
||||||
export async function captureAndSave(element, filename) {
|
|
||||||
const { default: html2canvas } = await import('html2canvas')
|
|
||||||
// Hide buttons during capture
|
|
||||||
const buttons = element.querySelectorAll('button')
|
|
||||||
buttons.forEach(b => { b._prevDisplay = b.style.display; b.style.display = 'none' })
|
|
||||||
try {
|
|
||||||
const canvas = await html2canvas(element, {
|
|
||||||
scale: 2,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
useCORS: true,
|
|
||||||
})
|
|
||||||
buttons.forEach(b => b.style.display = b._prevDisplay || '')
|
|
||||||
const dataUrl = canvas.toDataURL('image/png')
|
|
||||||
return saveImageFromUrl(dataUrl, filename)
|
|
||||||
} catch (e) {
|
|
||||||
buttons.forEach(b => b.style.display = b._prevDisplay || '')
|
|
||||||
console.error('captureAndSave failed:', e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a fullscreen image popup for long-press saving (mobile fallback).
|
|
||||||
*/
|
|
||||||
function showImagePopup(dataUrl, filename) {
|
|
||||||
const overlay = document.createElement('div')
|
|
||||||
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'
|
|
||||||
overlay.onclick = () => overlay.remove()
|
|
||||||
overlay.innerHTML = `
|
|
||||||
<div style="color:white;font-size:13px;text-align:center;margin:12px 0;flex-shrink:0" onclick="event.stopPropagation()">
|
|
||||||
📱 长按下方图片 → 保存到相册
|
|
||||||
</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()">
|
|
||||||
<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>
|
|
||||||
`
|
|
||||||
document.body.appendChild(overlay)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -727,24 +727,33 @@ function exportPDF() {
|
|||||||
setTimeout(() => w.print(), 500)
|
setTimeout(() => w.print(), 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save element as image — same flow as recipe card:
|
// Save element as image — identical to recipe card flow
|
||||||
// 1. html2canvas → dataUrl 2. saveImageFromUrl (share/popup/download)
|
|
||||||
async function saveElementAsImage(el, name) {
|
async function saveElementAsImage(el, name) {
|
||||||
if (!el) { ui.showToast('找不到卡片'); return }
|
if (!el) { ui.showToast('找不到卡片'); return }
|
||||||
|
const html2canvas = (await import('html2canvas')).default
|
||||||
|
const { saveImageFromUrl } = await import('../composables/useSaveImage')
|
||||||
|
|
||||||
|
// Hide buttons during capture
|
||||||
|
const btns = el.querySelectorAll('button')
|
||||||
|
btns.forEach(b => { b._d = b.style.display; b.style.display = 'none' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const html2canvas = (await import('html2canvas')).default
|
// Same params as recipe card's generateCardImage
|
||||||
const { saveImageFromUrl } = await import('../composables/useSaveImage')
|
const canvas = await html2canvas(el, {
|
||||||
// Hide buttons during capture
|
backgroundColor: null,
|
||||||
const btns = el.querySelectorAll('button')
|
scale: 3,
|
||||||
btns.forEach(b => { b._d = b.style.display; b.style.display = 'none' })
|
useCORS: true,
|
||||||
const canvas = await html2canvas(el, { scale: 2, backgroundColor: '#fff', useCORS: true })
|
allowTaint: false,
|
||||||
|
})
|
||||||
btns.forEach(b => b.style.display = b._d || '')
|
btns.forEach(b => b.style.display = b._d || '')
|
||||||
|
|
||||||
const dataUrl = canvas.toDataURL('image/png')
|
const dataUrl = canvas.toDataURL('image/png')
|
||||||
const result = await saveImageFromUrl(dataUrl, name)
|
await saveImageFromUrl(dataUrl, name)
|
||||||
if (result === 'shared' || result === 'downloaded') ui.showToast('图片已保存')
|
ui.showToast('图片已保存')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('saveElementAsImage:', e)
|
btns.forEach(b => b.style.display = b._d || '')
|
||||||
ui.showToast('保存失败')
|
console.error('saveElementAsImage failed:', e)
|
||||||
|
ui.showToast('截图失败: ' + (e.message || '未知错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user