diff --git a/assets/js/admin.js b/assets/js/admin.js
index 5dd185d..5b4ea54 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -8,6 +8,8 @@
var $addAllBtn = $('.yoone-add-all-simple-products');
var $clearBtn = $('.yoone-clear-products-list');
var $productSelect = $('select[name="yoone_bundle_allowed_products[]"]');
+ var $selectMode = $('#yoone_bundle_select_mode');
+ var $productsField = $('#yoone_bundle_products_list_field');
if (typeof YoonePBAdmin === 'undefined') {
return;
@@ -41,10 +43,44 @@
refreshTerms(val);
});
+ function updateProductsListState() {
+ var mode = $selectMode.val();
+ var isAll = (mode === 'all');
+ // 在 All 模式下,隐藏整个选择框字段;其它模式显示并更新标签文案
+ if ($productsField.length) {
+ $productsField.toggle(!isAll);
+ var $label = $productsField.find('> label');
+ if ($label.length) {
+ $label.text(mode === 'exclude' ? 'Excluded Products' : 'Included Products');
+ }
+ var $desc = $productsField.find('.yoone-bundle-list-desc');
+ if ($desc.length) {
+ if (mode === 'exclude') {
+ $desc.text('Simple products only. In Exclude mode, ALL published simple products are available EXCEPT the listed ones.');
+ } else {
+ $desc.text('Simple products only. In Include mode, ONLY the listed products will be available.');
+ }
+ }
+ }
+ // 额外:在 All 模式下禁用按钮与选择框(避免误操作)
+ $productSelect.prop('disabled', isAll);
+ $addAllBtn.prop('disabled', isAll);
+ }
+ if ($selectMode.length) {
+ updateProductsListState();
+ $selectMode.on('change', updateProductsListState);
+ }
+
// Add all simple products to the products select
if ($addAllBtn.length && $productSelect.length) {
$addAllBtn.on('click', function(){
var $btn = $(this);
+ var mode = $selectMode.val();
+ if (mode === 'exclude') {
+ // 在排除模式下“添加全部”会导致有效商品为空,先弹出确认。
+ var ok = window.confirm('You are in Exclude mode. Adding ALL simple products to the exclusion list will result in NO effective products. Continue?');
+ if (!ok) return;
+ }
$btn.prop('disabled', true);
$productSelect.prop('disabled', true);
$.post(YoonePBAdmin.ajax_url, {
@@ -82,4 +118,60 @@
});
}
});
-})(jQuery);
\ No newline at end of file
+})(jQuery);
+ function renderEffectiveList(items, count) {
+ var $list = $('#yoone_effective_products_list');
+ var $count = $('.yoone-effective-count');
+ if (!$list.length || !$count.length) return;
+ $count.text(count || 0);
+ var html = '';
+ if (items && items.length) {
+ html += '
';
+ items.forEach(function(item){
+ html += '' + $('
').text(item.text).html() + ' (#' + item.id + ') ';
+ });
+ html += ' ';
+ } else {
+ html += 'No products will be available with the current configuration.
';
+ }
+ $list.html(html);
+ }
+
+ function recomputeEffective() {
+ if (typeof YoonePBAdmin === 'undefined') return;
+ var mode = $selectMode.val();
+ var allowed = $productSelect.val() || [];
+ $.post(YoonePBAdmin.ajax_url, {
+ action: 'yoone_calc_effective_products',
+ security: YoonePBAdmin.security,
+ select_mode: mode,
+ allowed_products: allowed
+ }).done(function(resp){
+ if (resp && resp.success && resp.data) {
+ renderEffectiveList(resp.data.items || [], resp.data.count || 0);
+ }
+ });
+ }
+
+ // 初始化:根据当前模式更新显示并首次计算有效列表
+ updateProductsListState();
+ recomputeEffective();
+
+ // 事件:模式切换与列表变更时重新计算
+ if ($selectMode.length) {
+ $selectMode.on('change', function(){
+ updateProductsListState();
+ recomputeEffective();
+ });
+ }
+ if ($productSelect.length) {
+ $productSelect.on('change', recomputeEffective);
+ }
+
+ // 刷新按钮:手动触发重新计算
+ var $refresh = $('.yoone-refresh-effective-products');
+ if ($refresh.length) {
+ $refresh.on('click', function(){
+ recomputeEffective();
+ });
+ }
\ No newline at end of file
diff --git a/includes/admin/class-yoone-product-bundles-admin.php b/includes/admin/class-yoone-product-bundles-admin.php
index 55cfdff..36504c0 100644
--- a/includes/admin/class-yoone-product-bundles-admin.php
+++ b/includes/admin/class-yoone-product-bundles-admin.php
@@ -24,6 +24,8 @@ class Yoone_Product_Bundles_Admin {
// AJAX: 获取所有 simple product
add_action('wp_ajax_yoone_get_all_simple_products', array($this, 'ajax_get_all_simple_products'));
+ // AJAX: 动态计算有效产品列表(根据选择模式与当前列表,不依赖已保存的配置)
+ add_action('wp_ajax_yoone_calc_effective_products', array($this, 'ajax_calc_effective_products'));
// AJAX: 根据所选 taxonomy 获取术语列表
add_action('wp_ajax_yoone_get_taxonomy_terms', array($this, 'ajax_get_taxonomy_terms'));
@@ -44,8 +46,23 @@ class Yoone_Product_Bundles_Admin {
public function render_product_data_panel() {
global $post;
$product = wc_get_product($post->ID);
+ // 前端使用的“有效产品集合”等从 get_bundle_config 计算;但后台的选择框应显示“原始配置列表”(在 include 模式为包含列表,在 exclude 模式为排除列表,在 all 模式忽略该列表)。
$config = Yoone_Product_Bundles::get_bundle_config($product);
- $allowed = $config['allowed_products'];
+ // 读取原始配置列表(不应用 include/exclude/all 的推导),并仅保留 simple 产品ID。
+ $allowed_raw = get_post_meta($post->ID, Yoone_Product_Bundles::META_ALLOWED_PRODUCTS, true);
+ $allowed_raw = is_array($allowed_raw) ? array_values(array_map('absint', $allowed_raw)) : array();
+ if (! empty($allowed_raw)) {
+ $simple_only = array();
+ foreach ($allowed_raw as $aid) {
+ $p = wc_get_product($aid);
+ if ($p && $p->is_type('simple')) {
+ $simple_only[] = $aid;
+ }
+ }
+ $allowed_raw = $simple_only;
+ }
+ // 供渲染“有效产品总数”的计算结果
+ $effective_allowed = isset($config['allowed_products']) ? (array) $config['allowed_products'] : array();
$min_qty = $config['min_qty'];
$select_mode = isset($config['select_mode']) ? $config['select_mode'] : 'include';
$group_taxonomy = isset($config['group_taxonomy']) ? $config['group_taxonomy'] : 'product_cat';
@@ -73,24 +90,59 @@ class Yoone_Product_Bundles_Admin {
));
// Allowed/Excluded products: use Woo's product search (select2), multiple
- echo '' . esc_html__('Products List', 'yoone-product-bundles') . ' ';
- echo '' . esc_html__('Add All Simple Products', 'yoone-product-bundles') . ' ';
- echo ' ';
- echo '' . esc_html__('Clear Products List', 'yoone-product-bundles') . ' ';
- // Search only for products, not variations, to avoid errors
- echo '';
- if (! empty($allowed)) {
- foreach ($allowed as $pid) {
- $p = wc_get_product($pid);
- if ($p) {
- printf('%s ', $pid, esc_html($p->get_formatted_name()));
+ if ($select_mode !== 'all') {
+ $list_label = ($select_mode === 'exclude')
+ ? esc_html__('Excluded Products', 'yoone-product-bundles')
+ : esc_html__('Included Products', 'yoone-product-bundles');
+ echo '' . $list_label . ' ';
+ echo '' . esc_html__('Add All Simple Products', 'yoone-product-bundles') . ' ';
+ echo ' ';
+ echo '' . esc_html__('Clear Products List', 'yoone-product-bundles') . ' ';
+ // Search only for products, not variations, to avoid errors
+ echo '';
+ // 显示“原始配置列表”作为可编辑项(exclude 模式记为排除列表,include 模式记为包含列表)
+ if (! empty($allowed_raw)) {
+ foreach ($allowed_raw as $pid) {
+ $p = wc_get_product($pid);
+ if ($p) {
+ printf('%s ', $pid, esc_html($p->get_formatted_name()));
+ }
}
}
+ echo ' ';
+ // Dynamic description based on selection mode
+ if ($select_mode === 'include') {
+ echo '' . esc_html__('Simple products only. In Include mode, ONLY the listed products will be available.', 'yoone-product-bundles') . ' ';
+ } else { // exclude
+ echo '' . esc_html__('Simple products only. In Exclude mode, ALL published simple products are available EXCEPT the listed ones.', 'yoone-product-bundles') . ' ';
+ }
+ echo '
';
+ } else {
+ // All 模式:不显示选择框,仅提示说明
+ echo '
';
}
- echo ' ';
- // Dynamic description based on selection mode
- echo '' . esc_html__('Simple products only. Include: only listed; Exclude: allow all except listed; All: ignore the list.', 'yoone-product-bundles') . ' ';
- echo '
';
+
+ // 只读展示:有效产品列表与总数(基于当前配置推导)
+ echo '';
+ echo '
';
+ echo '' . esc_html__('Refresh Effective Products', 'yoone-product-bundles') . ' ';
+ echo '
';
+ echo '
' . sprintf(esc_html__('Effective products (%s):', 'yoone-product-bundles'), '' . (int) count($effective_allowed) . ' ') . '
';
+ echo '
';
+ if (! empty($effective_allowed)) {
+ echo '
';
+ foreach ($effective_allowed as $pid) {
+ $p = wc_get_product($pid);
+ if ($p) {
+ printf('%s (#%d) ', esc_html($p->get_formatted_name()), (int) $pid);
+ }
+ }
+ echo ' ';
+ } else {
+ echo '
' . esc_html__('No products will be available with the current configuration.', 'yoone-product-bundles') . '
';
+ }
+ echo '
';
+ echo '
';
// Minimum bundle quantity
woocommerce_wp_text_input(array(
@@ -158,6 +210,11 @@ class Yoone_Product_Bundles_Admin {
$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));
+ // 保存选择模式(include | exclude | all)
+ $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);
+
// 保存 grouping taxonomy
$group_taxonomy = isset($_POST['yoone_bundle_group_taxonomy']) ? sanitize_text_field($_POST['yoone_bundle_group_taxonomy']) : 'product_cat';
$group_taxonomy = in_array($group_taxonomy, array('product_cat','product_tag'), true) ? $group_taxonomy : 'product_cat';
@@ -177,23 +234,97 @@ class Yoone_Product_Bundles_Admin {
}
check_ajax_referer('yoone-bundle-admin-nonce', 'security');
- $products = wc_get_products(array(
- 'type' => 'simple',
- 'status' => 'publish',
- 'limit' => -1,
+ // 返回所有已发布的 simple 产品;包含任何目录可见性(visible/hidden/exclude-from-catalog/search)
+ $product_ids = wc_get_products(array(
+ 'type' => array('simple'),
+ 'status' => array('publish'),
+ 'catalog_visibility' => 'any',
+ 'limit' => -1,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ 'return' => 'ids',
));
$results = array();
- foreach ($products as $product) {
- $results[] = array(
- 'id' => $product->get_id(),
- 'text' => $product->get_formatted_name(),
- );
+ if (is_array($product_ids)) {
+ foreach ($product_ids as $pid) {
+ $product = wc_get_product($pid);
+ if ($product) {
+ $results[] = array(
+ 'id' => (int) $pid,
+ 'text' => $product->get_formatted_name(),
+ );
+ }
+ }
}
wp_send_json_success($results);
}
+ /**
+ * AJAX: 动态计算有效产品列表与总数
+ * 输入:select_mode, allowed_products[](原始配置列表,不应用推导)
+ * 输出:{ items: [{id,text}], count: number }
+ */
+ public function ajax_calc_effective_products() {
+ if (! current_user_can('edit_products')) {
+ wp_send_json_error('permission_denied', 403);
+ }
+ check_ajax_referer('yoone-bundle-admin-nonce', 'security');
+
+ $select_mode = isset($_POST['select_mode']) ? sanitize_text_field($_POST['select_mode']) : 'include';
+ $select_mode = in_array($select_mode, array('include','exclude','all'), true) ? $select_mode : 'include';
+ $allowed_raw = isset($_POST['allowed_products']) ? (array) $_POST['allowed_products'] : array();
+ $allowed_raw = array_values(array_filter(array_map('absint', $allowed_raw)));
+ // 仅保留 simple 产品ID
+ if (! empty($allowed_raw)) {
+ $simple_only = array();
+ foreach ($allowed_raw as $aid) {
+ $p = wc_get_product($aid);
+ if ($p && $p->is_type('simple')) {
+ $simple_only[] = $aid;
+ }
+ }
+ $allowed_raw = $simple_only;
+ }
+
+ $effective_ids = array();
+ if ($select_mode === 'include') {
+ $effective_ids = $allowed_raw;
+ } else {
+ // exclude/all: 先取全部 simple 产品,再按模式处理
+ $all_ids = wc_get_products(array(
+ 'type' => array('simple'),
+ 'status' => array('publish'),
+ 'catalog_visibility' => 'any',
+ 'limit' => -1,
+ 'return' => 'ids',
+ ));
+ $all_ids = is_array($all_ids) ? array_values(array_map('absint', $all_ids)) : array();
+ if ($select_mode === 'all') {
+ $effective_ids = $all_ids;
+ } else { // exclude
+ $effective_ids = array_values(array_diff($all_ids, $allowed_raw));
+ }
+ }
+
+ $items = array();
+ foreach ($effective_ids as $pid) {
+ $p = wc_get_product($pid);
+ if ($p) {
+ $items[] = array(
+ 'id' => (int) $pid,
+ 'text' => $p->get_formatted_name(),
+ );
+ }
+ }
+
+ wp_send_json_success(array(
+ 'items' => $items,
+ 'count' => count($items),
+ ));
+ }
+
/**
* AJAX: 根据选择的 taxonomy 返回术语列表
*/
@@ -238,8 +369,4 @@ class Yoone_Product_Bundles_Admin {
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);
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/includes/class-yoone-product-bundles.php b/includes/class-yoone-product-bundles.php
index 11fa1f6..b4ae1fa 100644
--- a/includes/class-yoone-product-bundles.php
+++ b/includes/class-yoone-product-bundles.php
@@ -83,12 +83,13 @@ class Yoone_Product_Bundles {
}
// 根据选择模式计算最终 allowed 列表
if ($select_mode === 'all' || $select_mode === 'exclude') {
- // 获取所有 simple 产品ID
+ // 获取所有 simple 产品ID(包含任何目录可见性)
$all_ids = wc_get_products(array(
- 'type' => array('simple'),
- 'status' => array('publish'),
- 'limit' => -1,
- 'return' => 'ids',
+ 'type' => array('simple'),
+ 'status' => array('publish'),
+ 'catalog_visibility' => 'any',
+ 'limit' => -1,
+ 'return' => 'ids',
));
$all_ids = is_array($all_ids) ? array_values(array_map('absint', $all_ids)) : array();
if ($select_mode === 'all') {
diff --git a/templates/global/yoone-bundle-form.php b/templates/global/yoone-bundle-form.php
index 6b5f95f..b74b00f 100644
--- a/templates/global/yoone-bundle-form.php
+++ b/templates/global/yoone-bundle-form.php
@@ -32,10 +32,11 @@ foreach ($allowed as $pid) {
// 兜底:如果为 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',
+ 'type' => array('simple'),
+ 'status' => array('publish'),
+ 'catalog_visibility' => 'any',
+ 'limit' => -1,
+ 'return' => 'ids',
));
if (is_array($all_ids)) {
foreach ($all_ids as $pid) {