feat(admin): 增强产品捆绑功能,支持按分类或标签分组
- 新增选择模式(包含/排除/全部)配置 - 支持按产品分类或标签进行前端分组显示 - 优化后台界面布局和交互体验 - 添加动态加载术语功能 - 完善前端样式和兼容性
This commit is contained in:
parent
08a6245a6b
commit
bb12f2cd33
|
|
@ -1,3 +1,18 @@
|
||||||
/* Yoone Product Bundles 后台样式(简版) */
|
/* Yoone Product Bundles 后台样式优化 */
|
||||||
#yoone_bundle_data .description { color: #666; display: block; margin-top: 6px; }
|
#yoone_bundle_data { padding-top: 8px; }
|
||||||
#yoone_bundle_data .form-field { margin-bottom: 12px; }
|
#yoone_bundle_data .form-field { margin-bottom: 16px; }
|
||||||
|
/* 让标签独占一行,避免窄屏换行难看 */
|
||||||
|
#yoone_bundle_data .form-field > label { display: block; font-weight: 600; margin-bottom: 6px; }
|
||||||
|
/* 描述放到下一行,颜色更柔和 */
|
||||||
|
#yoone_bundle_data .description { color: #666; display: block; margin-top: 6px; line-height: 1.4; }
|
||||||
|
/* 选择器与搜索框占满一行 */
|
||||||
|
#yoone_bundle_data .wc-product-search,
|
||||||
|
#yoone_bundle_data .wc-enhanced-select,
|
||||||
|
#yoone_bundle_data select,
|
||||||
|
#yoone_bundle_data input[type="text"],
|
||||||
|
#yoone_bundle_data input[type="number"] { width: 100% !important; max-width: 820px; }
|
||||||
|
/* Select2 容器也撑满 */
|
||||||
|
#yoone_bundle_data .select2-container { width: 100% !important; max-width: 820px; }
|
||||||
|
/* 按钮的间距 */
|
||||||
|
#yoone_bundle_data .yoone-add-all-simple-products { margin-left: 0; margin-bottom: 8px; }
|
||||||
|
#yoone_bundle_data .yoone-clear-products-list { margin-left: 8px; margin-bottom: 8px; }
|
||||||
|
|
@ -1,52 +1,85 @@
|
||||||
(function($) {
|
/* Yoone Product Bundles - Admin dynamic grouping terms
|
||||||
$(document).ready(function() {
|
* When switching grouping taxonomy (category/tag), refresh the terms select list accordingly.
|
||||||
// “一键添加所有 Simple Product” 按钮点击事件
|
*/
|
||||||
$(document).on('click', '.yoone-add-all-simple-products', function(e) {
|
(function($){
|
||||||
e.preventDefault();
|
$(function(){
|
||||||
|
var $tax = $('#yoone_bundle_group_taxonomy');
|
||||||
|
var $terms = $('#yoone_bundle_group_terms');
|
||||||
|
var $addAllBtn = $('.yoone-add-all-simple-products');
|
||||||
|
var $clearBtn = $('.yoone-clear-products-list');
|
||||||
|
var $productSelect = $('select[name="yoone_bundle_allowed_products[]"]');
|
||||||
|
|
||||||
var $button = $(this);
|
if (typeof YoonePBAdmin === 'undefined') {
|
||||||
var $select = $('select.wc-product-search[name="yoone_bundle_allowed_products[]"]');
|
return;
|
||||||
var nonce = $('#yoone_bundle_admin_nonce_field').val();
|
}
|
||||||
|
|
||||||
$button.prop('disabled', true).text('正在加载...');
|
function refreshTerms(tax) {
|
||||||
|
if (!tax) return;
|
||||||
|
$terms.prop('disabled', true);
|
||||||
|
$.post(YoonePBAdmin.ajax_url, {
|
||||||
|
action: 'yoone_get_taxonomy_terms',
|
||||||
|
taxonomy: tax,
|
||||||
|
security: YoonePBAdmin.security
|
||||||
|
}).done(function(resp){
|
||||||
|
if (resp && resp.success && Array.isArray(resp.data)) {
|
||||||
|
var items = resp.data;
|
||||||
|
$terms.empty();
|
||||||
|
items.forEach(function(item){
|
||||||
|
var opt = $('<option/>').val(item.id).text(item.text);
|
||||||
|
$terms.append(opt);
|
||||||
|
});
|
||||||
|
// re-init enhanced selects
|
||||||
|
$(document.body).trigger('wc-enhanced-select-init');
|
||||||
|
}
|
||||||
|
}).always(function(){
|
||||||
|
$terms.prop('disabled', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$tax.on('change', function(){
|
||||||
url: ajaxurl,
|
var val = $(this).val();
|
||||||
method: 'POST',
|
refreshTerms(val);
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add all simple products to the products select
|
||||||
|
if ($addAllBtn.length && $productSelect.length) {
|
||||||
|
$addAllBtn.on('click', function(){
|
||||||
|
var $btn = $(this);
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
$productSelect.prop('disabled', true);
|
||||||
|
$.post(YoonePBAdmin.ajax_url, {
|
||||||
|
action: 'yoone_get_all_simple_products',
|
||||||
|
security: YoonePBAdmin.security
|
||||||
|
}).done(function(resp){
|
||||||
|
if (resp && resp.success && Array.isArray(resp.data)) {
|
||||||
|
// Clear then add all as selected
|
||||||
|
$productSelect.empty();
|
||||||
|
resp.data.forEach(function(item){
|
||||||
|
var existing = $productSelect.find('option[value="'+item.id+'"]');
|
||||||
|
if (!existing.length) {
|
||||||
|
var opt = $('<option/>').val(item.id).text(item.text).prop('selected', true);
|
||||||
|
$productSelect.append(opt);
|
||||||
|
} else {
|
||||||
|
existing.prop('selected', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Trigger change for select2/Woo product search
|
||||||
|
$productSelect.trigger('change');
|
||||||
|
}
|
||||||
|
}).always(function(){
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
$productSelect.prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear products list quickly
|
||||||
|
if ($clearBtn.length && $productSelect.length) {
|
||||||
|
$clearBtn.on('click', function(){
|
||||||
|
$productSelect.find('option').prop('selected', false);
|
||||||
|
// For select2/Woo product search, simply trigger change after clearing selection
|
||||||
|
$productSelect.val([]).trigger('change');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|
@ -24,6 +24,11 @@ class Yoone_Product_Bundles_Admin {
|
||||||
|
|
||||||
// AJAX: 获取所有 simple product
|
// AJAX: 获取所有 simple product
|
||||||
add_action('wp_ajax_yoone_get_all_simple_products', array($this, 'ajax_get_all_simple_products'));
|
add_action('wp_ajax_yoone_get_all_simple_products', array($this, 'ajax_get_all_simple_products'));
|
||||||
|
// AJAX: 根据所选 taxonomy 获取术语列表
|
||||||
|
add_action('wp_ajax_yoone_get_taxonomy_terms', array($this, 'ajax_get_taxonomy_terms'));
|
||||||
|
|
||||||
|
// 后台资源
|
||||||
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add_product_data_tab($tabs) {
|
public function add_product_data_tab($tabs) {
|
||||||
|
|
@ -42,7 +47,9 @@ class Yoone_Product_Bundles_Admin {
|
||||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||||
$allowed = $config['allowed_products'];
|
$allowed = $config['allowed_products'];
|
||||||
$min_qty = $config['min_qty'];
|
$min_qty = $config['min_qty'];
|
||||||
$cats = $config['categories'];
|
$select_mode = isset($config['select_mode']) ? $config['select_mode'] : 'include';
|
||||||
|
$group_taxonomy = isset($config['group_taxonomy']) ? $config['group_taxonomy'] : 'product_cat';
|
||||||
|
$group_terms = isset($config['group_terms']) ? (array) $config['group_terms'] : array();
|
||||||
|
|
||||||
echo '<div id="yoone_bundle_data" class="panel woocommerce_options_panel show_if_yoone_bundle">';
|
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');
|
wp_nonce_field('yoone-bundle-admin-nonce', 'yoone_bundle_admin_nonce_field');
|
||||||
|
|
@ -50,11 +57,28 @@ class Yoone_Product_Bundles_Admin {
|
||||||
echo '<div class="options_group">';
|
echo '<div class="options_group">';
|
||||||
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>';
|
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>';
|
||||||
|
|
||||||
// Allowed products: use Woo's product search (select2), multiple
|
// Selection mode
|
||||||
echo '<p class="form-field"><label>' . esc_html__('Allowed Products', 'yoone-product-bundles') . '</label>';
|
woocommerce_wp_select(array(
|
||||||
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>';
|
'id' => 'yoone_bundle_select_mode',
|
||||||
|
'name' => 'yoone_bundle_select_mode',
|
||||||
|
'label' => __('Selection Mode', 'yoone-product-bundles'),
|
||||||
|
'description' => __('Choose how products are selected for the bundle: include list, exclude list, or allow all simple products.', 'yoone-product-bundles'),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'include' => __('Include (only listed products)', 'yoone-product-bundles'),
|
||||||
|
'exclude' => __('Exclude (allow all except listed)', 'yoone-product-bundles'),
|
||||||
|
'all' => __('All (allow all simple products)', 'yoone-product-bundles'),
|
||||||
|
),
|
||||||
|
'value' => $select_mode,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Allowed/Excluded products: use Woo's product search (select2), multiple
|
||||||
|
echo '<p class="form-field"><label>' . esc_html__('Products List', 'yoone-product-bundles') . '</label>';
|
||||||
|
echo '<button type="button" class="button yoone-add-all-simple-products">' . esc_html__('Add All Simple Products', 'yoone-product-bundles') . '</button>';
|
||||||
|
echo ' ';
|
||||||
|
echo '<button type="button" class="button yoone-clear-products-list">' . esc_html__('Clear Products List', 'yoone-product-bundles') . '</button>';
|
||||||
// Search only for products, not variations, to avoid errors
|
// 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">';
|
echo '<select class="wc-product-search" multiple style="width: 100%;" 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)) {
|
if (! empty($allowed)) {
|
||||||
foreach ($allowed as $pid) {
|
foreach ($allowed as $pid) {
|
||||||
$p = wc_get_product($pid);
|
$p = wc_get_product($pid);
|
||||||
|
|
@ -64,7 +88,8 @@ class Yoone_Product_Bundles_Admin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
echo '</select>';
|
echo '</select>';
|
||||||
echo '<span class="description">' . esc_html__('Only simple products are supported. Variable products may be supported in a future version.', 'yoone-product-bundles') . '</span>';
|
// Dynamic description based on selection mode
|
||||||
|
echo '<span class="description">' . esc_html__('Simple products only. Include: only listed; Exclude: allow all except listed; All: ignore the list.', 'yoone-product-bundles') . '</span>';
|
||||||
echo '</p>';
|
echo '</p>';
|
||||||
|
|
||||||
// Minimum bundle quantity
|
// Minimum bundle quantity
|
||||||
|
|
@ -78,16 +103,29 @@ class Yoone_Product_Bundles_Admin {
|
||||||
'custom_attributes' => array('min' => '0'),
|
'custom_attributes' => array('min' => '0'),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Display categories (product_cat)
|
// Grouping taxonomy (product_cat or product_tag)
|
||||||
echo '<p class="form-field"><label>' . esc_html__('Display Categories', 'yoone-product-bundles') . '</label>';
|
woocommerce_wp_select(array(
|
||||||
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') . '">';
|
'id' => 'yoone_bundle_group_taxonomy',
|
||||||
$terms = get_terms(array('taxonomy' => 'product_cat', 'hide_empty' => false));
|
'label' => __('Grouping Taxonomy', 'yoone-product-bundles'),
|
||||||
|
'description' => __('Choose which taxonomy to group products by: category or tag.', 'yoone-product-bundles'),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'product_cat' => __('Category', 'yoone-product-bundles'),
|
||||||
|
'product_tag' => __('Tag', 'yoone-product-bundles'),
|
||||||
|
),
|
||||||
|
'value' => $group_taxonomy,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Display terms of selected taxonomy
|
||||||
|
echo '<p class="form-field"><label>' . esc_html__('Display Terms', 'yoone-product-bundles') . '</label>';
|
||||||
|
echo '<select id="yoone_bundle_group_terms" class="wc-enhanced-select" multiple style="width: 100%;" name="yoone_bundle_group_terms[]" data-placeholder="' . esc_attr__('Select terms to group products by…', 'yoone-product-bundles') . '">';
|
||||||
|
$terms = get_terms(array('taxonomy' => $group_taxonomy, 'hide_empty' => false));
|
||||||
foreach ($terms as $t) {
|
foreach ($terms as $t) {
|
||||||
$selected = in_array($t->term_id, $cats, true) ? 'selected' : '';
|
$selected = in_array($t->term_id, $group_terms, true) ? 'selected' : '';
|
||||||
printf('<option value="%d" %s>%s</option>', $t->term_id, $selected, esc_html($t->name));
|
printf('<option value="%d" %s>%s</option>', $t->term_id, $selected, esc_html($t->name));
|
||||||
}
|
}
|
||||||
echo '</select>';
|
echo '</select>';
|
||||||
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 '<span class="description">' . esc_html__('Group the allowed products by the selected taxonomy terms on the frontend. Only products matching the selected terms will be shown.', 'yoone-product-bundles') . '</span>';
|
||||||
echo '</p>';
|
echo '</p>';
|
||||||
|
|
||||||
echo '</div>'; // options_group
|
echo '</div>'; // options_group
|
||||||
|
|
@ -97,7 +135,7 @@ class Yoone_Product_Bundles_Admin {
|
||||||
public function save_product_meta($post_id) {
|
public function save_product_meta($post_id) {
|
||||||
// 无论当前 product 对象类型为何,只要提交了我们的字段,就进行保存。
|
// 无论当前 product 对象类型为何,只要提交了我们的字段,就进行保存。
|
||||||
// 这可以避免在首次切换产品类型时由于保存顺序问题导致配置未写入。
|
// 这可以避免在首次切换产品类型时由于保存顺序问题导致配置未写入。
|
||||||
$has_fields = isset($_POST['yoone_bundle_allowed_products']) || isset($_POST['yoone_bundle_min_quantity']) || isset($_POST['yoone_bundle_categories']);
|
$has_fields = isset($_POST['yoone_bundle_allowed_products']) || isset($_POST['yoone_bundle_min_quantity']) || isset($_POST['yoone_bundle_group_taxonomy']) || isset($_POST['yoone_bundle_group_terms']) || isset($_POST['yoone_bundle_select_mode']);
|
||||||
if (! $has_fields) return;
|
if (! $has_fields) return;
|
||||||
|
|
||||||
// 保存 allowed products
|
// 保存 allowed products
|
||||||
|
|
@ -120,10 +158,14 @@ class Yoone_Product_Bundles_Admin {
|
||||||
$min_qty = isset($_POST['yoone_bundle_min_quantity']) ? absint($_POST['yoone_bundle_min_quantity']) : 0;
|
$min_qty = isset($_POST['yoone_bundle_min_quantity']) ? absint($_POST['yoone_bundle_min_quantity']) : 0;
|
||||||
update_post_meta($post_id, Yoone_Product_Bundles::META_MIN_QTY, max(0, $min_qty));
|
update_post_meta($post_id, Yoone_Product_Bundles::META_MIN_QTY, max(0, $min_qty));
|
||||||
|
|
||||||
// 保存 categories
|
// 保存 grouping taxonomy
|
||||||
$cats = isset($_POST['yoone_bundle_categories']) ? (array) $_POST['yoone_bundle_categories'] : array();
|
$group_taxonomy = isset($_POST['yoone_bundle_group_taxonomy']) ? sanitize_text_field($_POST['yoone_bundle_group_taxonomy']) : 'product_cat';
|
||||||
$cats = array_values(array_filter(array_map('absint', $cats)));
|
$group_taxonomy = in_array($group_taxonomy, array('product_cat','product_tag'), true) ? $group_taxonomy : 'product_cat';
|
||||||
update_post_meta($post_id, Yoone_Product_Bundles::META_CATEGORIES, $cats);
|
update_post_meta($post_id, Yoone_Product_Bundles::META_GROUP_TAXONOMY, $group_taxonomy);
|
||||||
|
// 保存选择的术语
|
||||||
|
$terms = isset($_POST['yoone_bundle_group_terms']) ? (array) $_POST['yoone_bundle_group_terms'] : array();
|
||||||
|
$terms = array_values(array_filter(array_map('absint', $terms)));
|
||||||
|
update_post_meta($post_id, Yoone_Product_Bundles::META_GROUP_TERMS, $terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -151,4 +193,53 @@ class Yoone_Product_Bundles_Admin {
|
||||||
|
|
||||||
wp_send_json_success($results);
|
wp_send_json_success($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 根据选择的 taxonomy 返回术语列表
|
||||||
|
*/
|
||||||
|
public function ajax_get_taxonomy_terms() {
|
||||||
|
if (! current_user_can('edit_products')) {
|
||||||
|
wp_send_json_error('permission_denied', 403);
|
||||||
|
}
|
||||||
|
check_ajax_referer('yoone-bundle-admin-nonce', 'security');
|
||||||
|
|
||||||
|
$taxonomy = isset($_POST['taxonomy']) ? sanitize_text_field($_POST['taxonomy']) : 'product_cat';
|
||||||
|
if (! in_array($taxonomy, array('product_cat', 'product_tag'), true)) {
|
||||||
|
wp_send_json_error(array('message' => 'invalid_taxonomy'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$terms = get_terms(array('taxonomy' => $taxonomy, 'hide_empty' => false));
|
||||||
|
$results = array();
|
||||||
|
foreach ($terms as $t) {
|
||||||
|
$results[] = array('id' => (int) $t->term_id, 'text' => $t->name);
|
||||||
|
}
|
||||||
|
wp_send_json_success($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台资源:注入脚本以在切换分组 taxonomy 时动态刷新术语选择
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_assets($hook) {
|
||||||
|
// 仅在产品编辑页面加载
|
||||||
|
if ($hook !== 'post.php' && $hook !== 'post-new.php') return;
|
||||||
|
$screen = get_current_screen();
|
||||||
|
if (! $screen || $screen->post_type !== 'product') return;
|
||||||
|
|
||||||
|
$handle_js = 'yoone-pb-admin';
|
||||||
|
wp_register_script($handle_js, plugins_url('assets/js/admin.js', dirname(__FILE__, 3) . '/yoone-product-bundles.php'), array('jquery'), '1.0.0', true);
|
||||||
|
wp_enqueue_script($handle_js);
|
||||||
|
wp_localize_script($handle_js, 'YoonePBAdmin', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'security' => wp_create_nonce('yoone-bundle-admin-nonce'),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Enqueue CSS for nicer spacing & layout
|
||||||
|
$handle_css = 'yoone-pb-admin-css';
|
||||||
|
wp_register_style($handle_css, plugins_url('assets/css/admin.css', dirname(__FILE__, 3) . '/yoone-product-bundles.php'), array(), '1.0.1');
|
||||||
|
wp_enqueue_style($handle_css);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 保存选择模式
|
||||||
|
$select_mode = isset($_POST['yoone_bundle_select_mode']) ? sanitize_text_field($_POST['yoone_bundle_select_mode']) : 'include';
|
||||||
|
$select_mode = in_array($select_mode, array('include','exclude','all'), true) ? $select_mode : 'include';
|
||||||
|
update_post_meta($post_id, Yoone_Product_Bundles::META_SELECT_MODE, $select_mode);
|
||||||
|
|
@ -10,7 +10,13 @@ class Yoone_Product_Bundles {
|
||||||
// Post meta keys for configuration
|
// Post meta keys for configuration
|
||||||
const META_ALLOWED_PRODUCTS = '_yoone_bundle_allowed_products'; // array<int>
|
const META_ALLOWED_PRODUCTS = '_yoone_bundle_allowed_products'; // array<int>
|
||||||
const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int
|
const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int
|
||||||
const META_CATEGORIES = '_yoone_bundle_categories'; // array<int> product_cat term_ids
|
// 选择模式:include(仅包含列表)、exclude(排除列表,其余全部允许)、all(全部 simple 产品)
|
||||||
|
const META_SELECT_MODE = '_yoone_bundle_select_mode'; // 'include' | 'exclude' | 'all'
|
||||||
|
// 旧版:仅支持分类(product_cat)。
|
||||||
|
const META_CATEGORIES = '_yoone_bundle_categories'; // array<int> product_cat term_ids(兼容旧数据)
|
||||||
|
// 新版:支持按 taxonomy(product_cat 或 product_tag)进行分组显示
|
||||||
|
const META_GROUP_TAXONOMY = '_yoone_bundle_group_taxonomy'; // 'product_cat' | 'product_tag'
|
||||||
|
const META_GROUP_TERMS = '_yoone_bundle_group_terms'; // array<int> 选中的术语ID(来自所选taxonomy)
|
||||||
|
|
||||||
protected static $instance = null;
|
protected static $instance = null;
|
||||||
|
|
||||||
|
|
@ -53,11 +59,16 @@ class Yoone_Product_Bundles {
|
||||||
*/
|
*/
|
||||||
public static function get_bundle_config($product) {
|
public static function get_bundle_config($product) {
|
||||||
$product = is_numeric($product) ? wc_get_product($product) : $product;
|
$product = is_numeric($product) ? wc_get_product($product) : $product;
|
||||||
if (! $product) return array('allowed_products' => array(), 'min_qty' => 0, 'categories' => array());
|
if (! $product) return array('allowed_products' => array(), 'min_qty' => 0, 'group_taxonomy' => 'product_cat', 'group_terms' => array());
|
||||||
$pid = $product->get_id();
|
$pid = $product->get_id();
|
||||||
$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);
|
$select_mode = get_post_meta($pid, self::META_SELECT_MODE, true);
|
||||||
|
$select_mode = in_array($select_mode, array('include','exclude','all'), true) ? $select_mode : 'include';
|
||||||
|
// 读取新的分组设置
|
||||||
|
$group_tax = get_post_meta($pid, self::META_GROUP_TAXONOMY, true);
|
||||||
|
$group_terms = get_post_meta($pid, self::META_GROUP_TERMS, true);
|
||||||
|
$group_tax = in_array($group_tax, array('product_cat','product_tag'), true) ? $group_tax : 'product_cat';
|
||||||
// Keep only simple products (to avoid issues if variations or other types were selected in the backend)
|
// 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();
|
$allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array();
|
||||||
if (! empty($allowed)) {
|
if (! empty($allowed)) {
|
||||||
|
|
@ -70,11 +81,37 @@ class Yoone_Product_Bundles {
|
||||||
}
|
}
|
||||||
$allowed = $simple_only;
|
$allowed = $simple_only;
|
||||||
}
|
}
|
||||||
$cats = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
// 根据选择模式计算最终 allowed 列表
|
||||||
|
if ($select_mode === 'all' || $select_mode === 'exclude') {
|
||||||
|
// 获取所有 simple 产品ID
|
||||||
|
$all_ids = wc_get_products(array(
|
||||||
|
'type' => array('simple'),
|
||||||
|
'status' => array('publish'),
|
||||||
|
'limit' => -1,
|
||||||
|
'return' => 'ids',
|
||||||
|
));
|
||||||
|
$all_ids = is_array($all_ids) ? array_values(array_map('absint', $all_ids)) : array();
|
||||||
|
if ($select_mode === 'all') {
|
||||||
|
$allowed = $all_ids; // 全部 simple 产品
|
||||||
|
} else {
|
||||||
|
// exclude 模式:从全部 simple 产品中排除配置列表
|
||||||
|
$allowed = array_values(array_diff($all_ids, $allowed));
|
||||||
|
}
|
||||||
|
} // include 模式:保持 allowed 原样
|
||||||
|
// 兼容旧版:如果未配置新术语,但存在旧的分类设置,则沿用分类
|
||||||
|
if (empty($group_terms)) {
|
||||||
|
$cats = get_post_meta($pid, self::META_CATEGORIES, true);
|
||||||
|
$group_terms = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
||||||
|
$group_tax = 'product_cat';
|
||||||
|
} else {
|
||||||
|
$group_terms = is_array($group_terms) ? array_values(array_map('absint', $group_terms)) : array();
|
||||||
|
}
|
||||||
return array(
|
return array(
|
||||||
'allowed_products' => $allowed,
|
'allowed_products' => $allowed,
|
||||||
'min_qty' => max(0, $min),
|
'min_qty' => max(0, $min),
|
||||||
'categories' => $cats,
|
'select_mode' => $select_mode,
|
||||||
|
'group_taxonomy' => $group_tax,
|
||||||
|
'group_terms' => $group_terms,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,11 @@ global $product;
|
||||||
// --- 数据准备逻辑 ---
|
// --- 数据准备逻辑 ---
|
||||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||||
$allowed = $config['allowed_products'];
|
$allowed = $config['allowed_products'];
|
||||||
|
$select_mode = isset($config['select_mode']) ? $config['select_mode'] : 'include';
|
||||||
$min_qty = max(0, absint($config['min_qty']));
|
$min_qty = max(0, absint($config['min_qty']));
|
||||||
$cat_ids = $config['categories'];
|
// 读取按 taxonomy 分组的配置(支持 product_cat / product_tag)
|
||||||
|
$group_taxonomy = isset($config['group_taxonomy']) ? $config['group_taxonomy'] : 'product_cat';
|
||||||
|
$term_ids = isset($config['group_terms']) ? (array) $config['group_terms'] : array();
|
||||||
|
|
||||||
$allowed_products = array();
|
$allowed_products = array();
|
||||||
foreach ($allowed as $pid) {
|
foreach ($allowed as $pid) {
|
||||||
|
|
@ -26,16 +29,34 @@ foreach ($allowed as $pid) {
|
||||||
$allowed_products[$pid] = $p;
|
$allowed_products[$pid] = $p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 兜底:如果为 exclude/all 模式,但由于缓存或其他原因导致 allowed_products 为空,则直接读取所有 simple 产品
|
||||||
|
if (empty($allowed_products) && in_array($select_mode, array('exclude','all'), true)) {
|
||||||
|
$all_ids = wc_get_products(array(
|
||||||
|
'type' => array('simple'),
|
||||||
|
'status' => array('publish'),
|
||||||
|
'limit' => -1,
|
||||||
|
'return' => 'ids',
|
||||||
|
));
|
||||||
|
if (is_array($all_ids)) {
|
||||||
|
foreach ($all_ids as $pid) {
|
||||||
|
$p = wc_get_product($pid);
|
||||||
|
if ($p && $p->is_type('simple')) {
|
||||||
|
$allowed_products[$pid] = $p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$groups = array();
|
$groups = array();
|
||||||
if (!empty($cat_ids)) {
|
if (!empty($term_ids)) {
|
||||||
|
$is_grouped = true;
|
||||||
$others = array();
|
$others = array();
|
||||||
foreach ($allowed_products as $pid => $p) {
|
foreach ($allowed_products as $pid => $p) {
|
||||||
$terms = get_the_terms($pid, 'product_cat');
|
$terms = get_the_terms($pid, $group_taxonomy);
|
||||||
$matched = false;
|
$matched = false;
|
||||||
if (is_array($terms)) {
|
if (is_array($terms)) {
|
||||||
foreach ($terms as $t) {
|
foreach ($terms as $t) {
|
||||||
if (in_array($t->term_id, $cat_ids, true)) {
|
if (in_array($t->term_id, $term_ids, true)) {
|
||||||
if (!isset($groups[$t->term_id])) {
|
if (!isset($groups[$t->term_id])) {
|
||||||
$groups[$t->term_id] = array('term' => $t, 'items' => array());
|
$groups[$t->term_id] = array('term' => $t, 'items' => array());
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +69,7 @@ if (!empty($cat_ids)) {
|
||||||
}
|
}
|
||||||
if (!empty($others)) $groups[0] = array('term' => null, 'items' => $others);
|
if (!empty($others)) $groups[0] = array('term' => null, 'items' => $others);
|
||||||
} else {
|
} else {
|
||||||
|
$is_grouped = false;
|
||||||
$groups[0] = array('term' => null, 'items' => array_values($allowed_products));
|
$groups[0] = array('term' => null, 'items' => array_values($allowed_products));
|
||||||
}
|
}
|
||||||
// --- 数据准备逻辑结束 ---
|
// --- 数据准备逻辑结束 ---
|
||||||
|
|
@ -63,6 +85,12 @@ if (!empty($cat_ids)) {
|
||||||
<button type="submit" class="single_add_to_cart_button button alt" disabled><?php esc_html_e('Add to Cart', 'yoone-product-bundles'); ?></button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 兼容其它插件(如订阅插件)在标准钩位渲染附加选项
|
||||||
|
// 这些钩子通常位于默认的 add-to-cart 模板中,这里手动插入以实现联动。
|
||||||
|
do_action('woocommerce_before_add_to_cart_button');
|
||||||
|
?>
|
||||||
<?php if (empty($allowed_products)) : ?>
|
<?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>
|
<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 else : ?>
|
||||||
|
|
@ -71,7 +99,11 @@ if (!empty($cat_ids)) {
|
||||||
<?php if (!empty($group['term'])) : ?>
|
<?php if (!empty($group['term'])) : ?>
|
||||||
<h3 class="yoone-bundle-group-title"><?php echo esc_html($group['term']->name); ?></h3>
|
<h3 class="yoone-bundle-group-title"><?php echo esc_html($group['term']->name); ?></h3>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<h3 class="yoone-bundle-group-title"><?php esc_html_e('Available Products', 'yoone-product-bundles'); ?></h3>
|
<h3 class="yoone-bundle-group-title">
|
||||||
|
<?php echo $is_grouped
|
||||||
|
? esc_html__('Others', 'yoone-product-bundles')
|
||||||
|
: esc_html__('All Products', 'yoone-product-bundles'); ?>
|
||||||
|
</h3>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (empty($group['items'])) : ?>
|
<?php if (empty($group['items'])) : ?>
|
||||||
|
|
@ -106,5 +138,10 @@ if (!empty($cat_ids)) {
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 按照默认模板的结构,在按钮之后触发 after 钩子
|
||||||
|
do_action('woocommerce_after_add_to_cart_button');
|
||||||
|
?>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
Loading…
Reference in New Issue