feat: 实现混装产品卡片式布局及批量添加功能
- 新增卡片式布局模板替换原有表格形式 - 添加后台一键批量选择所有simple product功能 - 优化前端样式兼容性及交互体验 - 增加版本号管理及资源文件版本控制 - 重构前端逻辑提升代码可维护性
This commit is contained in:
parent
a1c47d3a57
commit
26e076af1a
|
|
@ -6,3 +6,71 @@
|
||||||
.yoone-bundle-table th, .yoone-bundle-table td { border-bottom: 1px solid #eee; padding: 8px; }
|
.yoone-bundle-table th, .yoone-bundle-table td { border-bottom: 1px solid #eee; padding: 8px; }
|
||||||
.yoone-bundle-meta { display: flex; gap: 1em; align-items: center; }
|
.yoone-bundle-meta { display: flex; gap: 1em; align-items: center; }
|
||||||
.yoone-bundle-actions { margin-top: 1em; }
|
.yoone-bundle-actions { margin-top: 1em; }
|
||||||
|
|
||||||
|
/* 在混装产品页面隐藏默认产品图片区域(兼容常见主题结构) */
|
||||||
|
.product.type-yoone_bundle div.images,
|
||||||
|
.single-product .product.type-yoone_bundle .images {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/* 兼容通过 body_class 添加的标记 */
|
||||||
|
.yoone-bundle-product div.images { display: none !important; }
|
||||||
|
|
||||||
|
/* 表格列宽优化 */
|
||||||
|
.yoone-bundle-table th:nth-child(3),
|
||||||
|
.yoone-bundle-table td:nth-child(3) { width: 160px; }
|
||||||
|
|
||||||
|
/* 强制混装产品页面的内容区域为全宽,以容纳卡片布局 */
|
||||||
|
.yoone-bundle-product-page .summary {
|
||||||
|
width: 100% !important; /* 覆盖主题默认的宽度限制 */
|
||||||
|
float: none !important; /* 清除浮动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-add-to-cart-button {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新的卡片式布局样式 */
|
||||||
|
.yoone-bundle-items-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px; /* 卡片之间的间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center; /* 垂直居中对齐 */
|
||||||
|
width: 150px; /* 卡片宽度,可根据需要调整 */
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card .item-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card .item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card .item-price {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #c00;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-item-card .item-quantity input {
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,52 @@
|
||||||
(function($){
|
(function($) {
|
||||||
// 预留:后台增强脚本(例如联动过滤 simple products、校验最小数量等)。
|
$(document).ready(function() {
|
||||||
|
// “一键添加所有 Simple Product” 按钮点击事件
|
||||||
|
$(document).on('click', '.yoone-add-all-simple-products', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $button = $(this);
|
||||||
|
var $select = $('select.wc-product-search[name="yoone_bundle_allowed_products[]"]');
|
||||||
|
var nonce = $('#yoone_bundle_admin_nonce_field').val();
|
||||||
|
|
||||||
|
$button.prop('disabled', true).text('正在加载...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_get_all_simple_products',
|
||||||
|
security: nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var existing_ids = $select.val() || [];
|
||||||
|
var products = response.data;
|
||||||
|
|
||||||
|
products.forEach(function(product) {
|
||||||
|
// 如果下拉列表中不存在该选项,则添加
|
||||||
|
if ($select.find('option[value="' + product.id + '"]').length === 0) {
|
||||||
|
var newOption = new Option(product.text, product.id, false, false);
|
||||||
|
$select.append(newOption);
|
||||||
|
}
|
||||||
|
// 将新获取的商品ID加入到已选中的ID数组中
|
||||||
|
if (existing_ids.indexOf(product.id.toString()) === -1) {
|
||||||
|
existing_ids.push(product.id.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 select2 的选中状态
|
||||||
|
$select.val(existing_ids).trigger('change');
|
||||||
|
$button.prop('disabled', false).text('一键添加所有 Simple Product');
|
||||||
|
} else {
|
||||||
|
alert('加载失败,请重试。');
|
||||||
|
$button.prop('disabled', false).text('一键添加所有 Simple Product');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('请求失败,请检查网络连接或联系管理员。');
|
||||||
|
$button.prop('disabled', false).text('一键添加所有 Simple Product');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
- 混装产品可批量添加simple product为混装可选商品
|
||||||
|
|
@ -19,6 +19,11 @@ class Yoone_Product_Bundles_Admin {
|
||||||
add_action('woocommerce_product_data_panels', array($this, 'render_product_data_panel'));
|
add_action('woocommerce_product_data_panels', array($this, 'render_product_data_panel'));
|
||||||
// 保存配置
|
// 保存配置
|
||||||
add_action('woocommerce_admin_process_product_meta', array($this, 'save_product_meta'));
|
add_action('woocommerce_admin_process_product_meta', array($this, 'save_product_meta'));
|
||||||
|
add_action('woocommerce_process_product_meta', array($this, 'save_product_meta'));
|
||||||
|
add_action('save_post_product', array($this, 'save_product_meta'), 15);
|
||||||
|
|
||||||
|
// AJAX: 获取所有 simple product
|
||||||
|
add_action('wp_ajax_yoone_get_all_simple_products', array($this, 'ajax_get_all_simple_products'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add_product_data_tab($tabs) {
|
public function add_product_data_tab($tabs) {
|
||||||
|
|
@ -39,13 +44,17 @@ class Yoone_Product_Bundles_Admin {
|
||||||
$min_qty = $config['min_qty'];
|
$min_qty = $config['min_qty'];
|
||||||
$cats = $config['categories'];
|
$cats = $config['categories'];
|
||||||
|
|
||||||
echo '<div id="yoone_bundle_data" class="panel woocommerce_options_panel hidden">';
|
echo '<div id="yoone_bundle_data" class="panel woocommerce_options_panel show_if_yoone_bundle">';
|
||||||
|
wp_nonce_field('yoone-bundle-admin-nonce', 'yoone_bundle_admin_nonce_field');
|
||||||
|
|
||||||
echo '<div class="options_group">';
|
echo '<div class="options_group">';
|
||||||
echo '<p>' . esc_html__('配置可混装的简单商品、最小混装数量以及前端展示的分类分组。', 'yoone-product-bundles') . '</p>';
|
echo '<p>' . esc_html__('配置可混装的简单商品、最小混装数量以及前端展示的分类分组。', 'yoone-product-bundles') . '</p>';
|
||||||
|
|
||||||
// 可混装商品:使用 Woo 的产品搜索(select2),multiple
|
// 可混装商品:使用 Woo 的产品搜索(select2),multiple
|
||||||
echo '<p class="form-field"><label>' . esc_html__('可混装商品', 'yoone-product-bundles') . '</label>';
|
echo '<p class="form-field"><label>' . esc_html__('可混装商品', 'yoone-product-bundles') . '</label>';
|
||||||
echo '<select class="wc-product-search" multiple style="width: 90%;" name="yoone_bundle_allowed_products[]" data-placeholder="' . esc_attr__('选择可混装的简单商品…', 'yoone-product-bundles') . '" data-action="woocommerce_json_search_products_and_variations">';
|
echo '<button type="button" class="button yoone-add-all-simple-products" style="margin-left: 10px;">' . esc_html__('一键添加所有 Simple Product', 'yoone-product-bundles') . '</button>';
|
||||||
|
// 仅搜索产品,不含变体,避免误选导致前端不显示
|
||||||
|
echo '<select class="wc-product-search" multiple style="width: 90%;" name="yoone_bundle_allowed_products[]" data-placeholder="' . esc_attr__('选择可混装的简单商品…', 'yoone-product-bundles') . '" data-action="woocommerce_json_search_products">';
|
||||||
if (! empty($allowed)) {
|
if (! empty($allowed)) {
|
||||||
foreach ($allowed as $pid) {
|
foreach ($allowed as $pid) {
|
||||||
$p = wc_get_product($pid);
|
$p = wc_get_product($pid);
|
||||||
|
|
@ -86,12 +95,25 @@ class Yoone_Product_Bundles_Admin {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save_product_meta($post_id) {
|
public function save_product_meta($post_id) {
|
||||||
$product = wc_get_product($post_id);
|
// 无论当前 product 对象类型为何,只要提交了我们的字段,就进行保存。
|
||||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return;
|
// 这可以避免在首次切换产品类型时由于保存顺序问题导致配置未写入。
|
||||||
|
$has_fields = isset($_POST['yoone_bundle_allowed_products']) || isset($_POST['yoone_bundle_min_quantity']) || isset($_POST['yoone_bundle_categories']);
|
||||||
|
if (! $has_fields) return;
|
||||||
|
|
||||||
// 保存 allowed products
|
// 保存 allowed products
|
||||||
$allowed = isset($_POST['yoone_bundle_allowed_products']) ? (array) $_POST['yoone_bundle_allowed_products'] : array();
|
$allowed = isset($_POST['yoone_bundle_allowed_products']) ? (array) $_POST['yoone_bundle_allowed_products'] : array();
|
||||||
$allowed = array_values(array_filter(array_map('absint', $allowed)));
|
$allowed = array_values(array_filter(array_map('absint', $allowed)));
|
||||||
|
// 仅保留 simple 产品ID
|
||||||
|
if (! empty($allowed)) {
|
||||||
|
$simple_only = array();
|
||||||
|
foreach ($allowed as $aid) {
|
||||||
|
$p = wc_get_product($aid);
|
||||||
|
if ($p && $p->is_type('simple')) {
|
||||||
|
$simple_only[] = $aid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$allowed = $simple_only;
|
||||||
|
}
|
||||||
update_post_meta($post_id, Yoone_Product_Bundles::META_ALLOWED_PRODUCTS, $allowed);
|
update_post_meta($post_id, Yoone_Product_Bundles::META_ALLOWED_PRODUCTS, $allowed);
|
||||||
|
|
||||||
// 保存 min qty
|
// 保存 min qty
|
||||||
|
|
@ -103,4 +125,30 @@ class Yoone_Product_Bundles_Admin {
|
||||||
$cats = array_values(array_filter(array_map('absint', $cats)));
|
$cats = array_values(array_filter(array_map('absint', $cats)));
|
||||||
update_post_meta($post_id, Yoone_Product_Bundles::META_CATEGORIES, $cats);
|
update_post_meta($post_id, Yoone_Product_Bundles::META_CATEGORIES, $cats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler: 获取所有已发布的 simple product
|
||||||
|
*/
|
||||||
|
public function ajax_get_all_simple_products() {
|
||||||
|
if (! current_user_can('edit_products')) {
|
||||||
|
wp_send_json_error('permission_denied', 403);
|
||||||
|
}
|
||||||
|
check_ajax_referer('yoone-bundle-admin-nonce', 'security');
|
||||||
|
|
||||||
|
$products = wc_get_products(array(
|
||||||
|
'type' => 'simple',
|
||||||
|
'status' => 'publish',
|
||||||
|
'limit' => -1,
|
||||||
|
));
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$results[] = array(
|
||||||
|
'id' => $product->get_id(),
|
||||||
|
'text' => $product->get_formatted_name(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($results);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +58,18 @@ class Yoone_Product_Bundles {
|
||||||
$allowed = get_post_meta($pid, self::META_ALLOWED_PRODUCTS, true);
|
$allowed = get_post_meta($pid, self::META_ALLOWED_PRODUCTS, true);
|
||||||
$min = absint(get_post_meta($pid, self::META_MIN_QTY, true));
|
$min = absint(get_post_meta($pid, self::META_MIN_QTY, true));
|
||||||
$cats = get_post_meta($pid, self::META_CATEGORIES, true);
|
$cats = get_post_meta($pid, self::META_CATEGORIES, true);
|
||||||
|
// 仅保留 simple 产品(避免后台选择了变体或其他类型导致前端不显示)
|
||||||
$allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array();
|
$allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array();
|
||||||
|
if (! empty($allowed)) {
|
||||||
|
$simple_only = array();
|
||||||
|
foreach ($allowed as $aid) {
|
||||||
|
$p = wc_get_product($aid);
|
||||||
|
if ($p && $p->is_type('simple')) {
|
||||||
|
$simple_only[] = $aid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$allowed = $simple_only;
|
||||||
|
}
|
||||||
$cats = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
$cats = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
||||||
return array(
|
return array(
|
||||||
'allowed_products' => $allowed,
|
'allowed_products' => $allowed,
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,19 @@
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
class Yoone_Product_Bundles_Frontend {
|
class Yoone_Product_Bundles_Frontend {
|
||||||
protected static $instance = null;
|
|
||||||
|
private static $_instance = null;
|
||||||
|
|
||||||
public static function instance() {
|
public static function instance() {
|
||||||
if (null === self::$instance) self::$instance = new self();
|
if (is_null(self::$_instance)) {
|
||||||
return self::$instance;
|
self::$_instance = new self();
|
||||||
|
}
|
||||||
|
return self::$_instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function __construct() {
|
private function __construct() {
|
||||||
// 用我们的模板替换默认的 add-to-cart 按钮
|
// 加载自定义的混装产品页面模板
|
||||||
add_action('woocommerce_' . Yoone_Product_Bundles::TYPE . '_add_to_cart', array($this, 'render_add_to_cart')); // 显示表单
|
add_filter('template_include', array($this, 'use_bundle_template'), 99);
|
||||||
|
|
||||||
// 处理加入购物车:校验数量并打包所选组件数据
|
// 处理加入购物车:校验数量并打包所选组件数据
|
||||||
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 6);
|
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 6);
|
||||||
|
|
@ -25,109 +28,152 @@ class Yoone_Product_Bundles_Frontend {
|
||||||
|
|
||||||
// 结算前动态调整价格
|
// 结算前动态调整价格
|
||||||
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_bundle_price'), 20, 1);
|
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_bundle_price'), 20, 1);
|
||||||
|
|
||||||
|
// 为混装产品页增加 body class,便于主题兼容样式控制
|
||||||
|
add_filter('body_class', array($this, 'filter_body_class'));
|
||||||
|
|
||||||
|
// 从 woocommerce_single_product_summary 钩子中移除默认的 add-to-cart
|
||||||
|
add_action('init', array($this, 'remove_default_add_to_cart'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染自定义表单:按所选分类分组显示允许的简单商品,提供数量输入。
|
* 移除默认的 add-to-cart 按钮,因为我们的模板会自己处理
|
||||||
*/
|
*/
|
||||||
public function render_add_to_cart() {
|
public function remove_default_add_to_cart() {
|
||||||
wc_get_template('single-product/add-to-cart/yoone-bundle.php', array(), '', YOONE_PB_PATH . 'templates/');
|
remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验:总选择数量 >= min_qty
|
* 如果是混装产品页面,则加载我们的自定义模板。
|
||||||
*/
|
*/
|
||||||
public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id, $variations, $cart_item_data) {
|
public function use_bundle_template($template) {
|
||||||
|
if (is_singular('product')) {
|
||||||
|
global $post;
|
||||||
|
$product = wc_get_product($post->ID);
|
||||||
|
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
|
||||||
|
// 加载插件内的完整页面模板
|
||||||
|
$new_template = YOONE_PB_PATH . 'templates/single-product-yoone-bundle.php';
|
||||||
|
if (file_exists($new_template)) {
|
||||||
|
// 加载前端资源
|
||||||
|
if (wp_style_is('yoone-pb-frontend', 'registered')) wp_enqueue_style('yoone-pb-frontend');
|
||||||
|
if (wp_script_is('yoone-pb-frontend', 'registered')) wp_enqueue_script('yoone-pb-frontend');
|
||||||
|
return $new_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在混装产品页增加 body class。
|
||||||
|
*/
|
||||||
|
public function filter_body_class($classes) {
|
||||||
|
global $post;
|
||||||
|
if (is_singular('product') && $post) {
|
||||||
|
$product = wc_get_product($post->ID);
|
||||||
|
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
|
||||||
|
$classes[] = 'yoone-bundle-product';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验混装产品加入购物车:至少选择一个组件。
|
||||||
|
*/
|
||||||
|
public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id = null, $variations = null, $cart_item_data = null) {
|
||||||
$product = wc_get_product($product_id);
|
$product = wc_get_product($product_id);
|
||||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return $passed;
|
if ($product->get_type() !== Yoone_Product_Bundles::TYPE) {
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||||
$min = max(0, absint($config['min_qty']));
|
$min_qty = max(1, absint($config['min_qty']));
|
||||||
|
|
||||||
// 从 POST 中收集数量
|
$components = !empty($_POST['yoone_bundle_components']) ? $_POST['yoone_bundle_components'] : array();
|
||||||
$selected = isset($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array();
|
|
||||||
$total_qty = 0;
|
$total_qty = 0;
|
||||||
foreach ($selected as $pid => $qty) {
|
foreach ($components as $comp_id => $qty) {
|
||||||
$qty = absint($qty);
|
$qty = absint($qty);
|
||||||
if ($qty > 0) $total_qty += $qty;
|
if ($qty > 0) {
|
||||||
|
$total_qty += $qty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($total_qty < $min) {
|
if ($total_qty < $min_qty) {
|
||||||
wc_add_notice(sprintf(__('请至少选择 %d 件混装组件后再加入购物车。', 'yoone-product-bundles'), $min), 'error');
|
wc_add_notice(sprintf(__('您需要至少选择 %d 件商品才能将此混装包加入购物车。', 'yoone-product-bundles'), $min_qty), 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $passed;
|
return $passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将所选组件数据存入购物车项。
|
* 将混装组件数据添加到购物车项目。
|
||||||
*/
|
*/
|
||||||
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
||||||
$product = wc_get_product($product_id);
|
$product = wc_get_product($product_id);
|
||||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return $cart_item_data;
|
if ($product->get_type() !== Yoone_Product_Bundles::TYPE) {
|
||||||
|
return $cart_item_data;
|
||||||
$selected = isset($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array();
|
|
||||||
// 仅保留大于 0 的数量
|
|
||||||
$components = array();
|
|
||||||
foreach ($selected as $pid => $qty) {
|
|
||||||
$pid = absint($pid);
|
|
||||||
$qty = absint($qty);
|
|
||||||
if ($pid && $qty > 0) $components[$pid] = $qty;
|
|
||||||
}
|
}
|
||||||
if (! empty($components)) {
|
|
||||||
|
if (isset($_POST['yoone_bundle_components'])) {
|
||||||
|
$components = array();
|
||||||
|
foreach ($_POST['yoone_bundle_components'] as $comp_id => $qty) {
|
||||||
|
$qty = absint($qty);
|
||||||
|
if ($qty > 0) {
|
||||||
|
$components[absint($comp_id)] = $qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($components)) {
|
||||||
$cart_item_data['yoone_bundle_components'] = $components;
|
$cart_item_data['yoone_bundle_components'] = $components;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $cart_item_data;
|
return $cart_item_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在购物车行项目中展示所选组件摘要。
|
* 在购物车和订单中显示混装组件的摘要。
|
||||||
*/
|
*/
|
||||||
public function display_cart_item_data($item_data, $cart_item) {
|
public function display_cart_item_data($item_data, $cart_item) {
|
||||||
if (! empty($cart_item['yoone_bundle_components'])) {
|
if (empty($cart_item['yoone_bundle_components'])) {
|
||||||
$lines = array();
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = "";
|
||||||
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
||||||
$p = wc_get_product($pid);
|
$product = wc_get_product($pid);
|
||||||
if ($p) {
|
if ($product) {
|
||||||
$lines[] = sprintf('%s × %d', $p->get_name(), $qty);
|
$value .= $product->get_name() . ' × ' . $qty . "; ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (! empty($lines)) {
|
|
||||||
$item_data[] = array(
|
$item_data[] = array(
|
||||||
'key' => __('混装内容', 'yoone-product-bundles'),
|
'key' => __('混装明细', 'yoone-product-bundles'),
|
||||||
'value' => implode("\n", $lines),
|
'value' => rtrim($value, "; "),
|
||||||
'display' => implode('<br/>', array_map('esc_html', $lines)),
|
'display' => ''
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
return $item_data;
|
return $item_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结算前根据组件动态调整混装产品的行项目价格(= 所选简单商品单价 × 数量 的总和)。
|
* 在将商品添加到购物车之前,根据所选组件动态计算混装产品的总价。
|
||||||
* 注意:这里使用的是产品当前价格(不含税的基础价),是否包含税由 WooCommerce 税设置决定。
|
|
||||||
*/
|
*/
|
||||||
public function adjust_bundle_price($cart) {
|
public function adjust_bundle_price($cart) {
|
||||||
if (is_admin() && ! defined('DOING_AJAX')) {
|
if (is_admin() && !defined('DOING_AJAX')) return;
|
||||||
return; // 后台不处理
|
|
||||||
}
|
|
||||||
if (empty($cart) || ! method_exists($cart, 'get_cart')) return;
|
|
||||||
|
|
||||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||||
if (empty($cart_item['yoone_bundle_components'])) continue;
|
if (isset($cart_item['yoone_bundle_components'])) {
|
||||||
$product = isset($cart_item['data']) ? $cart_item['data'] : null;
|
$total_price = 0;
|
||||||
if (! $product || ! is_a($product, 'WC_Product')) continue;
|
|
||||||
if ($product->get_type() !== Yoone_Product_Bundles::TYPE) continue;
|
|
||||||
|
|
||||||
$total = 0.0;
|
|
||||||
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
||||||
$p = wc_get_product($pid);
|
$product = wc_get_product($pid);
|
||||||
if (! $p) continue;
|
if ($product) {
|
||||||
$unit = floatval($p->get_price()); // 基础价
|
$total_price += (float) $product->get_price() * $qty;
|
||||||
$total += $unit * absint($qty);
|
}
|
||||||
|
}
|
||||||
|
$cart_item['data']->set_price($total_price);
|
||||||
}
|
}
|
||||||
// 将产品行项目价格设为总价
|
|
||||||
$product->set_price($total);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Yoone Product Bundles - 混装产品单页模板
|
||||||
|
*
|
||||||
|
* 此模板将完全取代 WooCommerce 的默认 single-product.php,提供一个包含卡片式选项的自定义布局。
|
||||||
|
*
|
||||||
|
* @version 1.0.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// 确保这是一个混装产品
|
||||||
|
if (!$product || $product->get_type() !== Yoone_Product_Bundles::TYPE) {
|
||||||
|
// 如果不是,则回退到标准模板
|
||||||
|
wc_get_template_part('content', 'single-product');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 数据准备逻辑 ---
|
||||||
|
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||||
|
$allowed = $config['allowed_products'];
|
||||||
|
$min_qty = max(0, absint($config['min_qty']));
|
||||||
|
$cat_ids = $config['categories'];
|
||||||
|
|
||||||
|
$allowed_products = array();
|
||||||
|
foreach ($allowed as $pid) {
|
||||||
|
$p = wc_get_product($pid);
|
||||||
|
if ($p && $p->is_type('simple')) {
|
||||||
|
$allowed_products[$pid] = $p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = array();
|
||||||
|
if (!empty($cat_ids)) {
|
||||||
|
$others = array();
|
||||||
|
foreach ($allowed_products as $pid => $p) {
|
||||||
|
$terms = get_the_terms($pid, 'product_cat');
|
||||||
|
$matched = false;
|
||||||
|
if (is_array($terms)) {
|
||||||
|
foreach ($terms as $t) {
|
||||||
|
if (in_array($t->term_id, $cat_ids, true)) {
|
||||||
|
if (!isset($groups[$t->term_id])) {
|
||||||
|
$groups[$t->term_id] = array('term' => $t, 'items' => array());
|
||||||
|
}
|
||||||
|
$groups[$t->term_id]['items'][] = $p;
|
||||||
|
$matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$matched) $others[] = $p;
|
||||||
|
}
|
||||||
|
if (!empty($others)) $groups[0] = array('term' => null, 'items' => $others);
|
||||||
|
} else {
|
||||||
|
$groups[0] = array('term' => null, 'items' => array_values($allowed_products));
|
||||||
|
}
|
||||||
|
// --- 数据准备逻辑结束 ---
|
||||||
|
|
||||||
|
get_header('shop');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php do_action('woocommerce_before_main_content'); ?>
|
||||||
|
|
||||||
|
<div id="product-<?php the_ID(); ?>" <?php wc_product_class('yoone-bundle-product-page', $product); ?>>
|
||||||
|
|
||||||
|
<div class="summary entry-summary">
|
||||||
|
<?php
|
||||||
|
// 显示标题、价格等
|
||||||
|
do_action('woocommerce_single_product_summary');
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 在标准摘要信息后,插入我们的混装表单
|
||||||
|
// 我们不再使用 woocommerce_template_single_add_to_cart, 而是直接渲染
|
||||||
|
?>
|
||||||
|
<div class="yoone-bundle-form-container">
|
||||||
|
<form class="cart yoone-bundle-form" action="<?php echo esc_url(apply_filters('woocommerce_add_to_cart_form_action', $product->get_permalink())); ?>" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="yoone-bundle-meta">
|
||||||
|
<p class="yoone-bundle-min"><?php printf(esc_html__('至少选择 %d 件混装组件', 'yoone-product-bundles'), $min_qty); ?></p>
|
||||||
|
<p class="yoone-bundle-selected"><?php esc_html_e('当前选择数量:', 'yoone-product-bundles'); ?><span class="yoone-bundle-selected-count">0</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($allowed_products)) : ?>
|
||||||
|
<p><?php esc_html_e('暂无可混装商品,请在后台为该混装产品配置。', 'yoone-product-bundles'); ?></p>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php foreach ($groups as $gid => $group) : ?>
|
||||||
|
<div class="yoone-bundle-group">
|
||||||
|
<?php if (!empty($group['term'])) : ?>
|
||||||
|
<h3 class="yoone-bundle-group-title"><?php echo esc_html($group['term']->name); ?></h3>
|
||||||
|
<?php else : ?>
|
||||||
|
<h3 class="yoone-bundle-group-title"><?php esc_html_e('可混装商品', 'yoone-product-bundles'); ?></h3>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (empty($group['items'])) : ?>
|
||||||
|
<p class="yoone-bundle-no-items"><?php esc_html_e('该分组暂无可混装商品', 'yoone-product-bundles'); ?></p>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="yoone-bundle-items-wrapper">
|
||||||
|
<?php foreach ($group['items'] as $item) : ?>
|
||||||
|
<?php
|
||||||
|
$pid = $item->get_id();
|
||||||
|
$thumbnail = $item->get_image('woocommerce_thumbnail');
|
||||||
|
?>
|
||||||
|
<div class="yoone-bundle-item-card">
|
||||||
|
<div class="item-image">
|
||||||
|
<a href="<?php echo esc_url(get_permalink($pid)); ?>" target="_blank">
|
||||||
|
<?php echo $thumbnail ? $thumbnail : wc_placeholder_img('woocommerce_thumbnail'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h4 class="item-title">
|
||||||
|
<a href="<?php echo esc_url(get_permalink($pid)); ?>" target="_blank"><?php echo esc_html($item->get_name()); ?></a>
|
||||||
|
</h4>
|
||||||
|
<div class="item-price">
|
||||||
|
<?php echo wp_kses_post($item->get_price_html()); ?>
|
||||||
|
</div>
|
||||||
|
<div class="item-quantity">
|
||||||
|
<input type="number" min="0" step="1" class="yoone-bundle-qty" name="yoone_bundle_components[<?php echo esc_attr($pid); ?>]" value="0" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-actions">
|
||||||
|
<input type="hidden" name="add-to-cart" value="<?php echo esc_attr($product->get_id()); ?>" />
|
||||||
|
<button type="submit" class="single_add_to_cart_button button alt" disabled><?php esc_html_e('加入购物车', 'yoone-product-bundles'); ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 显示产品描述、评论等 Tab
|
||||||
|
do_action('woocommerce_after_single_product_summary');
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php do_action('woocommerce_after_single_product'); ?>
|
||||||
|
|
||||||
|
<?php get_footer('shop'); ?>
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* 模板:单个混装产品的 add-to-cart 表单
|
|
||||||
* 路径:templates/single-product/add-to-cart/yoone-bundle.php
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
global $product;
|
|
||||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
|
||||||
$allowed = $config['allowed_products'];
|
|
||||||
$min_qty = max(0, absint($config['min_qty']));
|
|
||||||
$cat_ids = $config['categories'];
|
|
||||||
|
|
||||||
// 收集允许的商品对象,且必须是 simple product
|
|
||||||
$allowed_products = array();
|
|
||||||
foreach ($allowed as $pid) {
|
|
||||||
$p = wc_get_product($pid);
|
|
||||||
if ($p && $p->is_type('simple')) {
|
|
||||||
$allowed_products[$pid] = $p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按分类分组(如果未配置分类,则全部在一个组内)
|
|
||||||
$groups = array();
|
|
||||||
if (! empty($cat_ids)) {
|
|
||||||
foreach ($allowed_products as $pid => $p) {
|
|
||||||
$terms = get_the_terms($pid, 'product_cat');
|
|
||||||
$matched = false;
|
|
||||||
if (is_array($terms)) {
|
|
||||||
foreach ($terms as $t) {
|
|
||||||
if (in_array($t->term_id, $cat_ids, true)) {
|
|
||||||
$groups[$t->term_id]['term'] = $t;
|
|
||||||
$groups[$t->term_id]['items'][] = $p;
|
|
||||||
$matched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 若商品不属于所选分类,则不显示
|
|
||||||
if (! $matched) {
|
|
||||||
// 跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$groups[0] = array('term' => null, 'items' => array_values($allowed_products));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表单开始
|
|
||||||
?>
|
|
||||||
<form class="cart yoone-bundle-form" action="<?php echo esc_url( apply_filters( 'woocommerce_add_to_cart_form_action', $product->get_permalink() ) ); ?>" method="post" enctype="multipart/form-data">
|
|
||||||
<div class="yoone-bundle-meta">
|
|
||||||
<p class="yoone-bundle-min"><?php printf( esc_html__('至少选择 %d 件混装组件', 'yoone-product-bundles'), $min_qty ); ?></p>
|
|
||||||
<p class="yoone-bundle-selected"><?php esc_html_e('当前选择数量:', 'yoone-product-bundles'); ?><span class="yoone-bundle-selected-count">0</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (empty($groups)) : ?>
|
|
||||||
<p><?php esc_html_e('暂无可混装商品,请在后台为该混装产品配置。', 'yoone-product-bundles'); ?></p>
|
|
||||||
<?php else : ?>
|
|
||||||
<?php foreach ($groups as $gid => $group) : ?>
|
|
||||||
<div class="yoone-bundle-group">
|
|
||||||
<?php if (! empty($group['term'])) : ?>
|
|
||||||
<h3 class="yoone-bundle-group-title"><?php echo esc_html($group['term']->name); ?></h3>
|
|
||||||
<?php else : ?>
|
|
||||||
<h3 class="yoone-bundle-group-title"><?php esc_html_e('可混装商品', 'yoone-product-bundles'); ?></h3>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<table class="yoone-bundle-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><?php esc_html_e('商品', 'yoone-product-bundles'); ?></th>
|
|
||||||
<th><?php esc_html_e('价格', 'yoone-product-bundles'); ?></th>
|
|
||||||
<th style="width:140px;"><?php esc_html_e('数量', 'yoone-product-bundles'); ?></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($group['items'] as $item) : ?>
|
|
||||||
<?php $pid = $item->get_id(); ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="<?php echo esc_url(get_permalink($pid)); ?>" target="_blank"><?php echo esc_html($item->get_name()); ?></a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php echo wp_kses_post($item->get_price_html()); ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="number" min="0" step="1" class="yoone-bundle-qty" name="yoone_bundle_components[<?php echo esc_attr($pid); ?>]" value="0" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="yoone-bundle-actions">
|
|
||||||
<input type="hidden" name="add-to-cart" value="<?php echo esc_attr($product->get_id()); ?>" />
|
|
||||||
<button type="submit" class="single_add_to_cart_button button alt" disabled><?php esc_html_e('加入购物车', 'yoone-product-bundles'); ?></button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function($){
|
|
||||||
function updateState(){
|
|
||||||
var total = 0;
|
|
||||||
$('.yoone-bundle-qty').each(function(){
|
|
||||||
var v = parseInt($(this).val(), 10);
|
|
||||||
if (!isNaN(v) && v > 0) total += v;
|
|
||||||
});
|
|
||||||
$('.yoone-bundle-selected-count').text(total);
|
|
||||||
var min = <?php echo (int) $min_qty; ?>;
|
|
||||||
var btn = $('.yoone-bundle-actions .single_add_to_cart_button');
|
|
||||||
if (total >= min) {
|
|
||||||
btn.prop('disabled', false);
|
|
||||||
} else {
|
|
||||||
btn.prop('disabled', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(document).on('change keyup', '.yoone-bundle-qty', updateState);
|
|
||||||
$(document).ready(updateState);
|
|
||||||
})(jQuery);
|
|
||||||
</script>
|
|
||||||
|
|
@ -38,10 +38,13 @@ add_action('plugins_loaded', function () {
|
||||||
Yoone_Product_Bundles_Frontend::instance();
|
Yoone_Product_Bundles_Frontend::instance();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 插件版本号
|
||||||
|
define('YOONE_PB_VERSION', '0.1.1');
|
||||||
|
|
||||||
// 资源加载(前端样式/脚本)
|
// 资源加载(前端样式/脚本)
|
||||||
add_action('wp_enqueue_scripts', function () {
|
add_action('wp_enqueue_scripts', function () {
|
||||||
wp_register_style('yoone-pb-frontend', YOONE_PB_URL . 'assets/css/frontend.css', array(), '0.1.0');
|
wp_register_style('yoone-pb-frontend', YOONE_PB_URL . 'assets/css/frontend.css', array(), YOONE_PB_VERSION);
|
||||||
wp_register_script('yoone-pb-frontend', YOONE_PB_URL . 'assets/js/frontend.js', array('jquery'), '0.1.0', true);
|
wp_register_script('yoone-pb-frontend', YOONE_PB_URL . 'assets/js/frontend.js', array('jquery'), YOONE_PB_VERSION, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 后台资源
|
// 后台资源
|
||||||
|
|
@ -52,7 +55,8 @@ add_action('admin_enqueue_scripts', function ($hook) {
|
||||||
if ($screen && 'product' === $screen->post_type) {
|
if ($screen && 'product' === $screen->post_type) {
|
||||||
wp_enqueue_style('woocommerce_admin_styles');
|
wp_enqueue_style('woocommerce_admin_styles');
|
||||||
wp_enqueue_script('wc-product-search'); // Woo 的 select2 产品搜索
|
wp_enqueue_script('wc-product-search'); // Woo 的 select2 产品搜索
|
||||||
wp_enqueue_style('yoone-pb-admin', YOONE_PB_URL . 'assets/css/admin.css', array(), '0.1.0');
|
wp_enqueue_style('yoone-pb-admin', YOONE_PB_URL . 'assets/css/admin.css', array(), YOONE_PB_VERSION);
|
||||||
|
wp_enqueue_script('yoone-pb-admin', YOONE_PB_URL . 'assets/js/admin.js', array('jquery'), YOONE_PB_VERSION, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue