feat: add download route and view, implement image download functionality, and enhance QR code sharing in PickupView
This commit is contained in:
@@ -39,6 +39,11 @@ const router = createRouter({
|
||||
name: 'pickup',
|
||||
component: () => import('../views/PickupView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/download',
|
||||
name: 'download',
|
||||
component: () => import('../views/DownloadView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
262
src/views/DownloadView.vue
Normal file
262
src/views/DownloadView.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const imageParam = ref('')
|
||||
const hasError = ref(false)
|
||||
const isDownloading = ref(false)
|
||||
const downloadSuccess = ref(false)
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
if (imageParam.value) {
|
||||
return `https://assets.dekthai-online.com/static/photobooth/${imageParam.value}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const downloadImage = async () => {
|
||||
if (!imageUrl.value || isDownloading.value) return
|
||||
|
||||
isDownloading.value = true
|
||||
downloadSuccess.value = false
|
||||
|
||||
try {
|
||||
// Create a temporary link element for download
|
||||
const link = document.createElement('a')
|
||||
link.href = imageUrl.value
|
||||
link.download = imageParam.value || 'photobooth-image.jpg'
|
||||
link.style.display = 'none'
|
||||
|
||||
// Add to DOM, click, and remove
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
// Show success message
|
||||
downloadSuccess.value = true
|
||||
|
||||
// Hide success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
downloadSuccess.value = false
|
||||
}, 3000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
hasError.value = true
|
||||
} finally {
|
||||
isDownloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Get image parameter from URL query
|
||||
const image = route.query.image as string
|
||||
if (image) {
|
||||
imageParam.value = image
|
||||
} else {
|
||||
hasError.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="download-container">
|
||||
<header class="header">
|
||||
<h1>ดาวน์โหลดรูปภาพ</h1>
|
||||
<p>รูปภาพของคุณพร้อมให้ดาวน์โหลด</p>
|
||||
</header>
|
||||
|
||||
<section v-if="!hasError && imageUrl" class="download-section">
|
||||
<div class="download-actions">
|
||||
<button @click="downloadImage" :disabled="isDownloading" class="download-button">
|
||||
<span v-if="isDownloading" class="button-icon">⏳</span>
|
||||
<span v-else class="button-icon">📥</span>
|
||||
{{ isDownloading ? 'กำลังดาวน์โหลด...' : 'ดาวน์โหลดรูปภาพ' }}
|
||||
</button>
|
||||
|
||||
<div v-if="downloadSuccess" class="success-message">
|
||||
✅ ดาวน์โหลดสำเร็จ! ไฟล์ได้บันทึกในเครื่องของคุณแล้ว
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-display">
|
||||
<img :src="imageUrl" alt="Photo for download" class="download-image" @error="hasError = true" />
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section v-else class="error-section">
|
||||
<div class="error-message">
|
||||
<h2>ไม่พบรูปภาพ</h2>
|
||||
<p>ไม่สามารถโหลดรูปภาพได้ กรุณาตรวจสอบลิงก์อีกครั้ง</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Crete+Round&family=Old+Standard+TT&display=swap');
|
||||
|
||||
.download-container {
|
||||
min-height: 100vh;
|
||||
padding: 1.5rem;
|
||||
background: #fdfaf6;
|
||||
color: #3e2723;
|
||||
font-family: 'Old Standard TT', serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: #5d4037;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Crete Round', serif;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #6d4c41;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.image-display {
|
||||
background: #e0d8cf;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||||
border: 4px solid #a1887f;
|
||||
}
|
||||
|
||||
.download-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
filter: sepia(0.1) brightness(0.95);
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.download-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
background: #8d6e63;
|
||||
color: #fdfaf6;
|
||||
border: 2px solid #795548;
|
||||
padding: 1rem 2.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
font-family: 'Crete Round', serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.download-button:hover {
|
||||
background: #795548;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-section {
|
||||
text-align: center;
|
||||
background: #e0d8cf;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||||
border: 2px solid #bcaaa4;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-message h2 {
|
||||
color: #d32f2f;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Crete Round', serif;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
color: #6d4c41;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.download-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.download-button:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.download-image {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
width: 280px;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.8rem 1.5rem;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.error-message h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,7 +33,7 @@ const examplePhotos = ref([
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="examples-section" style="margin-top: 1rem;">
|
||||
<section class="examples-section" style="margin-top: 1rem; display: none;">
|
||||
<!-- <h2>ตัวอย่างผลงาน</h2> -->
|
||||
<div class="photo-examples">
|
||||
<div class="example-strip">
|
||||
|
||||
@@ -10,6 +10,20 @@ const cachedImageUrl = ref('')
|
||||
const isGeneratingImage = ref(false)
|
||||
const hasError = ref(false)
|
||||
|
||||
const qrCodeUrl = computed(() => {
|
||||
const filename = localStorage.getItem('photobooth-uploaded-filename')
|
||||
if (filename) {
|
||||
return `https://api.dekthai-online.com/apu-school/qrcode/photobooth.php?key=${filename}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const downloadPageUrl = computed(() => {
|
||||
const filename = localStorage.getItem('photobooth-uploaded-filename')
|
||||
if (filename) {
|
||||
return `${window.location.origin}/download?image=${filename}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
onMounted(() => {
|
||||
// โหลดข้อมูลจาก localStorage
|
||||
const savedPhotos = localStorage.getItem('photobooth-photos')
|
||||
@@ -281,7 +295,22 @@ const startOver = () => {
|
||||
<span class="button-icon">📥</span>
|
||||
ดาวน์โหลด
|
||||
</button>
|
||||
|
||||
<button @click="startOver" class="start-over-button">
|
||||
เริ่มใหม่
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- QR Code สำหรับ download รูปถ่าย -->
|
||||
<div v-if="qrCodeUrl" class="qr-code-section">
|
||||
<h3>สแกน QR Code เพื่อดาวน์โหลด</h3>
|
||||
<a :href="downloadPageUrl" target="_blank" class="qr-code-link">
|
||||
<img :src="qrCodeUrl" alt="QR Code for downloading photo" class="qr-code-image" />
|
||||
</a>
|
||||
<p class="qr-hint">หรือ <a :href="downloadPageUrl" target="_blank" class="download-link">คลิกที่นี่</a> เพื่อเปิดในแท็บใหม่</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="result-section">
|
||||
@@ -293,6 +322,7 @@ const startOver = () => {
|
||||
</div>
|
||||
|
||||
<img v-else-if="cachedImageUrl" :src="cachedImageUrl" alt="Combined Photo" class="combined-image" />
|
||||
|
||||
<div v-else-if="hasError" class="error-message">
|
||||
<p>ไม่สามารถสร้างภาพได้</p>
|
||||
</div>
|
||||
@@ -302,11 +332,9 @@ const startOver = () => {
|
||||
|
||||
|
||||
|
||||
<section class="footer-section">
|
||||
<button @click="startOver" class="start-over-button">
|
||||
เริ่มใหม่
|
||||
</button>
|
||||
</section>
|
||||
<!-- <section class="footer-section">
|
||||
|
||||
</section> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -408,6 +436,47 @@ const startOver = () => {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.qr-code-section {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-code-section h3 {
|
||||
color: #5d4037;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Crete Round', serif;
|
||||
}
|
||||
|
||||
.qr-code-image {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px solid #a1887f;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.qr-code-link:hover .qr-code-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #6d4c41;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
color: #8d6e63;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
color: #795548;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1.5rem; /* Increased gap */
|
||||
@@ -421,7 +490,7 @@ const startOver = () => {
|
||||
padding: 1rem 2.5rem; /* Larger padding */
|
||||
font-size: 1.2rem; /* Slightly larger font */
|
||||
font-weight: bold;
|
||||
border-radius: 8px; /* Softer border-radius */
|
||||
border-radius: 20px; /* Softer border-radius */
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
@@ -454,7 +523,7 @@ const startOver = () => {
|
||||
padding: 0.8rem 1.8rem; /* Adjusted padding */
|
||||
font-size: 1rem; /* Slightly smaller font */
|
||||
font-weight: bold;
|
||||
border-radius: 5px; /* Softer border-radius */
|
||||
border-radius: 20px; /* Softer border-radius */
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
|
||||
@@ -31,10 +31,56 @@ onMounted(() => {
|
||||
// เริ่มนับถอยหลัง
|
||||
startCountdown()
|
||||
|
||||
// Load the final image from localStorage
|
||||
finalImage.value = localStorage.getItem('photobooth-final-image');
|
||||
// // Load the final image from localStorage
|
||||
// finalImage.value = localStorage.getItem('photobooth-final-image');
|
||||
|
||||
// // Upload the final image to the server
|
||||
// if (finalImage.value) {
|
||||
// uploadFinalImage(finalImage.value);
|
||||
// }
|
||||
})
|
||||
|
||||
const uploadFinalImage = async (imageDataUrl: string) => {
|
||||
try {
|
||||
// Compress the image first to reduce size
|
||||
console.log('Original image size:', imageDataUrl.length);
|
||||
const compressedImage = await compressImage(imageDataUrl, 0.8); // 80% quality
|
||||
console.log('Compressed image size:', compressedImage.length);
|
||||
|
||||
// Convert compressed base64 data URL to Blob
|
||||
const response = await fetch(compressedImage);
|
||||
const blob = await response.blob();
|
||||
|
||||
// Create FormData
|
||||
const formData = new FormData();
|
||||
const file = new File([blob], 'photobooth-final.jpg', { type: 'image/jpeg' });
|
||||
formData.append('profile_image', file, file.name);
|
||||
|
||||
// Upload to API
|
||||
const uploadResponse = await fetch('https://api2.dekthai-online.com/g/upload/photobooth', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (uploadResponse.ok) {
|
||||
const result = await uploadResponse.json();
|
||||
console.log('Upload successful:', result);
|
||||
|
||||
// Save the filename to localStorage
|
||||
if (result.filename) {
|
||||
localStorage.setItem('photobooth-uploaded-filename', result.filename);
|
||||
console.log('Filename saved to localStorage:', result.filename);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('Upload failed:', uploadResponse.status, uploadResponse.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const startCountdown = () => {
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
@@ -232,12 +278,24 @@ const generateAndCacheImage = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
// สร้าง PNG data URL และบันทึกใน localStorage
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
// สร้าง compressed JPEG data URL และบันทึกใน localStorage
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.8) // 80% quality JPEG
|
||||
|
||||
try {
|
||||
saveToLocalStorage('photobooth-final-image', dataUrl);
|
||||
finalImage.value = dataUrl;
|
||||
|
||||
|
||||
// Load the final image from localStorage
|
||||
finalImage.value = localStorage.getItem('photobooth-final-image');
|
||||
|
||||
// Upload the final image to the server
|
||||
if (finalImage.value) {
|
||||
uploadFinalImage(dataUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
||||
console.error('localStorage quota exceeded for cached image')
|
||||
@@ -311,9 +369,9 @@ onMounted(() => {
|
||||
<p>กำลังเตรียมไฟล์สำหรับดาวน์โหลด...</p>
|
||||
</div>
|
||||
|
||||
<div class="final-image-display">
|
||||
<!-- <div class="final-image-display">
|
||||
<img v-if="finalImage" :src="finalImage" alt="Final Combined Image" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
<div class="loading-indicator">
|
||||
|
||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ['photobooth.lookmeblog.com']
|
||||
allowedHosts: ['photobooth.lookmeblog.com', 'photobooth.dekthai-online.com']
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user