diff --git a/frontend/src/components/RecipeDetailOverlay.vue b/frontend/src/components/RecipeDetailOverlay.vue index 02c8d27..d8a4bd7 100644 --- a/frontend/src/components/RecipeDetailOverlay.vue +++ b/frontend/src/components/RecipeDetailOverlay.vue @@ -583,11 +583,14 @@ async function saveImage() { await generateCardImage() } if (!cardImageUrl.value) return - const link = document.createElement('a') - link.download = `${recipe.value.name || '配方'}_配方卡.png` - link.href = cardImageUrl.value - link.click() - ui.showToast('已保存图片') + const filename = `${recipe.value.name || '配方'}_配方卡` + try { + const { saveImageFromUrl } = await import('../composables/useSaveImage') + await saveImageFromUrl(cardImageUrl.value, filename) + ui.showToast('已保存图片') + } catch { + ui.showToast('保存失败') + } } function copyText() { diff --git a/frontend/src/composables/useSaveImage.js b/frontend/src/composables/useSaveImage.js new file mode 100644 index 0000000..911d0bb --- /dev/null +++ b/frontend/src/composables/useSaveImage.js @@ -0,0 +1,76 @@ +/** + * Save image — on mobile use native share (save to photos), + * on desktop trigger download. + */ + +const isMobile = () => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) + +/** + * Save a data URL or blob as image. + * Mobile: navigator.share → save to photos + * Desktop: download link + */ +export async function saveImageFromUrl(dataUrl, filename) { + if (isMobile() && navigator.canShare) { + try { + const res = await fetch(dataUrl) + const blob = await res.blob() + const file = new File([blob], filename + '.png', { type: 'image/png' }) + if (navigator.canShare({ files: [file] })) { + await navigator.share({ files: [file] }) + return true + } + } catch {} + } + // Desktop fallback + const a = document.createElement('a') + a.href = dataUrl + a.download = filename + '.png' + a.click() + return true +} + +/** + * 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.style.display = 'none') + try { + const canvas = await html2canvas(element, { + scale: 2, + backgroundColor: '#ffffff', + useCORS: true, + }) + buttons.forEach(b => b.style.display = '') + return saveCanvasImage(canvas, filename) + } catch { + buttons.forEach(b => b.style.display = '') + return false + } +} + +/** + * Save a canvas element as image. + */ +export async function saveCanvasImage(canvas, filename) { + if (isMobile() && navigator.canShare) { + try { + const blob = await new Promise(r => canvas.toBlob(r, 'image/png')) + const file = new File([blob], filename + '.png', { type: 'image/png' }) + if (navigator.canShare({ files: [file] })) { + await navigator.share({ files: [file] }) + return true + } + } catch {} + } + // Desktop fallback + const url = canvas.toDataURL('image/png') + const a = document.createElement('a') + a.href = url + a.download = filename + '.png' + a.click() + return true +} diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index da6fa30..f9cbc38 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -725,26 +725,18 @@ function exportPDF() { setTimeout(() => w.print(), 500) } -// Save modal as image using html2canvas +// Save modal as image (mobile: share to photos, desktop: download) async function saveModalImage(name) { + const overlay = document.querySelector('.modal-overlay') + if (!overlay) return + const cardEl = overlay.querySelector('[style*="border-radius: 20px"], [style*="border-radius:20px"]') || + overlay.querySelector('.oil-card-modal') || overlay.children[0] + if (!cardEl) return try { - const { default: html2canvas } = await import('html2canvas') - const overlay = document.querySelector('.modal-overlay') - if (!overlay) return - const cardEl = overlay.querySelector('[style*="border-radius: 20px"], [style*="border-radius:20px"]') || overlay.children[0] - if (!cardEl) return - // Hide close buttons during capture - const btns = cardEl.querySelectorAll('button') - btns.forEach(b => b.style.display = 'none') - const canvas = await html2canvas(cardEl, { scale: 2, backgroundColor: '#ffffff', useCORS: true }) - btns.forEach(b => b.style.display = '') - const url = canvas.toDataURL('image/png') - const a = document.createElement('a') - a.href = url - a.download = (name || '精油知识卡') + '.png' - a.click() - ui.showToast('图片已保存') - } catch (e) { + const { captureAndSave } = await import('../composables/useSaveImage') + const ok = await captureAndSave(cardEl, name || '精油知识卡') + if (ok) ui.showToast('图片已保存') + } catch { ui.showToast('保存失败') } }