Khách hàng lướt xem thư viện ảnh sản phẩm, thấy ưng ý một mẫu nhưng lại lười tìm đến trang liên hệ để gửi yêu cầu? Đây là điểm “rơi vãi” tỷ lệ chuyển đổi (CR) rất phổ biến trên các website bán hàng hoặc nội thất. Để giải quyết triệt để vấn đề này, Shoppingweb mang đến cho bạn một giải pháp đột phá: Code Gallery VIP tích hợp Form Báo Giá chớp nhoáng.
Không chỉ tạo ra một giao diện Lightbox chuyên nghiệp hiển thị rõ SKU và Giá, đoạn code này còn nhúng trực tiếp một Form điền thông tin ngay trong Popup. Đặc biệt, hệ thống được trang bị công nghệ AJAX kết hợp gửi Email chạy nền (Background Email), mang lại trải nghiệm gửi form mượt mà “không độ trễ”, giúp bạn chốt sale ngay tại thời điểm khách hàng cảm xúc dâng cao nhất!
Bạn chỉ cần copy cho vào file functions.php là được nhé
[foxhi id=’1624′]/**
* 1. Tạo Shortcode [ux_gallery_vip] - Form Báo Giá + AJAX Submit + Validate Phone
*/
add_shortcode( 'ux_gallery_vip', function( $atts ) {
$atts = shortcode_atts( array(
'ids' => '',
), $atts );
if ( empty( $atts['ids'] ) ) {
return '<p style="text-align:center; padding: 50px 0;">Vui lòng chọn hình ảnh cho Gallery VIP.</p>';
}
$_attachments = get_posts( array(
'include' => $atts['ids'],
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => 'ASC',
'orderby' => 'post__in',
'posts_per_page' => -1
) );
if ( empty( $_attachments ) ) return '';
ob_start();
?>
<div class="lux-single-gach">
<div class="sku-section">
<h4 class="lux-fw-bold lux-title-section">Bảng Mã Sản Phẩm (SKU)</h4>
<div class="lux-gallery" id="lux-gallery-wrapper">
<?php
foreach ( $_attachments as $attachment ) :
$sku = ! empty( $attachment->post_excerpt ) ? $attachment->post_excerpt : 'Đang cập nhật';
$price = ! empty( $attachment->post_content ) ? $attachment->post_content : 'Liên hệ';
$img_url = wp_get_attachment_image_url( $attachment->ID, 'large' );
?>
<div class="lux-image-item">
<div class="lux-image-wrapper">
<div class="lux-badge-sku"><?php echo esc_html( $sku ); ?></div>
<div class="lux-badge-price"><?php echo esc_html( $price ); ?></div>
<img src="<?php echo esc_url( $img_url ); ?>" alt="<?php echo esc_attr( $sku ); ?>">
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="lux-preview-box" id="luxPreviewBox">
<div class="lux-preview-details">
<span class="lux-fw-bold">Mẫu <span class="current-img lux-text-gold"></span> / <span class="total-img"></span></span>
<span class="lux-close-icon" id="closeLightbox">×</span>
</div>
<div class="lux-image-box">
<div class="lux-badge-sku" id="lb-sku"></div>
<div class="lux-badge-price" id="lb-price"></div>
<div class="lux-slide prev">❮</div>
<div class="lux-slide next">❯</div>
<img src="" alt="Preview" id="lightboxImg">
</div>
<div class="lux-preview-footer">
<button class="lux-btn-gold" id="btnOpenOrderModal">
<span>➤</span> Mua Ngay / Xem Chi Tiết
</button>
</div>
</div>
<div class="lux-lb-shadow" id="lbShadow"></div>
<div class="modal lux-custom-modal fade" id="orderModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-white border-warning">
<div class="modal-header border-secondary" style="display: flex; justify-content: space-between; align-items: center;">
<h5 class="modal-title text-gold fw-bold m-0" style="color: #edb301;">Yêu Cầu Báo Giá</h5>
<button type="button" class="btn-close btn-close-white" style="background: none; border: none; font-size: 24px; color: #fff; cursor: pointer;">×</button>
</div>
<div class="modal-body p-4">
<form id="luxOrderForm">
<input type="hidden" id="luxAjaxUrl" value="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>">
<input type="hidden" id="luxNonce" value="<?php echo esc_attr( wp_create_nonce( 'lux_quote_nonce' ) ); ?>">
<div class="mb-3" style="margin-bottom: 15px;">
<label class="small text-muted mb-1" style="display: block;">Mẫu đã chọn:</label>
<input type="text" class="form-control bg-black text-gold border-secondary fw-bold" id="skuInput" readonly style="width: 100%; padding: 10px; background: #000; color: #edb301; border: 1px solid #555;">
</div>
<div class="mb-3" style="margin-bottom: 15px;">
<input type="text" class="form-control bg-dark text-white border-secondary" id="nameInput" placeholder="Họ và tên" required style="width: 100%; padding: 10px; background: #222; color: #fff; border: 1px solid #555;">
</div>
<div class="mb-4" style="margin-bottom: 25px;">
<input type="tel" class="form-control bg-dark text-white border-secondary" id="phoneInput" placeholder="Số điện thoại / Zalo (9-10 số)" required pattern="[0-9]*" style="width: 100%; padding: 10px; background: #222; color: #fff; border: 1px solid #555;">
</div>
<button type="submit" id="btnSubmitForm" class="btn btn-gold w-100 py-2" style="width: 100%; padding: 12px; border: none; cursor: pointer;">GỬI YÊU CẦU NGAY</button>
</form>
</div>
</div>
</div>
</div>
<style>
.lux-custom-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.85); z-index: 100009 !important; overflow-x: hidden; overflow-y: auto; opacity: 0; transition: opacity 0.3s ease; }
.lux-custom-modal.show { opacity: 1; }
.lux-custom-modal .modal-dialog { position: relative; width: auto; margin: 1.75rem auto; max-width: 500px; display: flex; align-items: center; min-height: calc(100% - 3.5rem); }
.lux-custom-modal .modal-content { position: relative; display: flex; flex-direction: column; width: 100%; pointer-events: auto; background-clip: padding-box; border-radius: 0.3rem; border: 1px solid #edb301; background-color: #111; }
.lux-custom-modal .modal-header { padding: 1rem 1rem; border-bottom: 1px solid #444; }
.lux-custom-modal .modal-body { padding: 1.5rem; }
#luxOrderForm .form-control::placeholder { color: #ffffff !important; opacity: 0.8 !important; }
#luxOrderForm .form-control::-webkit-input-placeholder { color: #ffffff !important; opacity: 0.8 !important; }
#luxOrderForm .form-control::-moz-placeholder { color: #ffffff !important; opacity: 0.8 !important; }
#luxOrderForm .form-control:-ms-input-placeholder { color: #ffffff !important; opacity: 0.8 !important; }
#luxOrderForm .form-control:focus { background-color: #111 !important; border-color: #d4af37 !important; box-shadow: 0 0 0 0.25rem rgba(212, 175, 55, 0.25) !important; color: #ffffff !important; outline: none; }
#btnOpenOrderModal, #luxOrderForm button { margin: 0; background: #dfb823; font-weight: bold; color: #000; border-radius: 5px; }
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const images = document.querySelectorAll("#lux-gallery-wrapper .lux-image-item");
if(!images.length) return;
const previewBox = document.getElementById("luxPreviewBox"),
previewImg = document.getElementById("lightboxImg"),
lbShadow = document.getElementById("lbShadow"),
lbSku = document.getElementById("lb-sku"),
lbPrice = document.getElementById("lb-price"),
currentImg = previewBox.querySelector(".current-img"),
totalImg = previewBox.querySelector(".total-img"),
closeLbBtn = document.getElementById("closeLightbox"),
btnOpenModal = document.getElementById("btnOpenOrderModal");
const orderModal = document.getElementById("orderModal");
const skuInput = document.getElementById("skuInput");
const nameInput = document.getElementById("nameInput");
const phoneInput = document.getElementById("phoneInput");
const closeModals = orderModal.querySelectorAll(".btn-close");
const orderForm = document.getElementById("luxOrderForm");
const btnSubmit = document.getElementById("btnSubmitForm");
let currentIndex = 0;
function showImage(index) {
const item = images[index];
previewImg.src = item.querySelector("img").src;
lbSku.innerText = item.querySelector(".lux-badge-sku").innerText;
lbPrice.innerText = item.querySelector(".lux-badge-price").innerText;
currentImg.innerText = index + 1;
totalImg.innerText = images.length;
currentIndex = index;
previewBox.querySelector(".prev").style.display = index === 0 ? "none" : "flex";
previewBox.querySelector(".next").style.display = index === images.length - 1 ? "none" : "flex";
}
images.forEach((img, i) => {
img.onclick = () => {
showImage(i);
previewBox.classList.add("show");
lbShadow.style.display = "block";
document.body.style.overflow = "hidden";
};
});
const closeLightbox = () => {
previewBox.classList.remove("show");
lbShadow.style.display = "none";
document.body.style.overflow = "auto";
};
closeLbBtn.onclick = closeLightbox;
lbShadow.onclick = closeLightbox;
previewBox.querySelector(".prev").onclick = (e) => { e.stopPropagation(); showImage(currentIndex - 1); };
previewBox.querySelector(".next").onclick = (e) => { e.stopPropagation(); showImage(currentIndex + 1); };
btnOpenModal.onclick = function() {
skuInput.value = lbSku.innerText;
orderModal.style.display = "block";
setTimeout(() => { orderModal.classList.add("show"); }, 10);
};
const closeModal = () => {
orderModal.classList.remove("show");
setTimeout(() => { orderModal.style.display = "none"; }, 300);
};
closeModals.forEach(btn => { btn.onclick = closeModal; });
orderModal.onclick = (e) => {
if (e.target === orderModal) { closeModal(); }
};
// RÀNG BUỘC KHI NHẬP CHỈ CHO PHÉP NHẬP SỐ
phoneInput.addEventListener('input', function (e) {
this.value = this.value.replace(/[^0-9]/g, '');
});
// BẮN AJAX KHI GỬI FORM
orderForm.onsubmit = async function(e) {
e.preventDefault();
// 1. FRONTEND VALIDATION: Kiểm tra định dạng số điện thoại
const phoneVal = phoneInput.value.trim();
// Regex: Bắt đầu bằng 0, tiếp theo là 8 đến 9 chữ số (Tổng 9-10 số)
const phoneRegex = /^0[0-9]{8,9}$/;
if (!phoneRegex.test(phoneVal)) {
alert('Vui lòng nhập số điện thoại hợp lệ (Bắt đầu bằng số 0, gồm 9-10 số).');
phoneInput.focus();
return; // Dừng lại không cho gửi
}
const ajaxUrl = document.getElementById('luxAjaxUrl').value;
const nonce = document.getElementById('luxNonce').value;
const originalBtnText = btnSubmit.innerText;
btnSubmit.innerText = "ĐANG GỬI...";
btnSubmit.disabled = true;
const formData = new FormData();
formData.append('action', 'lux_submit_quote');
formData.append('_ajax_nonce', nonce);
formData.append('sku', skuInput.value);
formData.append('name', nameInput.value);
formData.append('phone', phoneVal);
try {
const response = await fetch(ajaxUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
alert('Gửi yêu cầu thành công! Chúng tôi sẽ liên hệ lại sớm nhất.');
closeModal();
orderForm.reset();
} else {
alert('Lỗi: ' + (result.data || 'Không thể gửi yêu cầu.'));
}
} catch (error) {
alert('Có lỗi xảy ra kết nối mạng. Vui lòng thử lại.');
console.error(error);
} finally {
btnSubmit.innerText = originalBtnText;
btnSubmit.disabled = false;
}
};
});
</script>
<?php
return ob_get_clean();
} );
/**
* 2. Đăng ký Element vào Flatsome UX Builder
*/
add_action( 'ux_builder_setup', function() {
add_ux_builder_shortcode( 'ux_gallery_vip', array(
'name' => __( 'Gallery Trang Đơn', 'flatsome-admin' ),
'category' => __( 'Content', 'flatsome-admin' ),
'icon' => 'icon-image',
'options' => array(
'ids' => array(
'type' => 'gallery',
'heading' => __( 'Chọn hình ảnh', 'flatsome-admin' ),
),
),
) );
} );
/**
* 3. XỬ LÝ BACKEND - Nhận AJAX cực nhanh và Gửi Email chạy ngầm (Async)
*/
add_action( 'wp_ajax_lux_submit_quote', 'lux_handle_submit_quote_ajax' );
add_action( 'wp_ajax_nopriv_lux_submit_quote', 'lux_handle_submit_quote_ajax' );
function lux_handle_submit_quote_ajax() {
// 3.1. Xác thực Nonce (Chống CSRF)
if ( ! isset( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'lux_quote_nonce' ) ) {
wp_send_json_error( 'Xác thực bảo mật thất bại.' );
}
// 3.2. Sanitize dữ liệu
$sku = isset( $_POST['sku'] ) ? sanitize_text_field( wp_unslash( $_POST['sku'] ) ) : '';
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$phone = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';
$phone = preg_replace('/[^0-9]/', '', $phone);
// 3.3. Validation
if ( empty( $name ) || empty( $phone ) ) {
wp_send_json_error( 'Vui lòng điền đầy đủ họ tên và số điện thoại.' );
}
if ( strlen($phone) < 9 || strlen($phone) > 10 || substr($phone, 0, 1) !== '0' ) {
wp_send_json_error( 'Số điện thoại không hợp lệ.' );
}
// 3.4. Đưa vào hàng đợi (Queue) để gửi mail chạy nền thay vì bắt người dùng chờ
$mail_data = array(
'sku' => $sku,
'name' => $name,
'phone' => $phone,
'time' => current_time('d-m-Y H:i:s')
);
// Lên lịch gửi email ngay lập tức (nhưng tách biệt khỏi tiến trình AJAX hiện tại)
wp_schedule_single_event( time(), 'lux_background_send_email_action', array( $mail_data ) );
// 3.5. Trả về kết quả cho Frontend ngay lập tức (Tốc độ phản hồi < 0.5 giây)
wp_send_json_success( 'Tuyệt vời!' );
}
/**
* 4. WORKER GỬI EMAIL (Chạy ngầm ở Background)
*/
add_action( 'lux_background_send_email_action', 'lux_execute_background_email', 10, 1 );
function lux_execute_background_email( $data ) {
if ( empty( $data['sku'] ) || empty( $data['phone'] ) ) return;
$to = array( 'havanthuat1993@gmail.com', 'thuatpartner@gmail.com' );
$subject = 'Yêu cầu Báo Giá / Tư Vấn - SKU: ' . $data['sku'];
$message = '<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #ddd; border-radius: 5px; max-width: 600px;">';
$message .= '<h2 style="color: #d4af37; border-bottom: 2px solid #eee; padding-bottom: 10px;">Có khách hàng yêu cầu báo giá mẫu</h2>';
$message .= '<p><strong>Mã sản phẩm (SKU):</strong> <span style="color:red; font-size:16px;">' . esc_html( $data['sku'] ) . '</span></p>';
$message .= '<p><strong>Họ và tên khách hàng:</strong> ' . esc_html( $data['name'] ) . '</p>';
$message .= '<p><strong>Số điện thoại / Zalo:</strong> <a href="tel:' . esc_html( $data['phone'] ) . '" style="color:#0056b3; font-weight:bold;">' . esc_html( $data['phone'] ) . '</a></p>';
$message .= '<p><strong>Thời gian gửi:</strong> ' . esc_html( $data['time'] ) . '</p>';
$message .= '<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">';
$message .= '<p style="font-size: 12px; color: #777;">Email được gửi tự động từ hệ thống website Luxcera.vn.</p>';
$message .= '</div>';
$headers = array('Content-Type: text/html; charset=UTF-8');
// Gửi mail trong background
$sent = wp_mail( $to, $subject, $message, $headers );
if ( ! $sent ) {
error_log('Luxcera Async Mail Error: Failed to send quote for SKU: ' . $data['sku']);
}
}
Đoạn mã này là một hệ thống khép kín hoàn chỉnh từ Frontend (giao diện) đến Backend (máy chủ), sở hữu những tính năng cao cấp sau:
1. Giao Diện Bắt Mắt & Trải Nghiệm Mượt Mà (Frontend)
- Kế thừa ưu điểm hiển thị: Vẫn tự động lấy Mã sản phẩm (SKU) và Giá từ thư viện Media của WordPress hiển thị lên giao diện.
- Popup Form “1 Click”: Thay vì chuyển hướng (redirect), nút “Mua Ngay” giờ đây sẽ mở ra một Modal (bảng điền thông tin) mang phong cách Dark Theme cực kỳ sang trọng. Mã SKU của sản phẩm khách hàng đang xem sẽ tự động điền sẵn vào form, giúp họ tiết kiệm thao tác.
- Kiểm duyệt dữ liệu chặt chẽ (Validation): Sử dụng JavaScript để ép người dùng chỉ được nhập số vào ô điện thoại. Đồng thời, form cũng tự động kiểm tra định dạng số điện thoại chuẩn Việt Nam (Bắt đầu bằng số 0, gồm 9-10 chữ số) trước khi cho phép gửi.
2. Tối Ưu Tốc Độ Tuyệt Đối Bằng AJAX & Chạy Nền (Backend) Đây là phần “đắt giá” nhất của đoạn code:
- Công nghệ AJAX: Khi khách ấn “Gửi yêu cầu”, form sẽ bắn dữ liệu ngầm mà không làm tải lại (reload) trang web. Khách hàng sẽ nhận được thông báo thành công gần như ngay lập tức.
- Gửi Email Chạy Nền (Async Background Worker): Thông thường, các plugin form (như Contact Form 7) bắt người dùng phải “chờ” máy chủ gửi xong email (mất từ 2-5 giây). Code này sử dụng hàm
wp_schedule_single_eventcủa WordPress để đưa việc gửi Email vào hàng đợi (Queue). Máy chủ sẽ báo “Thành công” cho khách ngay lập tức (dưới 0.5s), và âm thầm gửi email thông báo về máy của Admin ở phía sau. - Bảo mật tối đa: Tích hợp
wp_create_noncegiúp chống lại các cuộc tấn công giả mạo yêu cầu (CSRF), đảm bảo hệ thống không bị spam tự động.
3. Tích Hợp Thẳng Vào UX Builder Của Flatsome (Kéo thả dễ dàng) Dù đoạn code bên dưới chứa đựng một “cỗ máy” xử lý nền tảng cực kỳ phức tạp (từ hiển thị UI, xác thực dữ liệu đến gửi mail Async), nhưng trải nghiệm dành cho quản trị viên website lại vô cùng đơn giản.
Đoạn mã này sẽ tự động sinh ra một Element hoàn toàn mới mang tên “Gallery Trang Đơn” nằm gọn trong thư viện Content của UX Builder. Giờ đây, để tạo một bộ sưu tập ảnh kèm nút báo giá, bạn không cần phải chạm vào một dòng code nào nữa. Chỉ cần mở trang bằng UX Builder, kéo thả block này ra, chọn hình ảnh từ thư viện Media là xong – mọi thuật toán sẽ tự động chạy mượt mà.
