From 1746bb5dd2f0137dd4e3ba2074c7b71786621d9c Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 6 Nov 2025 17:26:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=89=8D=E7=AB=AF):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=B7=B7=E8=A3=85=E4=BA=A7=E5=93=81=E5=89=8D=E7=AB=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E4=BC=98=E5=8C=96=E8=B4=AD=E7=89=A9=E8=BD=A6?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构混装产品的前端逻辑,改用模板部分替换完整页面模板 优化购物车处理流程,实现容器与子项关联管理 移除旧版混装产品单页模板,新增全局混装表单模板 --- .../class-yoone-product-bundles-admin.php | 34 +-- includes/class-yoone-product-bundles.php | 18 +- includes/class-yoone-product-type-bundle.php | 37 ++- .../class-yoone-product-bundles-frontend.php | 262 +++++++++--------- templates/global/yoone-bundle-form.php | 110 ++++++++ templates/single-product-yoone-bundle.php | 141 ---------- yoone-product-bundles.php | 10 +- 7 files changed, 308 insertions(+), 304 deletions(-) create mode 100644 templates/global/yoone-bundle-form.php delete mode 100644 templates/single-product-yoone-bundle.php diff --git a/includes/admin/class-yoone-product-bundles-admin.php b/includes/admin/class-yoone-product-bundles-admin.php index 8ba9389..ae7fd8a 100644 --- a/includes/admin/class-yoone-product-bundles-admin.php +++ b/includes/admin/class-yoone-product-bundles-admin.php @@ -1,6 +1,6 @@ __('混装产品', 'yoone-product-bundles'), + 'label' => __('Mix and Match', 'yoone-product-bundles'), 'target' => 'yoone_bundle_data', - 'class' => array('show_if_yoone_bundle'), // 仅当类型为 yoone_bundle 时显示 + 'class' => array('show_if_yoone_bundle'), // Only show for 'yoone_bundle' type 'priority' => 70, ); return $tabs; @@ -48,13 +48,13 @@ class Yoone_Product_Bundles_Admin { wp_nonce_field('yoone-bundle-admin-nonce', 'yoone_bundle_admin_nonce_field'); echo '
'; - echo '

' . esc_html__('配置可混装的简单商品、最小混装数量以及前端展示的分类分组。', 'yoone-product-bundles') . '

'; + echo '

' . esc_html__('Configure the simple products that can be included in the bundle, the minimum quantity, and the category grouping for the frontend display.', 'yoone-product-bundles') . '

'; - // 可混装商品:使用 Woo 的产品搜索(select2),multiple - echo '

'; - echo ''; - // 仅搜索产品,不含变体,避免误选导致前端不显示 - echo ''; if (! empty($allowed)) { foreach ($allowed as $pid) { $p = wc_get_product($pid); @@ -64,30 +64,30 @@ class Yoone_Product_Bundles_Admin { } } echo ''; - echo '' . esc_html__('仅支持 simple product;变体商品可在后续版本支持。', 'yoone-product-bundles') . ''; + echo '' . esc_html__('Only simple products are supported. Variable products may be supported in a future version.', 'yoone-product-bundles') . ''; echo '

'; - // 最小混装数量 + // Minimum bundle quantity woocommerce_wp_text_input(array( 'id' => 'yoone_bundle_min_quantity', - 'label' => __('最小混装数量', 'yoone-product-bundles'), + 'label' => __('Minimum Quantity', 'yoone-product-bundles'), 'type' => 'number', 'desc_tip' => true, - 'description' => __('顾客选择的总数量需不小于该值,方可加入购物车。', 'yoone-product-bundles'), + 'description' => __('The total quantity of selected items must be greater than or equal to this value to add to the cart.', 'yoone-product-bundles'), 'value' => $min_qty, 'custom_attributes' => array('min' => '0'), )); - // 展示分类(product_cat) - echo '

'; - echo ''; $terms = get_terms(array('taxonomy' => 'product_cat', 'hide_empty' => false)); foreach ($terms as $t) { $selected = in_array($t->term_id, $cats, true) ? 'selected' : ''; printf('', $t->term_id, $selected, esc_html($t->name)); } echo ''; - echo '' . esc_html__('用于在前端页面按分类分组展示可混装商品(仅展示与所选分类匹配的可混装商品)。', 'yoone-product-bundles') . ''; + echo '' . esc_html__('Group the allowed products by category on the frontend. Only products matching the selected categories will be shown.', 'yoone-product-bundles') . ''; echo '

'; echo '
'; // options_group diff --git a/includes/class-yoone-product-bundles.php b/includes/class-yoone-product-bundles.php index 2784c74..e5f8771 100644 --- a/includes/class-yoone-product-bundles.php +++ b/includes/class-yoone-product-bundles.php @@ -1,13 +1,13 @@ const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int const META_CATEGORIES = '_yoone_bundle_categories'; // array product_cat term_ids @@ -22,22 +22,22 @@ class Yoone_Product_Bundles { } private function __construct() { - // 将产品类型加入选择器 + // Add "Mix and Match" to the "Product Type" dropdown in the admin. add_filter('product_type_selector', array($this, 'register_product_type_in_selector')); - // 将类型映射到我们的 WC_Product 派生类 + // Map the type to our class name, so WooCommerce instantiates the correct product object. add_filter('woocommerce_product_class', array($this, 'map_product_class'), 10, 2); } /** - * 在后台“产品类型”下拉中添加“混装产品”。 + * Add "Mix and Match" to the "Product Type" dropdown in the admin. */ public function register_product_type_in_selector($types) { - $types[self::TYPE] = __('混装产品 (Yoone Bundle)', 'yoone-product-bundles'); + $types[self::TYPE] = __('Mix and Match (Yoone Bundle)', 'yoone-product-bundles'); return $types; } /** - * 将类型映射到类名,WooCommerce 会实例化对应产品对象。 + * Map the type to our class name, so WooCommerce instantiates the correct product object. */ public function map_product_class($classname, $product_type) { if ($product_type === self::TYPE) { @@ -47,7 +47,7 @@ class Yoone_Product_Bundles { } /** - * 读取并规范化 bundle 配置。 + * Read and normalize the bundle configuration. * @param int|WC_Product $product * @return array{allowed_products:int[],min_qty:int,categories:int[]} */ @@ -58,7 +58,7 @@ class Yoone_Product_Bundles { $allowed = get_post_meta($pid, self::META_ALLOWED_PRODUCTS, true); $min = absint(get_post_meta($pid, self::META_MIN_QTY, true)); $cats = get_post_meta($pid, self::META_CATEGORIES, true); - // 仅保留 simple 产品(避免后台选择了变体或其他类型导致前端不显示) + // Keep only simple products (to avoid issues if variations or other types were selected in the backend) $allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array(); if (! empty($allowed)) { $simple_only = array(); diff --git a/includes/class-yoone-product-type-bundle.php b/includes/class-yoone-product-type-bundle.php index d66740a..8c1613a 100644 --- a/includes/class-yoone-product-type-bundle.php +++ b/includes/class-yoone-product-type-bundle.php @@ -1,15 +1,16 @@ set_virtual(true); // 混装产品不作为单独实物发货,实际发货明细由所选组件决定(MVP 简化) + // A bundle product itself isn't shipped; shipping is determined by its components (MVP simplification). + $this->set_virtual(true); } public function get_type() { @@ -17,9 +18,35 @@ class WC_Product_Yoone_Bundle extends WC_Product { } /** - * 混装产品通常只允许单件购买(其内部包含多个组件)。 + * Bundle products are typically sold individually as they contain multiple items. */ public function is_sold_individually() { return true; } + + // --- Price Override --- + + public function get_price($context = 'view') { + return 0; + } + + public function get_regular_price($context = 'view') { + return 0; + } + + public function get_sale_price($context = 'view') { + return 0; + } + + public function set_price($price) { + // Prevent price from being changed. + } + + public function set_regular_price($price) { + // Prevent price from being changed. + } + + public function set_sale_price($price) { + // Prevent price from being changed. + } } \ No newline at end of file diff --git a/includes/frontend/class-yoone-product-bundles-frontend.php b/includes/frontend/class-yoone-product-bundles-frontend.php index df1402e..d132663 100644 --- a/includes/frontend/class-yoone-product-bundles-frontend.php +++ b/includes/frontend/class-yoone-product-bundles-frontend.php @@ -1,6 +1,6 @@ setup_hooks(); + } - // 处理加入购物车:校验数量并打包所选组件数据 - add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 6); - add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3); + /** + * Setup all frontend-related hooks. + */ + public function setup_hooks() { + // Handle the custom add-to-cart process for bundles + add_filter('woocommerce_add_to_cart_validation', array($this, 'process_bundle_add_to_cart'), 10, 3); - // 在购物车/订单中显示组件摘要 - add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2); + // When a bundle container is removed, remove its children. + add_action('woocommerce_cart_item_removed', array($this, 'remove_bundle_children'), 10, 2); - // 结算前动态调整价格 - add_action('woocommerce_before_calculate_totals', array($this, 'adjust_bundle_price'), 20, 1); + // Hide the "remove" link for child items in the cart. + add_filter('woocommerce_cart_item_remove_link', array($this, 'hide_child_remove_link'), 10, 2); + + // In the cart, show which bundle a child item belongs to. + add_filter('woocommerce_get_item_data', array($this, 'display_child_bundle_link'), 10, 2); - // 为混装产品页增加 body class,便于主题兼容样式控制 + // Add a body class to the bundle product page for easier styling. 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')); + // Replace the default "Add to Cart" button with our custom form. + add_action('woocommerce_single_product_summary', array($this, 'remove_default_add_to_cart_for_bundle'), 29); + add_action('woocommerce_single_product_summary', array($this, 'render_bundle_add_to_cart_form'), 30); } /** - * 移除默认的 add-to-cart 按钮,因为我们的模板会自己处理 + * Hijack the add-to-cart process for bundle products. + * Validates the submission, then adds the container and all child products to the cart. + * + * @return bool False to prevent the default add-to-cart action. */ - public function remove_default_add_to_cart() { - remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30); + public function process_bundle_add_to_cart($passed, $product_id, $quantity) { + $product = wc_get_product($product_id); + if (!$product || $product->get_type() !== Yoone_Product_Bundles::TYPE) { + return $passed; // Not our product, pass through. + } + + // 1. Validation + $config = Yoone_Product_Bundles::get_bundle_config($product); + $min_qty = max(1, absint($config['min_qty'])); + $components = !empty($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array(); + + $total_qty = 0; + $clean_components = array(); + foreach ($components as $comp_id => $qty) { + $qty = absint($qty); + if ($qty > 0) { + $total_qty += $qty; + $clean_components[absint($comp_id)] = $qty; + } + } + + if ($total_qty < $min_qty) { + wc_add_notice(sprintf(__('You need to select at least %d items to add this bundle to your cart.', 'yoone-product-bundles'), $min_qty), 'error'); + return false; + } + + // 2. Add items to cart + try { + $bundle_container_id = uniqid('bundle_'); + + // Add the main bundle product (as a 0-price container) + WC()->cart->add_to_cart($product_id, 1, 0, array(), array('yoone_bundle_container_id' => $bundle_container_id)); + + // Add child components + foreach ($clean_components as $comp_id => $qty) { + WC()->cart->add_to_cart($comp_id, $qty, 0, array(), array( + 'yoone_bundle_parent_id' => $bundle_container_id, + 'yoone_bundle_parent_product_id' => $product_id + )); + } + + // Set success message and redirect + wc_add_to_cart_message(array($product_id => 1), true); + add_filter('woocommerce_add_to_cart_redirect', function() { return wc_get_cart_url(); }); + + } catch (Exception $e) { + wc_add_notice($e->getMessage(), 'error'); + return false; + } + + // Prevent default add-to-cart for the main product + return false; } /** - * 如果是混装产品页面,则加载我们的自定义模板。 + * When a cart item is removed, check if it's a bundle container. + * If so, find and remove all its child items. */ - 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)) { - return $new_template; + public function remove_bundle_children($removed_cart_item_key, $cart) { + $removed_item = $cart->get_removed_cart_items()[$removed_cart_item_key]; + + if (isset($removed_item['yoone_bundle_container_id'])) { + $bundle_container_id = $removed_item['yoone_bundle_container_id']; + + foreach ($cart->get_cart() as $cart_item_key => $cart_item) { + if (isset($cart_item['yoone_bundle_parent_id']) && $cart_item['yoone_bundle_parent_id'] === $bundle_container_id) { + $cart->remove_cart_item($cart_item_key); } } } - return $template; } /** - * 在混装产品页增加 body class。 + * Hide the remove link for child items of a bundle. + */ + public function hide_child_remove_link($link, $cart_item_key) { + $cart_item = WC()->cart->get_cart_item($cart_item_key); + if (isset($cart_item['yoone_bundle_parent_id'])) { + return ''; + } + return $link; + } + + /** + * For child items, add a meta line in the cart to link back to the parent bundle. + */ + public function display_child_bundle_link($item_data, $cart_item) { + if (isset($cart_item['yoone_bundle_parent_product_id'])) { + $parent_product = wc_get_product($cart_item['yoone_bundle_parent_product_id']); + if ($parent_product) { + $item_data[] = array( + 'key' => __('Part of', 'yoone-product-bundles'), + 'value' => $parent_product->get_name(), + ); + } + } + return $item_data; + } + + /** + * Remove the default "Add to Cart" button for bundle products only. + */ + public function remove_default_add_to_cart_for_bundle() { + global $product; + if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) { + remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30); + } + } + + /** + * Render the custom "Add to Cart" form for bundle products. + */ + public function render_bundle_add_to_cart_form() { + global $product; + if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) { + wc_get_template('global/yoone-bundle-form.php', array(), '', YOONE_PB_PATH . 'templates/'); + } + } + + /** + * Add a body class for bundle product pages. */ public function filter_body_class($classes) { global $post; @@ -74,103 +181,4 @@ class Yoone_Product_Bundles_Frontend { } 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); - if ($product->get_type() !== Yoone_Product_Bundles::TYPE) { - return $passed; - } - - $config = Yoone_Product_Bundles::get_bundle_config($product); - $min_qty = max(1, absint($config['min_qty'])); - - $components = !empty($_POST['yoone_bundle_components']) ? $_POST['yoone_bundle_components'] : array(); - $total_qty = 0; - foreach ($components as $comp_id => $qty) { - $qty = absint($qty); - if ($qty > 0) { - $total_qty += $qty; - } - } - - if ($total_qty < $min_qty) { - wc_add_notice(sprintf(__('您需要至少选择 %d 件商品才能将此混装包加入购物车。', 'yoone-product-bundles'), $min_qty), 'error'); - return false; - } - - return $passed; - } - - /** - * 将混装组件数据添加到购物车项目。 - */ - public function add_cart_item_data($cart_item_data, $product_id, $variation_id) { - $product = wc_get_product($product_id); - if ($product->get_type() !== Yoone_Product_Bundles::TYPE) { - return $cart_item_data; - } - - 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; - } - } - - return $cart_item_data; - } - - /** - * 在购物车和订单中显示混装组件的摘要。 - */ - public function display_cart_item_data($item_data, $cart_item) { - if (empty($cart_item['yoone_bundle_components'])) { - return $item_data; - } - - $value = ""; - foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) { - $product = wc_get_product($pid); - if ($product) { - $value .= $product->get_name() . ' × ' . $qty . "; "; - } - } - - $item_data[] = array( - 'key' => __('混装明细', 'yoone-product-bundles'), - 'value' => rtrim($value, "; "), - 'display' => '' - ); - - return $item_data; - } - - /** - * 在将商品添加到购物车之前,根据所选组件动态计算混装产品的总价。 - */ - public function adjust_bundle_price($cart) { - if (is_admin() && !defined('DOING_AJAX')) return; - - foreach ($cart->get_cart() as $cart_item_key => $cart_item) { - if (isset($cart_item['yoone_bundle_components'])) { - $total_price = 0; - foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) { - $product = wc_get_product($pid); - if ($product) { - $total_price += (float) $product->get_price() * $qty; - } - } - $cart_item['data']->set_price($total_price); - } - } - } } \ No newline at end of file diff --git a/templates/global/yoone-bundle-form.php b/templates/global/yoone-bundle-form.php new file mode 100644 index 0000000..99057cb --- /dev/null +++ b/templates/global/yoone-bundle-form.php @@ -0,0 +1,110 @@ +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)); +} +// --- 数据准备逻辑结束 --- +?> + +
+
+
+

+

0

+
+ + +
+
+ +

+ + $group) : ?> +
+ +

name); ?>

+ +

+ + + +

+ +
+ + get_id(); + $thumbnail = $item->get_image('woocommerce_thumbnail'); + ?> +
+
+ + + +
+

+ get_name()); ?> +

+
+ get_price_html()); ?> +
+
+ +
+
+ +
+ +
+ + + +
+
\ No newline at end of file diff --git a/templates/single-product-yoone-bundle.php b/templates/single-product-yoone-bundle.php deleted file mode 100644 index 625e016..0000000 --- a/templates/single-product-yoone-bundle.php +++ /dev/null @@ -1,141 +0,0 @@ -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'); -?> - - - -
> - - - -
- -
- -
-
-
-

-

0

-
- - -

- - $group) : ?> -
- -

name); ?>

- -

- - - -

- -
- - get_id(); - $thumbnail = $item->get_image('woocommerce_thumbnail'); - ?> -
-
- - - -
-

- get_name()); ?> -

-
- get_price_html()); ?> -
-
- -
-
- -
- -
- - - -
- - -
-
-
- - - -
- - - - \ No newline at end of file diff --git a/yoone-product-bundles.php b/yoone-product-bundles.php index 555c869..78698f0 100644 --- a/yoone-product-bundles.php +++ b/yoone-product-bundles.php @@ -1,7 +1,7 @@

Yoone Product Bundles 需要启用 WooCommerce。

'; + echo '

' . esc_html__('Yoone Product Bundles requires WooCommerce to be activated.', 'yoone-product-bundles') . '

'; }); return; } -// 插件常量(路径) +// Plugin constants (paths) define('YOONE_PB_PATH', plugin_dir_path(__FILE__)); define('YOONE_PB_URL', plugin_dir_url(__FILE__));