372 lines
19 KiB
PHP
372 lines
19 KiB
PHP
<?php
|
||
/**
|
||
* Admin: Provides the configuration interface for bundle products (allowed products, min quantity, categories).
|
||
*/
|
||
defined('ABSPATH') || exit;
|
||
|
||
class Yoone_Product_Bundles_Admin {
|
||
protected static $instance = null;
|
||
|
||
public static function instance() {
|
||
if (null === self::$instance) self::$instance = new self();
|
||
return self::$instance;
|
||
}
|
||
|
||
private function __construct() {
|
||
// 添加一个产品数据标签页
|
||
add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tab'));
|
||
// 对应的面板内容
|
||
add_action('woocommerce_product_data_panels', array($this, 'render_product_data_panel'));
|
||
// 保存配置
|
||
add_action('woocommerce_admin_process_product_meta', array($this, 'save_product_meta'));
|
||
add_action('woocommerce_process_product_meta', array($this, 'save_product_meta'));
|
||
add_action('save_post_product', array($this, 'save_product_meta'), 15);
|
||
|
||
// 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'));
|
||
|
||
// 后台资源
|
||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
|
||
}
|
||
|
||
public function add_product_data_tab($tabs) {
|
||
$tabs['yoone_bundle'] = array(
|
||
'label' => __('Mix and Match', 'yoone-product-bundles'),
|
||
'target' => 'yoone_bundle_data',
|
||
'class' => array('show_if_yoone_bundle'), // Only show for 'yoone_bundle' type
|
||
'priority' => 70,
|
||
);
|
||
return $tabs;
|
||
}
|
||
|
||
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);
|
||
// 读取原始配置列表(不应用 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';
|
||
$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">';
|
||
wp_nonce_field('yoone-bundle-admin-nonce', 'yoone_bundle_admin_nonce_field');
|
||
|
||
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>';
|
||
|
||
// Selection mode
|
||
woocommerce_wp_select(array(
|
||
'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
|
||
if ($select_mode !== 'all') {
|
||
$list_label = ($select_mode === 'exclude')
|
||
? esc_html__('Excluded Products', 'yoone-product-bundles')
|
||
: esc_html__('Included Products', 'yoone-product-bundles');
|
||
echo '<p class="form-field" id="yoone_bundle_products_list_field"><label>' . $list_label . '</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
|
||
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">';
|
||
// 显示“原始配置列表”作为可编辑项(exclude 模式记为排除列表,include 模式记为包含列表)
|
||
if (! empty($allowed_raw)) {
|
||
foreach ($allowed_raw as $pid) {
|
||
$p = wc_get_product($pid);
|
||
if ($p) {
|
||
printf('<option value="%d" selected>%s</option>', $pid, esc_html($p->get_formatted_name()));
|
||
}
|
||
}
|
||
}
|
||
echo '</select>';
|
||
// Dynamic description based on selection mode
|
||
if ($select_mode === 'include') {
|
||
echo '<span class="description yoone-bundle-list-desc">' . esc_html__('Simple products only. In Include mode, ONLY the listed products will be available.', 'yoone-product-bundles') . '</span>';
|
||
} else { // exclude
|
||
echo '<span class="description yoone-bundle-list-desc">' . esc_html__('Simple products only. In Exclude mode, ALL published simple products are available EXCEPT the listed ones.', 'yoone-product-bundles') . '</span>';
|
||
}
|
||
echo '</p>';
|
||
} else {
|
||
// All 模式:不显示选择框,仅提示说明
|
||
echo '<p class="form-field" id="yoone_bundle_products_list_field" style="display:none"></p>';
|
||
}
|
||
|
||
// 只读展示:有效产品列表与总数(基于当前配置推导)
|
||
echo '<div id="yoone_effective_products_block" class="yoone-effective-products" style="margin-top:10px;">';
|
||
echo '<div class="yoone-effective-actions" style="margin-bottom:6px;">';
|
||
echo '<button type="button" class="button yoone-refresh-effective-products">' . esc_html__('Refresh Effective Products', 'yoone-product-bundles') . '</button>';
|
||
echo '</div>';
|
||
echo '<p>' . sprintf(esc_html__('Effective products (%s):', 'yoone-product-bundles'), '<span class="yoone-effective-count">' . (int) count($effective_allowed) . '</span>') . '</p>';
|
||
echo '<div id="yoone_effective_products_list">';
|
||
if (! empty($effective_allowed)) {
|
||
echo '<ul class="yoone-effective-list" style="max-height:220px; overflow:auto; margin:0; padding-left:1.5em; border:1px solid #ddd; background:#fff;">';
|
||
foreach ($effective_allowed as $pid) {
|
||
$p = wc_get_product($pid);
|
||
if ($p) {
|
||
printf('<li>%s <span style="color:#999;">(#%d)</span></li>', esc_html($p->get_formatted_name()), (int) $pid);
|
||
}
|
||
}
|
||
echo '</ul>';
|
||
} else {
|
||
echo '<p class="description">' . esc_html__('No products will be available with the current configuration.', 'yoone-product-bundles') . '</p>';
|
||
}
|
||
echo '</div>';
|
||
echo '</div>';
|
||
|
||
// Minimum bundle quantity
|
||
woocommerce_wp_text_input(array(
|
||
'id' => 'yoone_bundle_min_quantity',
|
||
'label' => __('Minimum Quantity', 'yoone-product-bundles'),
|
||
'type' => 'number',
|
||
'desc_tip' => true,
|
||
'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'),
|
||
));
|
||
|
||
// Grouping taxonomy (product_cat or product_tag)
|
||
woocommerce_wp_select(array(
|
||
'id' => 'yoone_bundle_group_taxonomy',
|
||
'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) {
|
||
$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));
|
||
}
|
||
echo '</select>';
|
||
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 '</div>'; // options_group
|
||
echo '</div>'; // panel
|
||
}
|
||
|
||
public function save_product_meta($post_id) {
|
||
// 无论当前 product 对象类型为何,只要提交了我们的字段,就进行保存。
|
||
// 这可以避免在首次切换产品类型时由于保存顺序问题导致配置未写入。
|
||
$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;
|
||
|
||
// 保存 allowed products
|
||
$allowed = isset($_POST['yoone_bundle_allowed_products']) ? (array) $_POST['yoone_bundle_allowed_products'] : array();
|
||
$allowed = array_values(array_filter(array_map('absint', $allowed)));
|
||
// 仅保留 simple 产品ID
|
||
if (! empty($allowed)) {
|
||
$simple_only = array();
|
||
foreach ($allowed as $aid) {
|
||
$p = wc_get_product($aid);
|
||
if ($p && $p->is_type('simple')) {
|
||
$simple_only[] = $aid;
|
||
}
|
||
}
|
||
$allowed = $simple_only;
|
||
}
|
||
update_post_meta($post_id, Yoone_Product_Bundles::META_ALLOWED_PRODUCTS, $allowed);
|
||
|
||
// 保存 min qty
|
||
$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';
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* AJAX handler: 获取所有已发布的 simple product
|
||
*/
|
||
public function ajax_get_all_simple_products() {
|
||
if (! current_user_can('edit_products')) {
|
||
wp_send_json_error('permission_denied', 403);
|
||
}
|
||
check_ajax_referer('yoone-bundle-admin-nonce', 'security');
|
||
|
||
// 返回所有已发布的 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();
|
||
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 返回术语列表
|
||
*/
|
||
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);
|
||
}
|
||
} |