From dac5f1ab847d048daa052ed31871aa81bd4521ee Mon Sep 17 00:00:00 2001 From: tikkhun Date: Fri, 7 Nov 2025 17:54:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=B7=BB=E5=8A=A0=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=8C=85=E6=9C=89=E6=95=88=E5=88=97=E8=A1=A8=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=AE=A1=E7=AE=97=E4=B8=8E=E5=B1=95=E7=A4=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现产品包配置界面中有效产品列表的动态计算与实时展示功能,包括: 1. 根据选择模式(include/exclude/all)动态计算有效产品 2. 添加AJAX端点计算有效产品列表 3. 在前端展示有效产品数量与列表 4. 优化产品查询包含所有目录可见性的产品 5. 添加模式切换时的界面状态更新 --- assets/js/admin.js | 94 ++++++++- .../class-yoone-product-bundles-admin.php | 187 +++++++++++++++--- includes/class-yoone-product-bundles.php | 11 +- templates/global/yoone-bundle-form.php | 9 +- 4 files changed, 261 insertions(+), 40 deletions(-) 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 += ''; + } 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 '

'; - echo ''; - echo ' '; - echo ''; - // 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('', $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 ''; + 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) {