Add or Remove Product Using Ajax

本文介绍如何在Magento中定制产品对比功能,包括配置模块、控制器文件的修改以及前端展示方式。实现了通过AJAX动态更新产品对比状态,并使用jQuery处理前端交互。

1. rewrite core compare action

<?xml version="1.0"?>
<config>
  <modules>
    <Bysoft_Mycatalog>
      <version>0.1.0</version>
    </Bysoft_Mycatalog>
  </modules>
  <global>
    <helpers>
      <mycatalog>
        <class>Bysoft_Mycatalog_Helper</class>
      </mycatalog>
      <catalog>
		<rewrite>
			<product_compare>Bysoft_Mycatalog_Helper_Product_Compare</product_compare>
		</rewrite>
      </catalog>
    </helpers>
    <resources>
	  <mycatalog_setup>
		<setup>
		  <module>Bysoft_Mycatalog</module>
		  <class>Mage_Eav_Model_Entity_Setup</class>
		</setup>
		<connection>
		  <use>core_setup</use>
		</connection>
	  </mycatalog_setup>
	  <mycatalog_write>
		<connection>
		  <use>core_write</use>
		</connection>
	  </mycatalog_write>
	  <mycatalog_read>
		<connection>
		  <use>core_read</use>
		</connection>
	  </mycatalog_read>
	</resources>
	<blocks>
	  <mycatalog>
		<class>Bysoft_Mycatalog_Block</class>
	  </mycatalog>
			<catalog>
				<rewrite>
					<layer_view>Bysoft_Mycatalog_Block_Layer_View</layer_view>
					<category_view>Bysoft_Mycatalog_Block_Category_View</category_view>
				</rewrite>
			</catalog>
	</blocks>
  </global>
  <frontend>
        <routers>
            <catalog>
				<args>
					<modules>
						<bysoft_mycatalog before="Mage_Catalog">Bysoft_Mycatalog</bysoft_mycatalog>
					</modules>
				</args>
			</catalog>
        </routers>
        <layout>
            <updates>
                <arithmetic>
                    <file>mycatalog.xml</file>
                </arithmetic>
            </updates>
        </layout>   
  </frontend>
</config> 

 2. in controller file. 

<?php 
require_once(Mage::getModuleDir('controllers','Mage_Catalog').DS.'Product'.DS.'CompareController.php');
class Bysoft_Mycatalog_Product_CompareController extends Mage_Catalog_Product_CompareController {
	public function changeajaxAction() {
		$productId = (int) $this->getRequest()->getParam('product');
		if ($productId
				&& (Mage::getSingleton('log/visitor')->getId() || Mage::getSingleton('customer/session')->isLoggedIn())
		) {
			if ($this->is_compare($productId)) {
				// to uncompared
				 $this->remove_compare($productId);
				
			} else {
				// to compared
				 $this->add_compare($productId);
			}			
		}		
	}
	/**
	 * check the product is compare or not
	 * @param unknown $productId
	 */
	public function is_compare($productId) {
		
		$product = Mage::getModel('catalog/product')->load($productId);
		$item = Mage::getModel('catalog/product_compare_item');
		$list = Mage::getModel('catalog/product_compare_list');
		//$list->_addVisitorToItem($item);
	
		$item->addVisitorId(Mage::getSingleton('log/visitor')->getId());
		if (Mage::getSingleton('customer/session')->isLoggedIn()) {
			$item->addCustomerData(Mage::getSingleton('customer/session')->getCustomer());
		}
	
		$item->loadByProduct($product);
		if ($item->getId()) {
			return true;
		} else {
			return false;
		}
	}
	
	public function add_compare($productId) {
		try {
			if ($productId
					&& (Mage::getSingleton('log/visitor')->getId() || Mage::getSingleton('customer/session')->isLoggedIn())
			) {
				$product = Mage::getModel('catalog/product')
				->setStoreId(Mage::app()->getStore()->getId())
				->load($productId);
		
				if ($product->getId()/* && !$product->isSuper()*/) {
					Mage::getSingleton('catalog/product_compare_list')->addProduct($product);
					Mage::getSingleton('catalog/session')->addSuccess(
					$this->__('The product %s has been added to comparison list.', Mage::helper('core')->escapeHtml($product->getName()))
					);
					Mage::dispatchEvent('catalog_product_compare_add_product', array('product'=>$product));
				}
		
				
				Mage::helper('catalog/product_compare')->calculate();
			}
			echo json_encode(array('status'=>'1','is_compare'=>'1'));
		} catch (Exception $e) {
			echo json_encode(array('status'=>'2','msg'=>$e->getMessage()));
		}
	}
	
	public function remove_compare($productId) {
		try {
			if ((int) $productId) {
				$product = Mage::getModel('catalog/product')
				->setStoreId(Mage::app()->getStore()->getId())
				->load($productId);
		
				if($product->getId()) {
					/** @var $item Mage_Catalog_Model_Product_Compare_Item */
					$item = Mage::getModel('catalog/product_compare_item');
					if(Mage::getSingleton('customer/session')->isLoggedIn()) {
						$item->addCustomerData(Mage::getSingleton('customer/session')->getCustomer());
					} elseif ($this->_customerId) {
						$item->addCustomerData(
								Mage::getModel('customer/customer')->load($this->_customerId)
						);
					} else {
						$item->addVisitorId(Mage::getSingleton('log/visitor')->getId());
					}
		
					$item->loadByProduct($product);
		
					if($item->getId()) {
						$item->delete();
						Mage::getSingleton('catalog/session')->addSuccess(
						$this->__('The product %s has been removed from comparison list.', $product->getName())
						);
						Mage::dispatchEvent('catalog_product_compare_remove_product', array('product'=>$item));
						Mage::helper('catalog/product_compare')->calculate();
					}
				}
			}
			echo  json_encode(array('status'=>'1','is_compare'=>0));
		} catch (Exception $e) {
			echo  json_encode(array('status'=>'2','msg'=>$e->getMessage()));
		}
	}
	
}
?>

 3. in frontend phtml file

<div class="compare-box">
	                        <a class="compare-a <?php echo $this->getCompareClass($_product->getId());?>"	                     	                 
	                        to_url="<?php echo $_compare_helper->getChangeAjaxUrl($_product);?>"
	                        >
	                    	<?php echo $this->getCompareLabel($_product->getId());?></a></div>

 4. jquery ajax

<script type="text/javascript">//<![CDATA[
   (function($){
		$(".compare-a").click(function(){
			var current_obj = $(this);
			var is_compare =  $(this).attr('is_compare');		
			var ajax_url =  $(this).attr('to_url');
			$.ajax({  
		            url: ajax_url,  
		            type: "POST",  
		            dataType:"json",  
		            success: function(json_obj) { 
		                if(json_obj.status == 1) {
		                	if (json_obj.is_compare == '1') {
			                	alert('is_compare:1');
			                	current_obj.addClass('compared');
			                	current_obj.removeClass('uncompared');			                	
			                	current_obj.html('Compared'); 
		                	} else {
		                		alert('is_compare:0');
		                		current_obj.addClass('uncompared');
			                	current_obj.removeClass('compared');			                
			                	current_obj.html('Uncompared');
			                }
		                } else {
			                alert(json_obj.msg);		            
		                }  
		            }
		    }); 
		})
    })(jQuery); 
         //]]></script>

 

AI Overview Implementing selective checkout with checkboxes on a cart page typically involves allowing users to select specific items in their cart for purchase, rather than requiring them to purchase all items. This functionality is not a standard feature in most e-commerce platforms and usually requires custom development or the use of specialized plugins/apps. General Approach: Modify the Cart Template: Add a checkbox next to each item in the cart. Ensure the checkbox is linked to the specific product or line item. Handle User Selections: Use JavaScript to detect changes in checkbox states (checked/unchecked). When a checkbox is selected or deselected, update the cart's subtotal and potentially the items that will be included in the checkout process. Adjust the Checkout Process: When the user proceeds to checkout, ensure that only the items corresponding to the selected checkboxes are passed to the order. This may involve modifying the platform's core checkout logic or using hooks/filters provided by the platform. Platform-Specific Considerations: WooCommerce (WordPress): Requires custom code in functions.php or a custom plugin to add checkboxes, manage selections, and modify the checkout process. You might use AJAX to update the cart dynamically as selections are made. Shopify: Involves modifying theme files (e.g., cart-template.liquid, theme.js) to add checkboxes and JavaScript to handle the logic. This often requires familiarity with Liquid (Shopify's templating language) and JavaScript. Other Platforms: The specific implementation will vary based on the platform's architecture and extensibility options. It may involve similar approaches of template modification and custom code. Important Notes: Complexity: Implementing selective checkout can be complex and may require advanced coding skills. User Experience: Carefully consider the user experience implications of selective checkout to ensure it is intuitive and does not cause confusion. Testing: Thoroughly test the functionality to ensure that selected items are correctly processed and that no loopholes exist (e.g., refreshing the page causing issues with selected items). I want to achieve this selective checkbox function in my cart page. Im using woocommerce and woodmart theme. Please advice on what to do
07-30
on functions.php, it says Your PHP code changes were not applied due to an error on line 82 of file wp-content/themes/woodmart-child/functions.php. Please fix and try saving again. Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, null given in wp-content/themes/woodmart-child/functions.php:82 Stack trace: #0 wp-content/themes/woodmart-child/functions.php(82): method_exists() #1 wp-includes/class-wp-hook.php(324): {closure}() #2 wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters() #3 wp-includes/plugin.php(517): WP_Hook->do_action() #4 wp-settings.php(749): do_action() #5 wp-config.php(105): require_once(‘/home/u18285596…’) #6 wp-load.php(50): require_once(‘/home/u18285596…’) #7 wp-admin/admin.php(35): require_once(‘/home/u18285596…’) #8 wp-admin/theme-editor.php(10): require_once(‘/home/u18285596…’) #9 {main} thrown cart.php <?php /** * Custom Cart Page for WooCommerce with Selective Checkout */ if (!defined('ABSPATH')) { exit; } do_action('woocommerce_before_cart'); // Provide context for JS (no layout change) $pc_cart_is_empty = WC()->cart->is_empty(); function pc_uid_for_view() { if (is_user_logged_in()) return 'user_' . get_current_user_id(); if (empty($_COOKIE['pc_cart_uid'])) { $token = wp_generate_uuid4(); setcookie('pc_cart_uid', $token, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', '', is_ssl(), false); $_COOKIE['pc_cart_uid'] = $token; } return 'guest_' . sanitize_text_field(wp_unslash($_COOKIE['pc_cart_uid'])); } $pc_uid = pc_uid_for_view(); // 已修正:仅在令牌存在时恢复购物车 if (method_exists(WC()->session, 'get')) { $token = WC()->session->get('pc_partial_token'); if ($token && !WC()->cart->is_empty()) { $payload = get_transient(pc_transient_key($token)); if (!empty($payload['snapshot'])) { pc_restore_cart_from_items($payload['snapshot']); WC()->cart->calculate_totals(); } } } ?> <div class="cart-page-section container" style="max-width: 1200px; margin: 0 auto;"> <form class="woocommerce-cart-form" action="<?php echo esc_url( wc_get_cart_url() ); ?>" method="post"> <?php do_action('woocommerce_before_cart_table'); ?> <?php wp_nonce_field('woocommerce-cart', 'woocommerce-cart-nonce'); ?> <table class="shop_table shop_table_responsive cart woocommerce-cart-form__contents"> <thead> <tr> <th class="product-select" style="width: 8%;"> <input type="checkbox" id="select-all-items" /> <label for="select-all-items" style="display: inline-block; margin-left: 5px; cursor: pointer;">全选</label> </th> <th class="product-info"><?php esc_html_e('Product', 'woocommerce'); ?></th> <th class="product-price"><?php esc_html_e('Price', 'woocommerce'); ?></th> <th class="product-quantity"><?php esc_html_e('Quantity', 'woocommerce'); ?></th> <th class="product-subtotal"><?php esc_html_e('Subtotal', 'woocommerce'); ?></th> <th class="product-remove"><?php esc_html_e('操作', 'woocommerce'); ?></th> </tr> </thead> <tbody> <?php do_action('woocommerce_before_cart_contents'); ?> <?php foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) : ?> <?php $_product = apply_filters('woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key); $product_id = apply_filters('woocommerce_cart_item_product_id', $cart_item['product_id'], $cart_item, $cart_item_key); if ( $_product && $_product->exists() && $cart_item['quantity'] > 0 && apply_filters('woocommerce_cart_item_visible', true, $cart_item, $cart_item_key) ) : $product_permalink = apply_filters('woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink($cart_item) : '', $cart_item, $cart_item_key); $line_total_value = (float) ($cart_item['line_total'] + $cart_item['line_tax']); $variation_id = isset($cart_item['variation_id']) ? (int)$cart_item['variation_id'] : 0; $variation_data = isset($cart_item['variation']) ? $cart_item['variation'] : array(); $variation_json = wp_json_encode($variation_data); ?> <tr class="woocommerce-cart-form__cart-item <?php echo esc_attr( apply_filters('woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key) ); ?>" data-cart_item_key="<?php echo esc_attr($cart_item_key); ?>" data-product_id="<?php echo esc_attr($product_id); ?>" data-variation_id="<?php echo esc_attr($variation_id); ?>" data-variation="<?php echo esc_attr($variation_json); ?>"> <!-- Checkbox Column --> <td class="product-select" data-title="<?php esc_attr_e('Select', 'woocommerce'); ?>"> <input type="checkbox" class="item-checkbox" name="selected_items[]" value="<?php echo esc_attr($cart_item_key); ?>" data-price="<?php echo esc_attr($line_total_value); ?>" /> </td> <!-- Product Info Column --> <td class="product-info" data-title="<?php esc_attr_e('Product', 'woocommerce'); ?>"> <div class="product-image"> <?php $thumbnail = apply_filters('woocommerce_cart_item_thumbnail', $_product->get_image(), $cart_item, $cart_item_key); if ( ! $product_permalink ) : echo $thumbnail; // PHPCS: XSS ok. else : printf('<a href="%s">%s</a>', esc_url($product_permalink), $thumbnail); // PHPCS: XSS ok. endif; ?> </div> <div class="product-name"> <?php if ( ! $product_permalink ) : echo wp_kses_post( apply_filters('woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key) . ' ' ); else : echo wp_kses_post( apply_filters('woocommerce_cart_item_name', sprintf('<a href="%s">%s</a>', esc_url($product_permalink), $_product->get_name()), $cart_item, $cart_item_key) ); endif; do_action('woocommerce_after_cart_item_name', $cart_item, $cart_item_key); echo wc_get_formatted_cart_item_data($cart_item); // PHPCS: XSS ok. ?> </div> </td> <!-- Price Column --> <td class="product-price" data-title="<?php esc_attr_e('Price', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_price', WC()->cart->get_product_price($_product), $cart_item, $cart_item_key); // PHPCS: XSS ok. ?> </td> <!-- Quantity Column --> <td class="product-quantity" data-title="<?php esc_attr_e('Quantity', 'woocommerce'); ?>"> <?php if ( $_product->is_sold_individually() ) : $product_quantity = sprintf('1 <input type="hidden" name="cart[%s][qty]" value="1" />', $cart_item_key); else : $product_quantity = woocommerce_quantity_input( array( 'input_name' => "cart[{$cart_item_key}][qty]", 'input_value' => $cart_item['quantity'], 'max_value' => $_product->get_max_purchase_quantity(), 'min_value' => '0', 'product_name' => $_product->get_name(), ), $_product, false ); endif; echo apply_filters('woocommerce_cart_item_quantity', $product_quantity, $cart_item_key, $cart_item); // PHPCS: XSS ok. ?> <small class="qty-status" style="display:none;margin-left:8px;color:#666;">保存中…</small> </td> <!-- Subtotal Column --> <td class="product-subtotal" data-title="<?php esc_attr_e('Subtotal', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal($_product, $cart_item['quantity']), $cart_item, $cart_item_key); // PHPCS: XSS ok. ?> </td> <!-- Remove Item Column --> <td class="product-remove" data-title="<?php esc_attr_e('操作', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_remove_link', sprintf( '<a href="%s" class="remove" aria-label="%s" data-product_id="%s" data-product_sku="%s">×</a>', esc_url( wc_get_cart_remove_url($cart_item_key) ), esc_attr__('Remove this item', 'woocommerce'), esc_attr($product_id), esc_attr($_product->get_sku()) ), $cart_item_key); ?> </td> </tr> <?php endif; ?> <?php endforeach; ?> <?php do_action('woocommerce_after_cart_contents'); ?> </tbody> </table> <?php do_action('woocommerce_after_cart_table'); ?> </form> </div> <!-- Sticky Footer --> <div class="cart-footer-actions sticky-footer" style="position: sticky; bottom: 0; background: white; padding: 15px; border-top: 1px solid #ddd; max-width: 1200px; margin: 0 auto;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div class="footer-left"> <input type="checkbox" id="footer-select-all"> <label for="footer-select-all" style="display: inline-block; margin-left: 5px; cursor: pointer;">全选</label> <button type="button" class="button" id="remove-selected-items">刪除選中的商品</button> <button type="button" class="button" id="clear-cart">清空購物車</button> </div> <div class="coupon-section"> <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="输入优惠券代码" style="padding: 8px; width: 200px; border: 1px solid #ddd; border-radius: 4px; margin-right: 5px;" /> <button type="button" class="button" id="apply-coupon">应用优惠券</button> </div> </div> <div style="display: flex; justify-content: space-between; align-items: center;"> <div class="selected-summary" style="font-size: 16px; font-weight: bold;"> 已选商品: <span id="selected-count">0</span> 件,共计: <span id="selected-total">RM0.00</span> </div> <a href="<?php echo esc_url( wc_get_checkout_url() ); ?>" class="checkout-button button alt wc-forward" id="partial-checkout">结算</a> </div> </div> <?php do_action('woocommerce_after_cart'); ?> <style> /* Layout styles (unchanged) */ .cart-page-section { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); margin-bottom: 30px; } .cart-page-section table.cart { width: 100%; border-collapse: collapse; margin-bottom: 20px; } .cart-page-section table.cart th, .cart-page-section table.cart td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; } .cart-page-section table.cart th { background-color: #f8f8f8; font-weight: bold; } .cart-page-section table.cart th.product-select, .cart-page-section table.cart td.product-select { width: 8%; text-align: center; vertical-align: middle; white-space: nowrap; } .cart-page-section table.cart td.product-info { width: 37%; vertical-align: top; } .cart-page-section table.cart .product-info .product-image { float: left; margin-right: 15px; width: 100px; height: 100px; } .cart-page-section table.cart .product-info .product-image img { max-width: 100%; height: auto; display: block; } .cart-page-section table.cart .product-info .product-name { overflow: hidden; } .cart-page-section table.cart td.product-price, .cart-page-section table.cart td.product-quantity, .cart-page-section table.cart td.product-subtotal { width: 15%; vertical-align: middle; } .cart-page-section table.cart td.product-remove { width: 5%; text-align: center; vertical-align: middle; } .cart-footer-actions { position: sticky; bottom: 0; background: #fff; padding: 15px; border-top: 1px solid #ddd; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); z-index: 1000; display: flex; flex-direction: column; gap: 15px; } .footer-left { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; } .coupon-section { display: flex; align-items: center; gap: 5px; } .selected-summary { font-size: 16px; font-weight: bold; flex: 1; } .cart-footer-actions .button { padding: 10px 20px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; white-space: nowrap; } .cart-footer-actions .button:hover { background-color: #d32f2f; } #partial-checkout { padding: 12px 24px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 4px; display: inline-block; transition: background-color 0.3s; white-space: nowrap; text-align: center; font-weight: bold; } #partial-checkout:hover { background-color: #0b7dda; } #coupon_code { padding: 8px; width: 200px; border: 1px solid #ddd; border-radius: 4px; transition: border-color 0.3s; } #coupon_code:focus { border-color: #2196F3; outline: none; } /* Responsive (unchanged) */ @media (max-width: 768px) { .cart-page-section table.cart thead { display: none; } .cart-page-section table.cart td { display: block; width: 100% !important; text-align: right; padding: 10px; position: relative; padding-left: 50%; } .cart-page-section table.cart td::before { content: attr(data-title); position: absolute; left: 15px; font-weight: bold; text-align: left; } .cart-page-section table.cart .product-info .product-image { float: none; margin: 0 auto 10px; } .cart-page-section table.cart td.product-select::before { content: "选择"; } .cart-footer-actions { flex-direction: column; align-items: flex-start; } .footer-left { width: 100%; justify-content: space-between; } .coupon-section { width: 100%; margin-top: 10px; } .coupon-section input { flex: 1; } .cart-footer-actions .footer-bottom-row { flex-direction: column; align-items: stretch; } .selected-summary { text-align: center; margin-bottom: 10px; } #partial-checkout { width: 100%; padding: 15px; } .cart-footer-actions .button { padding: 12px 15px; margin: 5px 0; width: 100%; text-align: center; } } @media (max-width: 480px) { .cart-page-section { padding: 15px; } .cart-page-section table.cart td { padding-left: 45%; } .cart-page-section table.cart td::before { font-size: 14px; } .cart-footer-actions { padding: 10px; } #coupon_code { width: 100%; } } /* Loader overlay above quantity input */ .product-quantity .quantity { position: relative; /* anchor for overlay */ } .quantity-saving-overlay { position: absolute; top: 0; left: 0; right: 0; height: 100%; display: none; /* hidden by default */ align-items: flex-start; /* appear at top */ justify-content: center;/* centered horizontally */ padding-top: 2px; background: rgba(255,255,255,0.0); /* transparent so it doesn't dim UI */ z-index: 10; pointer-events: none; /* don't block typing/clicks when visible */ } .quantity-saving-loader { width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> <script> jQuery(function($){ // Context var PC = { ajax_url: (window.wc_cart_params && window.wc_cart_params.ajax_url) || '<?php echo esc_url( admin_url("admin-ajax.php") ); ?>', security: (function(){ var n = $('input[name="woocommerce-cart-nonce"]').val(); if (n) return n; if (window.wc_cart_params && window.wc_cart_params.cart_nonce) return window.wc_cart_params.cart_nonce; return '<?php echo wp_create_nonce("woocommerce-cart"); ?>'; })(), cart_is_empty: <?php echo $pc_cart_is_empty ? 'true' : 'false'; ?>, cart_uid: '<?php echo esc_js($pc_uid); ?>' }; // Keys function lsKey(name){ return 'pc_' + name + '_' + PC.cart_uid; } var LS_SELECTION = lsKey('selected_items'); var LS_MASTER = lsKey('cart_master'); // Utilities function getSelectedKeys(){ return $('.item-checkbox:checked').map(function(){ return this.value; }).get(); } function fmtRM(n){ n = isNaN(n) ? 0 : n; return 'RM' + Number(n).toFixed(2); } // Selection persistence function readSelection(){ try { return JSON.parse(localStorage.getItem(LS_SELECTION) || '[]'); } catch(e){ return []; } } function writeSelection(keys){ try { localStorage.setItem(LS_SELECTION, JSON.stringify(keys||[])); } catch(e){} } function restoreSelectionFromLS(){ var saved = readSelection(); if (!Array.isArray(saved)) saved = []; $('.item-checkbox').each(function(){ $(this).prop('checked', saved.indexOf(this.value) !== -1); }); } window.addEventListener('storage', function(ev){ if (ev.key === LS_SELECTION) { restoreSelectionFromLS(); updateSelectedSummary(); } }); // Selected summary function updateSelectedSummary(){ var total = 0, count = 0; $('.item-checkbox:checked').each(function(){ var price = parseFloat($(this).data('price')); if (!isNaN(price)) { total += price; count++; } }); $('#selected-count').text(count); $('#selected-total').text(fmtRM(total)); var totalCbs = $('.item-checkbox').length; var checkedCbs = $('.item-checkbox:checked').length; var allChecked = totalCbs > 0 && checkedCbs === totalCbs; $('#select-all-items, #footer-select-all').prop('checked', allChecked); writeSelection(getSelectedKeys()); } // Select all $('#select-all-items, #footer-select-all').off('change.sc').on('change.sc', function(){ var checked = $(this).prop('checked'); $('.item-checkbox').prop('checked', checked); updateSelectedSummary(); }); // 单个勾选项变化时,更新统计与“全选”状态 $(document).on('change', '.item-checkbox', updateSelectedSummary); // Snapshot cart DOM -> localStorage (guest resilience) function snapshotCartFromDOM() { var items = []; $('tr.cart_item').each(function(){ var $row = $(this); var pid = parseInt($row.attr('data-product_id'),10)||0; var vid = parseInt($row.attr('data-variation_id'),10)||0; var qty = parseFloat($row.find('input.qty').val())||0; var variation = {}; try { variation = JSON.parse($row.attr('data-variation')||'{}'); } catch(e){ variation = {}; } if (pid && qty > 0) { items.push({ product_id: pid, variation_id: vid, variation: variation, quantity: qty }); } }); try { localStorage.setItem(LS_MASTER, JSON.stringify({ ts: Date.now(), items: items })); } catch(e){} } function maybeRehydrateFromLocal() { if (!PC.cart_is_empty) return; var raw = localStorage.getItem(LS_MASTER); if (!raw) return; var data; try { data = JSON.parse(raw); } catch(e){ return; } var items = (data && data.items) ? data.items : []; if (!items.length) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'pc_rehydrate_cart', security: PC.security, items: JSON.stringify(items) } }).done(function(res){ if (res && res.success) { location.reload(); // display rehydrated items } }); } // AFTER $('#remove-selected-items').off('click.sc').on('click.sc', function(){ var keys = getSelectedKeys(); if (!keys.length) { alert('请选择要删除的商品'); return; } if (!confirm('确定要删除选中的商品吗?')) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'remove_selected_cart_items', selected_items: keys, security: PC.security } }).done(function(res){ if (res && res.success) { keys.forEach(function(k){ var $row = $('tr.cart_item[data-cart_item_key="'+k+'"]'); $row.fadeOut(250, function(){ $(this).remove(); snapshotCartFromDOM(); updateSelectedSummary(); }); }); // 从本地选择集合中剔除已删除项 var saved = readSelection().filter(function(k0){ return keys.indexOf(k0) === -1; }); writeSelection(saved); } else { alert((res && res.data && (res.data.message || res.data)) || '删除失败,请重试'); } }).fail(function(){ alert('删除失败,请重试'); }); }); // Clear cart // AFTER $('#clear-cart').off('click.sc').on('click.sc', function(){ if (!confirm('确定要清空购物车吗?')) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'empty_cart', security: PC.security } }).done(function(res){ if (res && res.success) { writeSelection([]); localStorage.removeItem(LS_MASTER); location.reload(); } else { alert((res && res.data && (res.data.message || res.data)) || '清空购物车失败,请重试'); } }).fail(function(){ alert('清空购物车失败,请重试'); }); }); // Apply coupon // AFTER $('#apply-coupon').off('click.sc').on('click.sc', function(){ var code = ($.trim($('#coupon_code').val()) || ''); if (!code) { alert('请输入优惠券代码'); return; } var $btn = $(this).prop('disabled', true).text('处理中...'); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'apply_coupon', coupon_code: code, security: PC.security } }).done(function(res){ if (res && res.success) { location.reload(); } else { alert((res && res.data && (res.data.message || res.data)) || '优惠券应用失败,请重试'); $btn.prop('disabled', false).text('应用优惠券'); } }).fail(function(){ alert('发生错误,请重试'); $btn.prop('disabled', false).text('应用优惠券'); }); }); // ---- Quantity auto-save (fixed regex + loader above input) ---- // 已修正:正确的正则表达式 function parseCartKeyFromInputName(n) { var m = (n || '').match(/^cart\[([a-zA-Z0-9_]+)\]\[qty\]$/); return m ? m[1] : null; } function ensureLoader($quantityContainer){ var $overlay = $quantityContainer.find('.quantity-saving-overlay'); if (!$overlay.length) { $overlay = $('<div class="quantity-saving-overlay"><div class="quantity-saving-loader"></div></div>'); $quantityContainer.append($overlay); } return $overlay; } var qtyTimers = {}; $(document).on('input change', 'input.qty', function(){ var $input = $(this); var key = parseCartKeyFromInputName($input.attr('name')); if (!key) return; var $row = $input.closest('tr.cart_item'); var $quantityContainer = $input.closest('.quantity'); var val = parseInt($input.val(), 10); if (isNaN(val) || val < 0) val = 0; // debounce if (qtyTimers[key]) clearTimeout(qtyTimers[key]); var $overlay = ensureLoader($quantityContainer); $overlay.hide(); qtyTimers[key] = setTimeout(function(){ $overlay.show(); $input.prop('disabled', true); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'update_cart_item_qty', cart_item_key: key, qty: val, security: PC.security } }).done(function(res){ if (res && res.success && res.data){ if (res.data.subtotal_html) { $row.find('td.product-subtotal').html(res.data.subtotal_html); } var $cb = $row.find('.item-checkbox'); if ($cb.length && typeof res.data.line_total_incl_tax === 'number') { $cb.attr('data-price', res.data.line_total_incl_tax); } if (val === 0 || res.data.removed) { $row.fadeOut(300, function(){ $(this).remove(); snapshotCartFromDOM(); updateSelectedSummary(); }); } else { snapshotCartFromDOM(); updateSelectedSummary(); } } else { var msg = (res && res.data && (res.data.message || res.data)) || '数量更新失败'; alert(msg); } }).fail(function(){ alert('数量更新失败,请重试'); }).always(function(){ $overlay.hide(); $input.prop('disabled', false); }); }, 500); // 0.5s debounce }); // Partial checkout -> regular checkout page $('#partial-checkout').off('click.sc').on('click.sc', function(e){ e.preventDefault(); var keys = getSelectedKeys(); if (!keys.length) { alert('请至少选择一件商品结算'); return; } var $btn = $(this), t = $btn.text(); $btn.prop('disabled', true).text('创建订单中...'); snapshotCartFromDOM(); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'create_direct_order', selected_items: keys, security: PC.security } }).done(function(res){ if (res && res.success && res.data && res.data.checkout_url) { window.location.href = res.data.checkout_url; // /checkout/?pc_token=... } else { alert((res && res.data && (res.data.message || res.data)) || '创建订单失败,请重试'); $btn.prop('disabled', false).text(t); } }).fail(function(xhr){ var msg = '创建订单失败'; if (xhr && xhr.responseJSON && xhr.responseJSON.data) { msg += ':' + (xhr.responseJSON.data.message || xhr.responseJSON.data); } alert(msg); $btn.prop('disabled', false).text(t); }); }); // Keep LS selection after clicking "x" remove; also snapshot $(document).on('click', 'a.remove', function(){ var key = $(this).closest('tr.cart_item').attr('data-cart_item_key'); if (key) { var saved = readSelection().filter(function(k){ return k !== key; }); writeSelection(saved); snapshotCartFromDOM(); } }); // Init restoreSelectionFromLS(); updateSelectedSummary(); snapshotCartFromDOM(); maybeRehydrateFromLocal(); }); // NEW: Real-time cart count updater function updateHeaderCartCount() { $.ajax({ url: PC.ajax_url, method: 'POST', data: { action: 'get_cart_count', security: PC.security }, success: function(response) { if (response.success) { $('.cart-count').text(response.count); } } }); } // Update on cart changes $(document).on('cart_updated', function() { updateHeaderCartCount(); }); // Initial update updateHeaderCartCount(); }); </script> functions.php <?php defined('ABSPATH') || exit; /** * Robust partial checkout to regular /checkout/ with durable cart snapshot * - Snapshot full cart before virtualizing selection for checkout * - Do not remove anything until order is created * - On success (thank-you), rebuild cart as (snapshot - purchased) * - On cancel/back (visit cart), restore snapshot * - Guest resilience: localStorage + rehydrate AJAX */ /* ------------------------------------------------- * Helpers * ------------------------------------------------- */ /** * 新增:购物车数量AJAX端点 */ add_action('wp_ajax_get_cart_count', 'pc_get_cart_count'); add_action('wp_ajax_nopriv_get_cart_count', 'pc_get_cart_count'); function pc_get_cart_count() { check_ajax_referer('woocommerce-cart', 'security'); $count = WC()->cart->get_cart_contents_count(); wp_send_json_success(array('count' => $count)); } function pc_get_cart_uid() { if (is_user_logged_in()) { return 'user_' . get_current_user_id(); } if (empty($_COOKIE['pc_cart_uid'])) { $token = wp_generate_uuid4(); setcookie('pc_cart_uid', $token, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', '', is_ssl(), false); $_COOKIE['pc_cart_uid'] = $token; } return 'guest_' . sanitize_text_field(wp_unslash($_COOKIE['pc_cart_uid'])); } function pc_build_item_key($product_id, $variation_id = 0) { return (int)$product_id . '|' . (int)$variation_id; } function pc_snapshot_current_cart() { if (!WC()->cart) wc_load_cart(); $items = array(); foreach (WC()->cart->get_cart() as $ci_key => $ci) { $pid = isset($ci['product_id']) ? (int)$ci['product_id'] : 0; $vid = isset($ci['variation_id']) ? (int)$ci['variation_id'] : 0; $qty = isset($ci['quantity']) ? wc_stock_amount($ci['quantity']) : 0; $var = isset($ci['variation']) && is_array($ci['variation']) ? $ci['variation'] : array(); if ($pid && $qty > 0) { $items[] = array( 'product_id' => $pid, 'variation_id' => $vid, 'variation' => array_map('wc_clean', $var), 'quantity' => $qty, ); } } return $items; } function pc_restore_cart_from_items($items) { if (!WC()->cart) wc_load_cart(); WC()->cart->empty_cart(); foreach ((array)$items as $it) { $pid = isset($it['product_id']) ? (int)$it['product_id'] : 0; $vid = isset($it['variation_id']) ? (int)$it['variation_id'] : 0; $qty = isset($it['quantity']) ? wc_stock_amount($it['quantity']) : 0; $var = isset($it['variation']) && is_array($it['variation']) ? array_map('wc_clean', $it['variation']) : array(); if ($pid && $qty > 0) { WC()->cart->add_to_cart($pid, $qty, $vid, $var); } } WC()->cart->calculate_totals(); } // Fix cart restoration logic add_action('wp_loaded', function() { if (!method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if ($token && WC()->cart->is_empty()) { $payload = get_transient(pc_transient_key($token)); if (!empty($payload['snapshot'])) { pc_restore_cart_from_items($payload['snapshot']); WC()->cart->calculate_totals(); } } }, 20); function pc_transient_key($token) { return 'pc_partial_payload_' . sanitize_key($token); } /* ------------------------------------------------- * AJAX: Local rehydrate when Woo cart is empty * ------------------------------------------------- */ add_action('wp_ajax_pc_rehydrate_cart', 'pc_rehydrate_cart'); add_action('wp_ajax_nopriv_pc_rehydrate_cart', 'pc_rehydrate_cart'); function pc_rehydrate_cart() { check_ajax_referer('woocommerce-cart', 'security'); $raw = isset($_POST['items']) ? wp_unslash($_POST['items']) : ''; $items = is_string($raw) ? json_decode($raw, true) : (array)$raw; if (!is_array($items)) { wp_send_json_error(array('message' => 'Invalid items.'), 400); } if (!WC()->cart) wc_load_cart(); if (!WC()->cart->is_empty()) { wp_send_json_success(array('message' => 'Cart not empty.')); } foreach ($items as $it) { $pid = isset($it['product_id']) ? (int)$it['product_id'] : 0; $vid = isset($it['variation_id']) ? (int)$it['variation_id'] : 0; $qty = isset($it['quantity']) ? wc_stock_amount($it['quantity']) : 0; $var = isset($it['variation']) && is_array($it['variation']) ? array_map('wc_clean', $it['variation']) : array(); if ($pid && $qty > 0) { WC()->cart->add_to_cart($pid, $qty, $vid, $var); } } WC()->cart->calculate_totals(); wp_send_json_success(array('rehydrated' => true)); } /* ------------------------------------------------- * AJAX: Update qty (per-row; no page reload) * ------------------------------------------------- */ add_action('wp_ajax_update_cart_item_qty', 'pc_update_cart_item_qty'); add_action('wp_ajax_nopriv_update_cart_item_qty', 'pc_update_cart_item_qty'); function pc_update_cart_item_qty() { check_ajax_referer('woocommerce-cart', 'security'); $key = isset($_POST['cart_item_key']) ? wc_clean(wp_unslash($_POST['cart_item_key'])) : ''; $qty = isset($_POST['qty']) ? wc_stock_amount($_POST['qty']) : null; if (!$key || $qty === null) { wp_send_json_error(array('message' => 'Missing params.'), 400); } if (!WC()->cart) wc_load_cart(); if ($qty <= 0) { $removed = WC()->cart->remove_cart_item($key); WC()->cart->calculate_totals(); wp_send_json_success(array('removed' => (bool)$removed)); } else { $set = WC()->cart->set_quantity($key, $qty, true); WC()->cart->calculate_totals(); $cart_item = WC()->cart->get_cart_item($key); if (!$cart_item) { wp_send_json_error(array('message' => 'Cart item not found after update.'), 404); } $_product = $cart_item['data']; $subtotal_html = apply_filters( 'woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal($_product, $cart_item['quantity']), $cart_item, $key ); // Use line_total + line_tax (after totals) for checkbox data-price $line_total_incl_tax = (float)($cart_item['line_total'] + $cart_item['line_tax']); wp_send_json_success(array( 'subtotal_html' => $subtotal_html, 'line_total_incl_tax' => $line_total_incl_tax, 'removed' => false, )); } } /* ------------------------------------------------- * AJAX: Remove selected * ------------------------------------------------- */ add_action('wp_ajax_remove_selected_cart_items', 'pc_remove_selected_cart_items'); add_action('wp_ajax_nopriv_remove_selected_cart_items', 'pc_remove_selected_cart_items'); function pc_remove_selected_cart_items() { check_ajax_referer('woocommerce-cart', 'security'); $keys = isset($_POST['selected_items']) ? (array) $_POST['selected_items'] : array(); if (!WC()->cart) wc_load_cart(); foreach ($keys as $k) { $k = wc_clean(wp_unslash($k)); WC()->cart->remove_cart_item($k); } WC()->cart->calculate_totals(); wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: Empty cart * ------------------------------------------------- */ add_action('wp_ajax_empty_cart', 'pc_empty_cart'); add_action('wp_ajax_nopriv_empty_cart', 'pc_empty_cart'); function pc_empty_cart() { check_ajax_referer('woocommerce-cart', 'security'); if (!WC()->cart) wc_load_cart(); WC()->cart->empty_cart(); wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: Apply coupon * ------------------------------------------------- */ add_action('wp_ajax_apply_coupon', 'pc_apply_coupon'); add_action('wp_ajax_nopriv_apply_coupon', 'pc_apply_coupon'); function pc_apply_coupon() { check_ajax_referer('woocommerce-cart', 'security'); $code = isset($_POST['coupon_code']) ? wc_format_coupon_code(wp_unslash($_POST['coupon_code'])) : ''; if (!$code) { wp_send_json_error(array('message' => __('请输入优惠券代码', 'woocommerce')), 400); } if (!WC()->cart) wc_load_cart(); $applied = WC()->cart->apply_coupon($code); WC()->cart->calculate_totals(); if (is_wp_error($applied)) { wp_send_json_error(array('message' => $applied->get_error_message()), 400); } if (!$applied) { wp_send_json_error(array('message' => __('优惠券应用失败', 'woocommerce')), 400); } wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: Start partial checkout to regular checkout page * ------------------------------------------------- */ add_action('wp_ajax_create_direct_order', 'pc_create_direct_order'); add_action('wp_ajax_nopriv_create_direct_order', 'pc_create_direct_order'); function pc_create_direct_order() { check_ajax_referer('woocommerce-cart', 'security'); $selected_keys = isset($_POST['selected_items']) ? (array) $_POST['selected_items'] : array(); if (empty($selected_keys)) { wp_send_json_error(array('message' => __('请选择要结算的商品', 'woocommerce')), 400); } if (!WC()->cart) wc_load_cart(); // Snapshot full cart $snapshot = pc_snapshot_current_cart(); // Build selected items from current cart based on cart_item_key list $selected = array(); foreach (WC()->cart->get_cart() as $ci_key => $ci) { if (!in_array($ci_key, $selected_keys, true)) { continue; } $pid = (int)$ci['product_id']; $vid = (int)$ci['variation_id']; $qty = wc_stock_amount($ci['quantity']); $var = isset($ci['variation']) && is_array($ci['variation']) ? array_map('wc_clean', $ci['variation']) : array(); if ($pid && $qty > 0) { $selected[] = array( 'product_id' => $pid, 'variation_id' => $vid, 'variation' => $var, 'quantity' => $qty, ); } } if (empty($selected)) { wp_send_json_error(array('message' => __('没有可结算的商品', 'woocommerce')), 400); } $token = wp_generate_uuid4(); $payload = array( 'uid' => pc_get_cart_uid(), 'snapshot' => $snapshot, 'selected' => $selected, 'created' => time(), ); set_transient(pc_transient_key($token), $payload, 2 * DAY_IN_SECONDS); // Put token in session (used across checkout AJAX calls) if (method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', $token); } $checkout_url = add_query_arg('pc_token', rawurlencode($token), wc_get_checkout_url()); wp_send_json_success(array('checkout_url' => $checkout_url)); } /* ------------------------------------------------- * Virtualize cart on checkout for token and rebuild after purchase * ------------------------------------------------- */ // Entering checkout with token: virtualize cart to selected items add_action('woocommerce_before_checkout_form', function() { if (!isset($_GET['pc_token'])) return; $token = sanitize_text_field(wp_unslash($_GET['pc_token'])); $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['selected'])) return; if (!WC()->cart) wc_load_cart(); // Virtualize to selected items only pc_restore_cart_from_items($payload['selected']); // Persist token in session for all subsequent checkout AJAX calls if (method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', $token); } }, 1); // Safety: just-in-time re-virtualization before order processing add_action('woocommerce_before_checkout_process', function() { if (!method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['selected'])) return; // Ensure cart still equals selected set pc_restore_cart_from_items($payload['selected']); }, 1); // Tag order with token add_action('woocommerce_checkout_create_order', function($order) { $token = null; if (isset($_GET['pc_token'])) { $token = sanitize_text_field(wp_unslash($_GET['pc_token'])); } elseif (method_exists(WC()->session, 'get')) { $token = WC()->session->get('pc_partial_token'); } if ($token) { $order->update_meta_data('_pc_partial_token', $token); $order->update_meta_data('_pc_cart_snapshot', $token); } }, 10, 1); // Fixed: Only remove purchased items from cart add_action('woocommerce_thankyou', function($order_id) { $order = wc_get_order($order_id); if (!$order) return; $token = $order->get_meta('_pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) { if (method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', null); } delete_transient(pc_transient_key($token)); return; } // 1. Restore FULL snapshot (all items) pc_restore_cart_from_items($payload['snapshot']); WC()->cart->calculate_totals(); // 2. Remove only the purchased (selected) items foreach ($payload['selected'] as $selected_item) { $cart_item_key = pc_find_cart_item($selected_item['product_id'], $selected_item['variation_id']); if ($cart_item_key) { WC()->cart->remove_cart_item($cart_item_key); } } // 3. Update cart totals WC()->cart->calculate_totals(); // Cleanup token if (method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', null); } delete_transient(pc_transient_key($token)); }, 20); // Helper: Find cart item by product and variation ID function pc_find_cart_item($product_id, $variation_id = 0) { if (!WC()->cart) wc_load_cart(); foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) { $cart_pid = isset($cart_item['product_id']) ? (int)$cart_item['product_id'] : 0; $cart_vid = isset($cart_item['variation_id']) ? (int)$cart_item['variation_id'] : 0; if ($cart_pid == $product_id && $cart_vid == $variation_id) { return $cart_item_key; } } return false; } // Visiting cart with active token: restore full snapshot (cancel/back) add_action('woocommerce_before_cart', function() { if (!method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) return; // Restore full snapshot so cart page always shows everything pc_restore_cart_from_items($payload['snapshot']); }, 1); /* ------------------------------------------------- * Keep cart count accurate during checkout process * ------------------------------------------------- */ add_filter('woocommerce_cart_contents_count', function($count) { // Check if partial token exists if (!method_exists(WC()->session, 'get')) return $count; $token = WC()->session->get('pc_partial_token'); if ($token) { $payload = get_transient(pc_transient_key($token)); // Always show full cart count even during checkout if (!empty($payload['snapshot'])) { $snapshot_count = 0; foreach ($payload['snapshot'] as $item) { $snapshot_count += (int)$item['quantity']; } return $snapshot_count; } } return $count; }); // Ensure cart item totals are accurate in header add_action('woocommerce_before_cart', function() { pc_maintain_cart_consistency(); }); add_action('woocommerce_before_checkout_form', function() { pc_maintain_cart_consistency(); }); function pc_maintain_cart_consistency() { if (!method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) return; // Immediately restore full cart for consistent UI pc_restore_cart_from_items($payload['snapshot']); WC()->cart->calculate_totals(); }
09-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值