แก้โหลด pdf จากมือถือนาน
เมื่อกดปุ่มดาวน์โหลด จาก มือถือ หน้า pdf ส่วนของ ลายเซ็น จะหมุนๆ นานมากๆ ไม่เสร็จซะที แต่เมลส่งเข้า รวมทั้งลายเซ็น ทำให้เซฟ pdf ในมือถือตอนนั้นไม่ได้ (เดสทอปเซฟได้ปกติ)
สาเหตุหลักที่บนมือถือเกิดอาการ “หมุนค้าง นานมากๆ ไม่เสร็จซะที” แต่เมลส่งผ่านปกติ และเดสท็อปเซฟได้ปกติ เกิดจากปัญหาสองจุดนี้ในโค้ดครับ:
- ฟังก์ชัน
window.print();บน Webview/เบราว์เซอร์มือถือ: ในฟังก์ชันprintContentพี่ใช้ระบบwindow.print();ซึ่งบนเดสท็อปจะเปิดหน้าต่าง Preview การพิมพ์ขึ้นมาให้เซฟเป็น PDF ได้ทันที แต่บนมือถือ (โดยเฉพาะถ้าเปิดผ่านแอปอย่าง Line, Messenger หรือเบราว์เซอร์บางตัว) คำสั่งwindow.print()จะทำงานได้ไม่สมบูรณ์ หรือพยายามเรียกคำสั่งพิมพ์ของระบบปฏิบัติการขึ้นมาจนทำให้หน้าเว็บค้าง (หมุนวนไม่สิ้นสุด) เนื่องจากสถานะisProcessing = true;ถูกล็อกค้างไว้และไม่มีวันปล่อย - พี่โหลดไลบรารี่
html2pdf.jsมาแล้วแต่ยังไม่ได้เรียกใช้: ใน<head>มีการดึงhtml2pdf.bundle.min.jsมาเตรียมไว้แล้ว แต่ในฟังก์ชันprintContentกลับไปเรียกwindow.print();แทน ซึ่งตัวไลบรารี่นี้สร้างมาเพื่อแก้ปัญหานี้บนมือถือโดยเฉพาะ (แปลง DOM เป็น PDF แล้วสั่งดาวน์โหลดตรงๆ ไม่ผ่านคำสั่ง Print ของเครื่อง)
วิธีแก้ไข
ให้พี่ปรับแก้โค้ดในฟังก์ชัน printContent(el) ด้านล่างนี้ โดยเปลี่ยนจากการใช้ window.print() มาใช้ html2pdf ในการเจนไฟล์ดาวน์โหลดแทนครับ
โค้ดเดิมในไฟล์ของพี่ (บรรทัดประมาณ 480-485):
JavaScript
// ... โค้ดส่วนบนของ printContent ...
var $printButton = $('#print, button, .signature-pad-wrapper, input[type="file"]');
$printButton.css('display', 'none');
window.print();
// คืนค่าปุ่มและอินพุตอัปโหลดไฟล์ให้กลับมาโชว์บนหน้าเว็บปกติ
$printButton.css('display', '');
$('#' + el).find('#signature-image').css('display', 'none');
}
ให้เปลี่ยนเป็นโค้ดชุดนี้แทนครับ:
JavaScript
// ซ่อนปุ่มและอินพุตอัปโหลดไฟล์ไม่ให้ติดไปในเนื้อหาดาวน์โหลด PDF
var $printButton = $('#print, button, .signature-pad-wrapper, input[type="file"]');
$printButton.css('display', 'none');
// ตั้งค่าสำหรับ html2pdf.js
var opt = {
margin: [10, 10, 10, 10], // ระยะขอบ (บน, ซ้าย, ล่าง, ขวา) เป็นมิลลิเมตร
filename: 'Application_' + applicantName.replace(/\s+/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true }, // scale: 2 เพิ่มความคมชัด, useCORS ดึงรูปข้ามโดเมน
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
// เรียกใช้ html2pdf แทน window.print()
html2pdf().set(opt).from(document.getElementById(el)).save().then(function() {
// คืนค่าปุ่มและสิ่งต่างๆ กลับมาโชว์บนหน้าเว็บปกติหลังจากเซฟ PDF เสร็จแล้ว
$printButton.css('display', '');
$('#' + el).find('#signature-image').css('display', 'none');
isProcessing = false; // ปลดล็อกตัวแปรสถานะ
}).catch(function(error) {
console.error('pdf generation error:', error);
$printButton.css('display', '');
$('#' + el).find('#signature-image').css('display', 'none');
isProcessing = false;
});
}
จุดที่โค้ดชุดใหม่นี้เข้าไปเคลียร์ปัญหา:
- ทำงานจบในตัวเอง:
html2pdfจะแปลงพื้นที่ใน#PDFทั้งหมดรวมถึงลายเซ็นที่เป็น Base64 ออกมาเป็นไฟล์ PDF และยิงคำสั่งดาวน์โหลดลงเครื่องมือถือตรงๆ โดยไม่ไปสะดุดกับฟังก์ชันสั่งพิมพ์ของระบบมือถือ - ปลดล็อกอาการหมุนค้าง: การใช้โครงสร้าง
.then()จะทำให้อินเทอร์เฟซปลดล็อกตัวเอง (isProcessing = false;) ทันทีที่การสร้าง PDF และส่งคำสั่งดาวน์โหลดสำเร็จ ทำให้หน้าจอไม่ค้างและปุ่มกดจะกลับมาทำงานตามปกติครับ
ทดสอบแล้ว
ในมือถือ เซฟเร็ว ไม่ดีดหน้าขึ้นมา เมื่อเปิดไฟล์ในมือถือ พบว่า เป็นหน้าว่าง
การที่ดาวน์โหลดบนมือถือแล้วได้ “หน้าว่าง (Blank Page)” เกิดจากโครงสร้าง CSS และการจัดวางแท็ก <body> กับ <div> ในไฟล์ของพี่ไปขัดขวางการทำงานของ html2pdf.js (และ html2canvas) ครับ
มีอยู่ 3 จุดหลัก ที่ทำให้มันมองเห็นหน้าฟอร์มเป็นหน้าว่างเมื่อเปิดในมือถือ:
- สไตล์ของ
.main-containมีposition: absolute;และoverflow: hidden;: ตัวเก็บครอบทั้งหมดของพี่ถูกเซ็ตให้ล็อกหน้าจอและตัดส่วนที่เกินทิ้ง (overflow: hidden) ทำให้เมื่อhtml2canvasพยายามจะวาดภาพหน้าเว็บในโหมดมือถือ มันจะมองไม่เห็นเนื้อหาที่อยู่ลึกลงไปจนกลายเป็นหน้าว่าง - การวางโครงสร้างแท็ก HTML สลับตำแหน่ง: ในโค้ดของพี่มีแท็ก
<div id="overlay"></div>และ<div class="main-contain">โผล่มาคั่น ก่อนหน้าแท็ก<body>ซึ่งตามหลักมาตรฐาน (และระบบเรนเดอร์ PDF) แท็กควบคุมทั้งหมดต้องอยู่ภายใต้<body>เท่านั้น ไม่งั้นมันจะหา Element#PDFไม่เจอตอนประมวลผลบนมือถือ - รูปภาพโลโก้หัวกระดาษ (
head-bki.png): ถ้าลิ้งก์รูปภาพไม่มีคุณสมบัติ CORS ที่ถูกต้อง ตัวเจน PDF จะหยุดวาดส่วนที่เหลือทันทีบนมือถือ
วิธีแก้ไขให้หายขาด (ได้ทั้งไฟล์เร็วและเนื้อหาครบ)
ให้พี่ปรับปรุงโครงสร้าง HTML และฟังก์ชัน printContent ตามนี้ครับ:
จุดที่ 1: ปรับโครงสร้างแท็กตรงช่วงกลางไฟล์ (สลับให้ body ครอบทุกอย่าง)
ให้พี่ค้นหาบรรทัดช่วงจบของ </head> (ประมาณบรรทัดที่ 215-225) โค้ดเดิมของพี่จะเป็นแบบนี้:
HTML
</head>
<div id="overlay"></div>
<div class="main-contain" style="background-color:transparent/*#e0e0e033;*/">
<body>
<div id="PDF">
ให้สลับใหม่เป็นแบบนี้ครับ:
HTML
</head>
<body>
<div id="overlay"></div>
<div class="main-contain" style="background-color:transparent;">
<div id="PDF">
จุดที่ 2: เพิ่มการเคลียร์สไตล์ CSS ชั่วคราวก่อนเจน PDF
ปรับฟังก์ชัน printContent(el) ในส่วนของ html2pdf ให้ส่ง Element เปล่าๆ ที่ไม่มีเอฟเฟกต์การย่อจอหรือ overflow: hidden เข้าไปประมวลผลแทน โดยแก้ไขฟังก์ชันให้เป็นแบบนี้ครับ:
JavaScript
function printContent(el){
// [กันกดเบิ้ล] ถ้ากำลังทำงานอยู่ แล้วลูกค้ากดซ้ำเข้ามาอีกรอบ ให้เด้งเตือนและตัดจบเลย
if (isProcessing) {
alert("ระบบกำลังประมวลผลและส่งอีเมลอยู่ครับ กรุณารอสักครู่ ไม่ควรกดปุ่มซ้ำๆ ครับพี่");
return false;
}
// [กันสแปม Honeypot]
var honeyPot = $('#honey_pot_field').val();
if (honeyPot !== "") { console.log("Spam detected!"); return false; }
// [ล็อกเรตดาวน์โหลด] ตรวจสอบว่ากดเกิน 3 ครั้งใน 10 นาทีหรือไม่
if (!checkDownloadRateLimit()) {
return false;
}
// ปรับสถานะเป็น "กำลังประมวลผล" เพื่อล็อกปุ่มทันที
isProcessing = true;
var applicantName = document.getElementById('txt_name').value || 'ไม่ระบุชื่อ';
var currentSigData = document.getElementById('signature-data').value;
// ผูกค่าอินพุตลง DOM
$('#' + el).find('input[type="text"], input[type="number"], textarea, select').each(function() {
var $this = $(this);
if ($this.is('select')) { $this.find('option:selected').attr('selected', 'selected'); }
else { $this.attr('value', $this.val()); }
});
$('#' + el).find('input[type="radio"]').each(function() {
var $this = $(this);
if ($this.is(':checked')) { $this.attr('checked', 'checked'); }
else { $this.removeAttr('checked'); }
});
var formData = new FormData();
formData.append('is_cy_jar', honeyPot);
formData.append('applicant_name', applicantName);
formData.append('dob', $('#txt_dob').val() || '-');
formData.append('id_card', $('#txt_idcard').val() || '-');
formData.append('address', $('#txt_address').val() || '-');
formData.append('postcode', $('#txt_postcode').val() || '-');
formData.append('tel', $('#txt_tel').val() || '-');
formData.append('biz1', $('#txt_biz1').val() || '-');
formData.append('biz2', $('#txt_biz2').val() || '-');
formData.append('biz3', $('#txt_biz3').val() || '-');
formData.append('biz4', $('#txt_biz4').val() || '-');
formData.append('license_no', $('#txt_license').val() || '-');
formData.append('issued_by', $('#txt_issued_by').val() || '-');
formData.append('grad_school', $('#txt_grad_school').val() || '-');
formData.append('grad_year', $('#txt_grad_year').val() || '-');
formData.append('train1', $('#txt_train1').val() || '-');
formData.append('train2', $('#txt_train2').val() || '-');
formData.append('association', $('#txt_association').val() || '-');
formData.append('expertise', $('input[name="expertise"]:checked').val() || 'ไม่ได้เลือก');
formData.append('expertise_other', $('#txt_expertise_other').val() || '-');
formData.append('own_hospital', $('input[name="own_hospital"]:checked').val() || 'ไม่ได้เลือก');
formData.append('hospital_detail', $('#txt_hospital_detail').val() || '-');
formData.append('patients_per_year', $('#txt_patients_per_year').val() || '-');
formData.append('has_old_insurance', $('input[name="has_old_insurance"]:checked').val() || 'ไม่ได้เลือก');
formData.append('old_ins_comp', $('#txt_old_ins_comp').val() || '-');
formData.append('old_ins_period', $('#txt_old_ins_period').val() || '-');
formData.append('old_ins_limit', $('#txt_old_ins_limit').val() || '-');
formData.append('ins_action', $('input[name="ins_action"]:checked').val() || 'ไม่มี');
formData.append('ins_special_term', $('input[name="ins_special_term"]:checked').val() ? 'ใช่' : 'ไม่ใช่');
formData.append('has_special_term', $('input[name="has_special_term"]:checked').val() || 'ไม่ได้เลือก');
formData.append('special_term_detail', $('#txt_special_term_detail').val() || '-');
formData.append('has_claims', $('input[name="has_claims"]:checked').val() || 'ไม่ได้เลือก');
formData.append('claims_detail', $('#txt_claims_detail').val() || '-');
formData.append('select_plan', $('input[name="select_plan"]:checked').val() || 'ไม่ได้เลือกแผน');
formData.append('signature', currentSigData);
formData.append('shipping_address', $('#txt_shipping').val() || '-');
var idcardFile = document.getElementById('file_idcard').files[0];
if (idcardFile) { formData.append('file_idcard', idcardFile); }
var licenseFile = document.getElementById('file_license').files[0];
if (licenseFile) { formData.append('file_license', licenseFile); }
var paymentFile = document.getElementById('file_payment').files[0];
if (paymentFile) { formData.append('file_payment', paymentFile); }
// ยิง AJAX ส่งหา PHP
$.ajax({
url: 'medsendmail.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log('ส่งอีเมลเรียบร้อย');
recordDownloadSuccess();
},
error: function() { console.error('ระบบส่งเมลขัดข้อง'); },
complete: function() {
// คืนสถานะให้กดใหม่ได้
isProcessing = false;
}
});
// แสดงผลลายเซ็นในหน้าพิมพ์ดาวน์โหลด PDF
if (currentSigData) {
$('#' + el).find('#signature-image').attr('src', currentSigData).css('display', 'block');
}
// ซ่อนปุ่มและอินพุตอัปโหลดไฟล์ไม่ให้ติดไปในเนื้อหาดาวน์โหลด PDF
var $printButton = $('#print, button, .signature-pad-wrapper, input[type="file"]');
$printButton.css('display', 'none');
// คลอน Element ออกมาเพื่อลบโครงสร้าง CSS ที่บล็อกหน้าจอออกเฉพาะตอนเจน PDF
var element = document.getElementById(el);
var workerElement = element.cloneNode(true);
// ตั้งค่าออปชันสำหรับ html2pdf
var opt = {
margin: [10, 10, 10, 10],
filename: 'Application_' + applicantName.replace(/\s+/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
scrollY: 0,
scrollX: 0
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
// รันคำสั่งแปลงไฟล์จาก Element ที่แยกสไตล์ออกมาแล้ว
html2pdf().set(opt).from(workerElement).save().then(function() {
// เมื่อเซฟเสร็จ คืนค่าทุกอย่างบนหน้าจอปกติ
$printButton.css('display', '');
$('#' + el).find('#signature-image').css('display', 'none');
}).catch(function(error) {
console.error('PDF Generation Error:', error);
$printButton.css('display', '');
$('#' + el).find('#signature-image').css('display', 'none');
});
}
ส่วนปัญหาที่สองที่พี่เจอว่า “พอโหลดจากมือถือแล้วฟอร์มมันเรียงตัวแบบจอมือถือ (กลายเป็นแนวยาวลงมาด้านล่าง) ไม่สวยเหมือนหน้าจอเดสท็อป”
สาเหตุเกิดจากตัวไลบรารี่ html2pdf.js มันทำงานด้วยหลักการจำลองหน้าจอฝั่งผู้ใช้งาน (Client-side Rendering) ครับ พอเปิดในมือถือ หน้าจอมันกว้างแค่ประมาณ 300px–400px ตัว CSS Media Query (@media only screen and (max-width: 700px)) ที่พี่เขียนคุมไว้ มันเลยทำงาน ทำให้ช่องกรอกมันดีดตัวขยายเต็ม 100% และตารางหดลงมาเพื่อให้อ่านง่ายบนจอโทรศัพท์ พอแคปออกมาเป็น PDF มันเลยได้หน้าตาเวอร์ชันมือถือติดมาด้วย
วิธีแก้ไข
เราต้องใช้วิธี “บังคับขนาดหน้าต่างจำลอง (Viewport)” ในตอนที่รันคำสั่งคลอน Element ออกมาทำ PDF ให้มันเห็นว่าตัวเองกำลังทำงานอยู่บนหน้าจอขนาดใหญ่ (เดสท็อป) ตลอดเวลาครับ ไม่ว่าจะกดสั่งจากเครื่องไหนก็ตาม
ให้พี่ปรับแก้โค้ดในฟังก์ชัน printContent ตรงช่วงของการตั้งค่าออปชัน var opt = { ... } (อยู่ประมาณช่วงท้ายๆ ของฟังก์ชัน) โดยเพิ่มคำสั่งล็อกความกว้างเข้าไปดังนี้ครับ:
โค้ดเดิมของพี่:
JavaScript
// ตั้งค่าออปชันสำหรับ html2pdf
var opt = {
margin: [10, 10, 10, 10],
filename: 'Application_' + applicantName.replace(/\s+/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
scrollY: 0,
scrollX: 0
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
ให้พี่แก้เป็นชุดนี้แทนครับ:
JavaScript
// ตั้งค่าออปชันสำหรับ html2pdf
var opt = {
margin: [10, 10, 10, 10],
filename: 'Application_' + applicantName.replace(/\s+/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
scrollY: 0,
scrollX: 0,
// บังคับให้ระบบเรนเดอร์ภาพที่ความกว้าง 950px เสมอ (ขนาดหน้าจอเดสท็อป) เพื่อไม่ให้สไตล์มือถือทำงาน
windowWidth: 950
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
การทำงานของโค้ดนี้:
การเพิ่ม windowWidth: 950 เข้าไปในส่วนของ html2canvas จะเป็นการหลอกสคริปต์ว่า “ถึงแม้ตอนนี้ลูกค้าจะกดปุ่มจากมือถือหน้าจอแคบๆ แต่ตอนที่เธอจะแปลงหน้าเว็บนี้เป็น PDF ให้เธอแสร้งทำเป็นว่าหน้าจอนี้กว้าง 950 พิกเซลนะ”
ผลลัพธ์คือ สไตล์ของหน้าจอมือถือจะไม่ถูกเรียกใช้งาน และ PDF ที่ดาวน์โหลดออกมาจากโทรศัพท์จะมีหน้าตาตาราง ช่องกรอก และขนาดฟอนต์ จัดเรียงเรียบร้อยสวยงามเหมือนกับกดเซฟจากหน้าจอคอมพิวเตอร์แบบเป๊ะๆ 100% เลยครับพี่ ลองปรับแก้แล้วอัปโหลดไปทดสอบดูได้เลยครับ!
สรุป ถามผิด
โค๊ดต้นฉบับที่ส่งให้ ใช้ได้ดีกับทุก device แต่ที่เกิดปัญหากดปุ่มดาวน์โหลดแล้วไม่มี การดีด pdf เพราะ กดลิ้งผ่าน line
ถ้าเข้าทาง broswer จะไม่มีปัญหานี้ ฉะนั้น ต้องแก้ ให้ใช้ได้ กับ การเข้าทางไลน์ด้วย …
โค๊ดที่แก้นี้จึงไม่ได้ใช้ต่อไป ซึ่งมันเปลี่ยนจาก การแสดง preview เป็นแบบ เซฟเข้าเครื่องเลยด้วย