feat(前端): 重构混装产品前端逻辑并优化购物车处理

重构混装产品的前端逻辑,改用模板部分替换完整页面模板
优化购物车处理流程,实现容器与子项关联管理
移除旧版混装产品单页模板,新增全局混装表单模板
This commit is contained in:
tikkhun 2025-11-06 17:26:36 +08:00
parent 02d7cf85df
commit 1746bb5dd2
7 changed files with 308 additions and 304 deletions

View File

@ -1,6 +1,6 @@
<?php
/**
* 后台:为混装产品提供配置界面(可混装商品、最小数量、分类)。
* Admin: Provides the configuration interface for bundle products (allowed products, min quantity, categories).
*/
defined('ABSPATH') || exit;
@ -28,9 +28,9 @@ class Yoone_Product_Bundles_Admin {
public function add_product_data_tab($tabs) {
$tabs['yoone_bundle'] = array(
'label' => __('混装产品', '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 '<div class="options_group">';
echo '<p>' . esc_html__('配置可混装的简单商品、最小混装数量以及前端展示的分类分组。', 'yoone-product-bundles') . '</p>';
echo '<p>' . 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') . '</p>';
// 可混装商品:使用 Woo 的产品搜索select2multiple
echo '<p class="form-field"><label>' . esc_html__('可混装商品', 'yoone-product-bundles') . '</label>';
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">';
// Allowed products: use Woo's product search (select2), multiple
echo '<p class="form-field"><label>' . esc_html__('Allowed Products', 'yoone-product-bundles') . '</label>';
echo '<button type="button" class="button yoone-add-all-simple-products" style="margin-left: 10px;">' . esc_html__('Add All Simple Products', 'yoone-product-bundles') . '</button>';
// Search only for products, not variations, to avoid errors
echo '<select class="wc-product-search" multiple style="width: 90%;" name="yoone_bundle_allowed_products[]" data-placeholder="' . esc_attr__('Search for simple products…', 'yoone-product-bundles') . '" data-action="woocommerce_json_search_products">';
if (! empty($allowed)) {
foreach ($allowed as $pid) {
$p = wc_get_product($pid);
@ -64,30 +64,30 @@ class Yoone_Product_Bundles_Admin {
}
}
echo '</select>';
echo '<span class="description">' . esc_html__('仅支持 simple product变体商品可在后续版本支持。', 'yoone-product-bundles') . '</span>';
echo '<span class="description">' . esc_html__('Only simple products are supported. Variable products may be supported in a future version.', 'yoone-product-bundles') . '</span>';
echo '</p>';
// 最小混装数量
// 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 '<p class="form-field"><label>' . esc_html__('前端展示分类', 'yoone-product-bundles') . '</label>';
echo '<select class="wc-enhanced-select" multiple style="width: 90%;" name="yoone_bundle_categories[]" data-placeholder="' . esc_attr__('选择用于分组展示的分类…', 'yoone-product-bundles') . '">';
// Display categories (product_cat)
echo '<p class="form-field"><label>' . esc_html__('Display Categories', 'yoone-product-bundles') . '</label>';
echo '<select class="wc-enhanced-select" multiple style="width: 90%;" name="yoone_bundle_categories[]" data-placeholder="' . esc_attr__('Select categories to group products by…', 'yoone-product-bundles') . '">';
$terms = get_terms(array('taxonomy' => 'product_cat', 'hide_empty' => false));
foreach ($terms as $t) {
$selected = in_array($t->term_id, $cats, true) ? 'selected' : '';
printf('<option value="%d" %s>%s</option>', $t->term_id, $selected, esc_html($t->name));
}
echo '</select>';
echo '<span class="description">' . esc_html__('用于在前端页面按分类分组展示可混装商品(仅展示与所选分类匹配的可混装商品)。', 'yoone-product-bundles') . '</span>';
echo '<span class="description">' . esc_html__('Group the allowed products by category on the frontend. Only products matching the selected categories will be shown.', 'yoone-product-bundles') . '</span>';
echo '</p>';
echo '</div>'; // options_group

View File

@ -1,13 +1,13 @@
<?php
/**
* 核心:注册产品类型、常量、工具方法。
* Core: Registers the product type, constants, and utility methods.
*/
defined('ABSPATH') || exit;
class Yoone_Product_Bundles {
const TYPE = 'yoone_bundle';
// 配置的 postmeta 键名
// Post meta keys for configuration
const META_ALLOWED_PRODUCTS = '_yoone_bundle_allowed_products'; // array<int>
const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int
const META_CATEGORIES = '_yoone_bundle_categories'; // array<int> 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();

View File

@ -1,15 +1,16 @@
<?php
/**
* 产品类型对象:WC_Product_Yoone_Bundle
* - 定义产品类型标识;
* - 可选择设置为售卖方式(例如仅允许单件购买)。
* Product Type Object: WC_Product_Yoone_Bundle
* - Defines the product type identifier;
* - Can be configured to set purchasing rules (e.g., sold individually).
*/
defined('ABSPATH') || exit;
class WC_Product_Yoone_Bundle extends WC_Product {
public function __construct($product) {
parent::__construct($product);
$this->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.
}
}

View File

@ -1,6 +1,6 @@
<?php
/**
* 前端:渲染混装产品的自定义 add-to-cart 表单、校验并加入购物车
* 前端:为混装产品渲染自定义的“添加到购物车”表单,并处理验证和加入购物车的逻辑
*/
defined('ABSPATH') || exit;
@ -16,53 +16,160 @@ class Yoone_Product_Bundles_Frontend {
}
private function __construct() {
// 加载自定义的混装产品页面模板
add_filter('template_include', array($this, 'use_bundle_template'), 99);
$this->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() . ' &times; ' . $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);
}
}
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* Yoone Product Bundles - Mix and Match Product Selection Form
*
* This template part contains the full frontend interface for selecting bundle components,
* including the card-based layout, quantity inputs, and dynamically updated total count.
* It is hooked into `woocommerce_single_product_summary` to replace the standard add-to-cart form.
*
* @version 3.0.1
*/
defined('ABSPATH') || exit;
global $product;
// --- 数据准备逻辑 ---
$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));
}
// --- 数据准备逻辑结束 ---
?>
<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__('Select at least %d items to build your bundle.', 'yoone-product-bundles'), $min_qty); ?></p>
<p class="yoone-bundle-selected"><?php esc_html_e('Currently selected:', 'yoone-product-bundles'); ?> <span class="yoone-bundle-selected-count">0</span></p>
<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('Add to Cart', 'yoone-product-bundles'); ?></button>
</div>
</div>
<?php if (empty($allowed_products)) : ?>
<p><?php esc_html_e('No products are available for this bundle. Please configure it in the backend.', '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('Available Products', 'yoone-product-bundles'); ?></h3>
<?php endif; ?>
<?php if (empty($group['items'])) : ?>
<p class="yoone-bundle-no-items"><?php esc_html_e('No products available in this group.', '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; ?>
</form>
</div>

View File

@ -1,141 +0,0 @@
<?php
/**
* Yoone Product Bundles - 混装产品单页模板 (v2)
*
* 此模板将完全取代 WooCommerce 的默认 single-product.php提供一个包含卡片式选项的自定义布局。
* 它通过调用 get_header(), get_footer() do_action() 来确保与主题的兼容性。
*
* @version 2.0.0
*/
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); ?>>
<?php do_action('woocommerce_before_single_product_summary'); ?>
<div class="summary entry-summary">
<?php
do_action('woocommerce_single_product_summary');
?>
</div>
<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
do_action('woocommerce_after_single_product_summary');
?>
</div>
<?php do_action('woocommerce_after_single_product'); ?>
<?php get_footer('shop'); ?>

View File

@ -1,7 +1,7 @@
<?php
/**
* Plugin Name: Yoone Product Bundles (混装产品)
* Description: WooCommerce 提供“混装产品Bundle”类型在后台为某个产品配置可混装的简单商品与最小混装数量前端允许顾客按分类选择混装商品与数量当达到最小数量时可加入购物车结算价格为所选商品的价格总和。
* Plugin Name: Yoone Product Bundles
* Description: Adds a "Mix and Match" product type to WooCommerce. Allows creating a bundle product where customers can select from a list of simple products, set quantities, and add to cart if a minimum quantity is met. The price is the sum of the selected products.
* Author: Yoone
* Version: 0.1.0
* Requires at least: 6.0
@ -12,15 +12,15 @@
defined('ABSPATH') || exit;
// 简单防御:确保 WooCommerce 已激活
// Basic defense: ensure WooCommerce is active
if (! function_exists('WC')) {
add_action('admin_notices', function () {
echo '<div class="notice notice-error"><p>Yoone Product Bundles 需要启用 WooCommerce。</p></div>';
echo '<div class="notice notice-error"><p>' . esc_html__('Yoone Product Bundles requires WooCommerce to be activated.', 'yoone-product-bundles') . '</p></div>';
});
return;
}
// 插件常量(路径)
// Plugin constants (paths)
define('YOONE_PB_PATH', plugin_dir_path(__FILE__));
define('YOONE_PB_URL', plugin_dir_url(__FILE__));