เป็นระบบฟอร์มเมล
มี signature pad
อัพโหลดไฟล์ภาพ,pdf (มี preview ถ้าเป็นภาพ)
เมื่อกรอกเสร็จ ส่งเข้าเมล และ มีปุ่มดาวน์โหลดหน้านี้เป็น pdf
ขณะส่งขึ้น progress bar แบบ real time
ขณะดาวน์โหลด pdf กฌขึ้นสถานะให้เหร้ความคืบหน้า %
ถ้าเปิดในไลน์ จะมีขึ้นเตือนให้เปิดใน browser
ระบบกันสแปม กันกดส่งซ้ำ
เก็บค่าใน localhost
เพื่อตอบโจทย์ระบบที่ “ง่าย เร็ว เสถียร ใช้ได้ทุกอุปกรณ์” และทำงานฝั่ง Client (Localhost) เป็นหลัก โครงสร้างที่เหมาะสมที่สุดคือการใช้ HTML5 + Tailwind CSS (ดีไซน์สะอาดยืดหยุ่น) + Vanilla JavaScript (ไม่ต้องพึ่ง Framework หนาๆ ให้ช้า)
ส่วนการสร้าง PDF แนะนำให้ใช้ html2pdf.js และการวาดลายเซ็นใช้ signature_pad ซึ่งเสถียรที่สุดในฝั่งเบราว์เซอร์ครับ
นี่คือโครงสร้างระบบและตัวอย่างโค้ดที่คุณสามารถนำไปวางในไฟล์ index.html เพื่อรันบน Localhost ได้ทันทีครับ:
1. โครงสร้างระบบ (HTML & JavaScript)
HTML
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบฟอร์มและบันทึกข้อมูล (Fast & Stable)</title>
<!-- Tailwind CSS สำหรับดีไซน์ที่คลีนและรองรับทุกหน้าจอ -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Libraries สำคัญ: Signature Pad และ HTML2PDF -->
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
</head>
<body class="bg-gray-50 text-gray-800 font-sans min-h-screen p-4 sm:p-8">
<!-- แจ้งเตือนเมื่อเปิดใน LINE Webview -->
<div id="line-alert" class="hidden fixed top-0 left-0 w-full bg-amber-500 text-white text-center py-3 px-4 z-50 shadow-md font-semibold flex justify-between items-center">
<span>⚠️ เพื่อการใช้งานที่เสถียรที่สุด (การอัพโหลด/ดาวน์โหลดไฟล์) กรุณากดเปิดด้วยเบราว์เซอร์ปกติ (เช่น Chrome, Safari)</span>
<button onclick="document.getElementById('line-alert').remove()" class="bg-black/20 hover:bg-black/40 px-2 py-1 rounded text-sm">ปิด</button>
</div>
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm p-6 sm:p-8 border border-gray-100" id="form-content">
<h2 class="text-2xl font-bold mb-6 text-gray-900 border-b pb-4">ฟอร์มบันทึกข้อมูลและส่งเอกสาร</h2>
<form id="main-form" onsubmit="handleSubmit(event)" class="space-y-6">
<!-- ข้อมูลผู้กรอก -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ชื่อ-นามสกุล *</label>
<input type="text" id="fullname" required class="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">อีเมลผู้ส่ง *</label>
<input type="email" id="email" required class="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">รายละเอียดเพิ่มเติม</label>
<textarea id="details" rows="3" class="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div>
<!-- อัพโหลดไฟล์ + Preview -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">อัพโหลดไฟล์ (รูปภาพ หรือ PDF) *</label>
<input type="file" id="file-upload" accept="image/*,application/pdf" required onchange="handleFilePreview(this)" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<!-- พื้นที่ Preview -->
<div id="preview-container" class="mt-3 hidden border border-dashed border-gray-300 p-2 rounded-lg text-center bg-gray-50">
<img id="image-preview" class="max-h-48 mx-auto hidden rounded" />
<div id="pdf-preview" class="hidden text-sm text-gray-600 flex items-center justify-center gap-2">
📄 <span id="pdf-name"></span>
</div>
</div>
</div>
<!-- Signature Pad -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ลงลายมือชื่อ *</label>
<div class="border border-gray-300 rounded-lg bg-gray-50 p-1">
<canvas id="signature-pad" class="w-full h-40 bg-white rounded border border-gray-100 touch-none"></canvas>
</div>
<div class="mt-2 text-right">
<button type="button" onclick="clearSignature()" class="text-xs text-red-600 hover:underline">ล้างลายเซ็น</button>
</div>
</div>
<!-- ป้องกันสแปมด้วย Honeypot (ซ่อนไว้ไม่ให้คนเห็น แต่บอทจะชอบกรอก) -->
<input type="text" id="honeypot" class="hidden" autocomplete="off">
<!-- Progress Bar สำหรับการส่งเมล์ / โหลด PDF -->
<div id="progress-wrapper" class="hidden space-y-2">
<div class="flex justify-between text-xs font-semibold text-blue-700">
<span id="progress-status">กำลังดำเนินการ...</span>
<span id="progress-percent">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-200" style="width: 0%"></div>
</div>
</div>
<!-- ปุ่มแอคชั่น (ไม่แสดงใน PDF) -->
<div id="action-buttons" class="flex flex-col sm:flex-row gap-3 pt-4 border-t border-gray-100">
<button type="submit" id="btn-submit" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-lg transition disabled:opacity-50">
ส่งข้อมูลเข้าอีเมล
</button>
<button type="button" id="btn-pdf" onclick="downloadPDF()" class="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2.5 px-4 rounded-lg transition disabled:opacity-50">
ดาวน์โหลดหน้านี้เป็น PDF
</button>
</div>
</form>
</div>
<script>
let signaturePad;
let isSubmitting = false;
// 1. ตรวจสอบ LINE และโหลดค่าจาก LocalStorage เมื่อเปิดหน้าเว็บ
document.addEventListener("DOMContentLoaded", function() {
// ตรวจสอบว่าเป็น LINE Webview หรือไม่
const ua = navigator.userAgent || navigator.vendor || window.opera;
if (ua.indexOf("Line") > -1) {
document.getElementById('line-alert').classList.remove('hidden');
}
// ตั้งค่า Signature Pad
const canvas = document.getElementById('signature-pad');
signaturePad = new SignaturePad(canvas);
// ปรับขนาด Canvas ให้เหมาะกับหน้าจออุปกรณ์
function resizeCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
signaturePad.clear();
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// ดึงข้อมูลเก่าจาก LocalStorage (Localhost) มาใส่ในฟอร์มให้อัตโนมัติ เพื่อความเร็ว
if(localStorage.getItem('formData')) {
const savedData = JSON.parse(localStorage.getItem('formData'));
document.getElementById('fullname').value = savedData.fullname || '';
document.getElementById('email').value = savedData.email || '';
document.getElementById('details').value = savedData.details || '';
}
});
// 2. จัดการ Preview ไฟล์
function handleFilePreview(input) {
const file = input.files[0];
const container = document.getElementById('preview-container');
const imgPreview = document.getElementById('image-preview');
const pdfPreview = document.getElementById('pdf-preview');
const pdfName = document.getElementById('pdf-name');
container.classList.remove('hidden');
imgPreview.classList.add('hidden');
pdfPreview.classList.add('hidden');
if (file) {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
imgPreview.src = e.target.result;
imgPreview.classList.remove('hidden');
}
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
pdfName.textContent = file.name;
pdfPreview.classList.remove('hidden');
}
}
}
function clearSignature() {
signaturePad.clear();
}
// 3. ควบคุมสถานะ Progress Bar
function updateProgress(status, percent) {
const wrapper = document.getElementById('progress-wrapper');
const textStatus = document.getElementById('progress-status');
const textPercent = document.getElementById('progress-percent');
const bar = document.getElementById('progress-bar');
wrapper.classList.remove('hidden');
textStatus.textContent = status;
textPercent.textContent = `${percent}%`;
bar.style.width = `${percent}%`;
if(percent >= 100) {
setTimeout(() => wrapper.classList.add('hidden'), 1500);
}
}
// 4. บันทึกข้อมูลและจำลองการส่งเมล (กันกดซ้ำ + กันสแปม)
async function handleSubmit(event) {
event.preventDefault();
// กันกดซ้ำ (Double-click Protection)
if (isSubmitting) return;
// กันสแปม (Honeypot Protection) - ถ้ามีบอทมากรอกช่องที่ซ่อนไว้ จะปฏิเสธทันที
if (document.getElementById('honeypot').value !== '') {
alert('Spam detected!');
return;
}
if (signaturePad.isEmpty()) {
alert('กรุณาลงลายมือชื่อก่อนส่งเอกสาร');
return;
}
isSubmitting = true;
setButtonsDisabled(true);
// เก็บค่าเข้า LocalStorage ไว้บนคอม/มือถือเครื่องนี้ทันที
const formData = {
fullname: document.getElementById('fullname').value,
email: document.getElementById('email').value,
details: document.getElementById('details').value
};
localStorage.setItem('formData', JSON.stringify(formData));
// จำลองการอัพโหลดและส่ง Mail API ด้วย Real-time Progress Bar
let progress = 0;
const interval = setInterval(() => {
progress += 10;
updateProgress('กำลังเตรียมข้อมูลและส่งอีเมล...', progress);
if (progress >= 100) {
clearInterval(interval);
/**
* [NOTE สำหรับระบบหลังบ้าน]
* ตรงนี้ใช้ส่งข้อมูลผ่าน Fetch API ไปยัง Backend (เช่น PHP, Node.js หรือ Email Service เช่น EmailJS)
* ตัวอย่าง:
* fetch('/api/send-email', { method: 'POST', body: JSON.stringify(...) })
*/
alert('บันทึกข้อมูลและส่งอีเมลเรียบร้อยแล้ว!');
isSubmitting = false;
setButtonsDisabled(false);
}
}, 150);
}
// 5. ดาวน์โหลดหน้านี้เป็น PDF + เช็ค % ความคืบหน้า
function downloadPDF() {
const element = document.getElementById('form-content');
const buttons = document.getElementById('action-buttons');
// ซ่อนปุ่มกดไม่ให้ติดเข้าไปในไฟล์ PDF
buttons.style.display = 'none';
const opt = {
margin: [10, 10, 10, 10],
filename: `Document_${Date.now()}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
// แสดง Progress ดาวน์โหลดแบบจำลองให้เข้ากับ html2pdf lifecycle
updateProgress('กำลังสร้างไฟล์ PDF...', 20);
setTimeout(() => updateProgress('กำลังประมวลผลหน้ากระดาษ...', 50), 300);
setTimeout(() => updateProgress('จัดเรียงรูปแบบเอกสาร...', 80), 600);
html2pdf().set(opt).from(element).save().then(() => {
updateProgress('ดาวน์โหลดสำเร็จ!', 100);
// แสดงปุ่มกดกลับมาเหมือนเดิม
buttons.style.display = 'flex';
}).catch(err => {
console.error(err);
buttons.style.display = 'flex';
updateProgress('เกิดข้อผิดพลาด', 0);
});
}
function setButtonsDisabled(status) {
document.getElementById('btn-submit').disabled = status;
document.getElementById('btn-pdf').disabled = status;
}
</script>
</body>
</html>
2. จุดเด่นในการทำงานของระบบนี้
- เสถียรและเร็วเพราะแยกการประมวลผล: การทำ Preview และแปลงหน้าเว็บเป็น PDF จัดการเสร็จสรรพที่ฝั่งผู้ใช้งาน (Client-side) ทำให้ทำงานได้ทันทีโดยไม่สร้างภาระให้ฝั่ง Server ในส่วนนี้
- การเก็บข้อมูลใน Localhost: ใช้กลไก
localStorageในการเซฟค่าฟิลด์อย่าง ชื่อ หรือ อีเมล ไว้ในเครื่องผู้ใช้ทันทีเมื่อกดปุ่มส่ง ทำให้หากกลับมาเปิดหน้าเว็บนี้อีกครั้งในอนาคต ข้อมูลที่เคยกรอกบ่อยๆ จะถูก Auto-fill ให้เองโดยไม่ต้องพิมพ์ซ้ำ - ระบบความปลอดภัยและการป้องกัน:
- กันกดซ้ำ (Debounce/Lock): ใช้ตัวแปร
isSubmittingปิดการทำงานของปุ่มทันทีที่กดครั้งแรกเพื่อไม่ให้เกิดการส่งข้อมูลซ้อนซ้ำซ้อนในระบบฐานข้อมูล - กันสแปมบอท (Honeypot): ใช้ Input ที่ซ่อนไว้ บอทอัตโนมัติจะกรอกฟิลด์นี้โดยไม่รู้ตัว แต่ถ้าผู้ใช้ทั่วไปส่งข้อมูล ฟิลด์นี้จะว่างเปล่า ถ้าระบบพบว่ามีข้อมูลในฟิลด์นี้จะตัดการทำงานทันที
- กันกดซ้ำ (Debounce/Lock): ใช้ตัวแปร
- การรองรับแอปพลิเคชัน LINE: เพิ่มสคริปต์ตรวจจับ
User Agentหากเปิดผ่าน In-App Browser ของ LINE จะแสดงแถบเตือนสีส้มด้านบน แนะนำให้ย้ายไปเปิดที่ Chrome หรือ Safari เพื่อหลีกเลี่ยงปัญหาของระบบ Webview ในตัวแปรแอปที่มักจะจำกัดสิทธิ์ในการสั่งเซฟไฟล์และดาวน์โหลดลงเครื่องโดยตรงครับ