批次編輯商品屬性

WooCommerce 後台清單頁,上方可用分類/標籤篩選;提供母子式下拉選單,批次為勾選商品加入指定屬性與屬性項目(若無該屬性則先建立並排在最後)。列表移除SKU欄位;每個全域屬性各自成欄顯示;屬性類別以 slug 升冪排列;欄位標題可換行;外層視需要允許水平滾動。

<?php
/**
 * Plugin Name: WC Attribute Bulk Manager (Per-Attribute Columns, Wrapped Headers)
 * Description: WooCommerce 後台清單頁,上方可用分類/標籤篩選;提供母子式下拉選單,批次為勾選商品加入指定屬性與屬性項目(若無該屬性則先建立並排在最後)。列表移除SKU欄位;每個全域屬性各自成欄顯示;屬性類別以 slug 升冪排列;欄位標題可換行;外層視需要允許水平滾動。
 * Version:     1.2.0
 * Author:      your-name
 * License:     GPLv2 or later
 */
 
if ( ! defined( 'ABSPATH' ) ) exit;
 
class WC_Attribute_Bulk_Manager {
    const SLUG = 'wc-attribute-bulk-manager';
    const NONCE_ACTION = 'wc_abm_action';
    const NONCE_FIELD  = 'wc_abm_nonce';
 
    public function __construct() {
        add_action( 'admin_menu', [ $this, 'add_menu' ], 99 );
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
        add_action( 'wp_ajax_wc_abm_get_terms', [ $this, 'ajax_get_terms' ] );
        add_action( 'admin_post_wc_abm_apply', [ $this, 'handle_apply' ] );
    }
 
    public function add_menu() {
        add_submenu_page(
            'woocommerce',
            '屬性批次管理',
            '屬性批次管理',
            'manage_woocommerce',
            self::SLUG,
            [ $this, 'render_page' ]
        );
    }
 
    public function enqueue( $hook ) {
        if ( $hook !== 'woocommerce_page_' . self::SLUG ) return;
 
        wp_enqueue_style( 'wc-abm-admin', plugins_url( 'wc-abm.css', __FILE__ ), [], '1.2.0' );
        wp_enqueue_script( 'wc-abm-admin', plugins_url( 'wc-abm.js', __FILE__ ), [ 'jquery' ], '1.2.0', true );
        wp_localize_script( 'wc-abm-admin', 'WCABM', [
            'ajaxurl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( self::NONCE_ACTION ),
        ] );
 
        // 样式:維持欄寬行為與前版一致;表頭可換行;必要時可水平滾動
        $css = '
        .wc-abm-wrap .filters, .wc-abm-wrap .bulk-actions { display:flex; gap:12px; align-items:center; margin: 12px 0; flex-wrap: wrap;}
        .wc-abm-table-wrap { overflow-x: auto; } /* 必要時水平滾動 */
        .wc-abm-table{width:100%; border-collapse: collapse; background:#fff; min-width: 960px;} /* 保留較舒適的最小寬 */
        .wc-abm-table th, .wc-abm-table td{border:1px solid #ddd; padding:8px; vertical-align: top;}
        .wc-abm-table th{background:#f6f7f7; white-space: normal; word-break: break-word; line-height:1.3;} /* 表頭允許換行 */
        .wc-abm-table td{ white-space: normal; } /* 內容維持不換行(與前版一致) */
        .term-chip{display:inline-block; border:1px solid #ddd; padding:2px 6px; margin:2px 4px 2px 0; border-radius:4px; font-size:12px; background:#fbfbfb; white-space:nowrap;}
        .wc-abm-pagination{margin-top:12px;}
        ';
        wp_add_inline_style( 'wc-abm-admin', $css );
 
        $js = "
        jQuery(function($){
          function loadTerms(){
            var attr = $('#wc-abm-attr').val();
            var \$child = $('#wc-abm-term');
            \$child.html('<option value=\"\">載入中...</option>');
            $.post(WCABM.ajaxurl, {
              action:'wc_abm_get_terms',
              attr: attr,
              _ajax_nonce: WCABM.nonce
            }, function(resp){
              if(resp && resp.success){
                var opts = '<option value=\"\">請選擇屬性項目</option>';
                resp.data.forEach(function(t){
                  opts += '<option value=\"'+t.term_id+'\">'+t.name+'</option>';
                });
                \$child.html(opts);
              }else{
                \$child.html('<option value=\"\">沒有項目</option>');
              }
            });
          }
          $('#wc-abm-attr').on('change', loadTerms);
 
          $('#wc-abm-checkall').on('change', function(){
            $('.wc-abm-rowcheck').prop('checked', this.checked);
          });
        });
        ";
        wp_add_inline_script( 'wc-abm-admin', $js );
    }
 
    // 子選單:維持預設順序(不再強制以項目 slug 排序)
    public function ajax_get_terms() {
        check_ajax_referer( self::NONCE_ACTION );
        if ( ! current_user_can( 'manage_woocommerce' ) ) wp_send_json_error();
 
        $attr = isset($_POST['attr']) ? sanitize_text_field($_POST['attr']) : '';
        if ( empty($attr) ) wp_send_json_success( [] );
 
        $taxonomy = wc_attribute_taxonomy_name( $attr ); // 'color' -> 'pa_color'
        if ( ! taxonomy_exists( $taxonomy ) ) {
            if ( taxonomy_exists( $attr ) ) $taxonomy = $attr;
            else wp_send_json_success( [] );
        }
 
        $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false ] );
        if ( is_wp_error($terms) ) $terms = [];
 
        $out = [];
        foreach ( $terms as $t ) {
            $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
        }
        wp_send_json_success( $out );
    }
 
    public function render_page() {
        if ( ! class_exists( 'WooCommerce' ) ) {
            echo '<div class="notice notice-error"><p>請先啟用 WooCommerce。</p></div>';
            return;
        }
 
        // 篩選參數
        $cat   = isset($_GET['product_cat']) ? sanitize_text_field($_GET['product_cat']) : '';
        $tag   = isset($_GET['product_tag']) ? sanitize_text_field($_GET['product_tag']) : '';
        $paged = max(1, isset($_GET['paged']) ? intval($_GET['paged']) : 1 );
        $per_page = 20;
 
        $tax_query = [];
        if ( $cat ) $tax_query[] = [ 'taxonomy'=>'product_cat','field'=>'slug','terms'=>$cat ];
        if ( $tag ) $tax_query[] = [ 'taxonomy'=>'product_tag','field'=>'slug','terms'=>$tag ];
        if ( count($tax_query) > 1 ) $tax_query['relation'] = 'AND';
 
        $q = new WP_Query( [
            'post_type'      => 'product',
            'post_status'    => [ 'publish','private','draft' ],
            'posts_per_page' => $per_page,
            'paged'          => $paged,
            'tax_query'      => $tax_query,
            'orderby'        => 'ID',
            'order'          => 'DESC',
        ] );
 
        $total_pages = max(1, $q->max_num_pages);
 
        // 全域屬性(做為欄位與母選單來源)——以「屬性 slug(attribute_name)」升冪排序
        $attribute_taxonomies = wc_get_attribute_taxonomies(); // object[]: attribute_id, attribute_name, attribute_label
        usort($attribute_taxonomies, function($a,$b){
            return strnatcasecmp($a->attribute_name, $b->attribute_name);
        });
 
        // 母選單 options(依屬性 slug 升冪)
        $attr_options = '';
        if ( $attribute_taxonomies ) {
            foreach ( $attribute_taxonomies as $at ) {
                $label = $at->attribute_label ? $at->attribute_label : $at->attribute_name;
                $attr_options .= sprintf(
                    '<option value="%s">%s (%s)</option>',
                    esc_attr( $at->attribute_name ),
                    esc_html( $label ),
                    esc_html( 'pa_' . $at->attribute_name )
                );
            }
        }
 
        // 篩選下拉
        $cat_dd = wp_dropdown_categories( [
            'taxonomy'        => 'product_cat',
            'name'            => 'product_cat',
            'orderby'         => 'name',
            'hide_empty'      => false,
            'hierarchical'    => true,
            'show_option_all' => '全部分類',
            'value_field'     => 'slug',
            'selected'        => $cat,
            'echo'            => 0,
        ] );
        $tag_terms = get_terms( [ 'taxonomy' => 'product_tag', 'hide_empty' => false ] );
        $tag_dd = '<select name="product_tag"><option value="">全部標籤</option>';
        if ( ! is_wp_error( $tag_terms ) ) {
            foreach ( $tag_terms as $t ) {
                $tag_dd .= sprintf(
                    '<option value="%s"%s>%s</option>',
                    esc_attr( $t->slug ),
                    selected( $tag, $t->slug, false ),
                    esc_html( $t->name )
                );
            }
        }
        $tag_dd .= '</select>';
 
        echo '<div class="wrap wc-abm-wrap">';
        echo '<h1 class="wp-heading-inline">屬性批次管理</h1>';
 
        // 篩選列(GET)
        echo '<form method="get" class="filters">';
        echo '<input type="hidden" name="page" value="'.esc_attr(self::SLUG).'"/>';
        echo $cat_dd;
        echo $tag_dd;
        submit_button( '篩選', 'secondary', '', false );
        echo '</form>';
 
        // 列表 + 批次操作(POST)
        echo '<form method="post" action="'. esc_url( admin_url('admin-post.php') ) .'">';
        wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
        echo '<input type="hidden" name="action" value="wc_abm_apply"/>';
 
        // 保留原過濾與頁碼(提交後帶回)
        echo '<input type="hidden" name="_abm_keep_cat" value="'.esc_attr($cat).'"/>';
        echo '<input type="hidden" name="_abm_keep_tag" value="'.esc_attr($tag).'"/>';
        echo '<input type="hidden" name="_abm_keep_paged" value="'.esc_attr($paged).'"/>';
 
        // 批次操作(母子式)
        echo '<div class="bulk-actions">';
        echo '<label>屬性(母選單):</label>';
        echo '<select id="wc-abm-attr" name="attr" required>';
        echo '<option value="">請選擇屬性</option>';
        echo $attr_options ?: '<option value="">(尚未建立任何全域屬性)</option>';
        echo '</select>';
 
        echo '<label>屬性項目(子選單):</label>';
        echo '<select id="wc-abm-term" name="term_id" required>';
        echo '<option value="">請先選擇屬性</option>';
        echo '</select>';
 
        submit_button( '套用至勾選商品', 'primary', 'wc-abm-apply', false, [ 'style' => 'margin-left:8px;' ] );
        echo '</div>';
 
        // 表格外層:必要時水平滾動
        echo '<div class="wc-abm-table-wrap">';
 
        // 表頭:勾選、ID、商品名稱 + 「每個屬性一欄」(屬性以 slug 升冪)
        echo '<table class="widefat fixed striped wc-abm-table">';
        echo '<thead><tr>';
        echo '<th style="width:32px;"><input type="checkbox" id="wc-abm-checkall"/></th>';
        echo '<th style="width:80px;">ID</th>';
        echo '<th>商品名稱</th>';
        if ( $attribute_taxonomies ) {
            foreach ( $attribute_taxonomies as $at ) {
                $label = $at->attribute_label ?: $at->attribute_name;
                echo '<th>'. esc_html( $label ) .'</th>'; // 表頭允許換行
            }
        }
        echo '</tr></thead><tbody>';
 
        if ( $q->have_posts() ) {
            while ( $q->have_posts() ) { $q->the_post();
                $product = wc_get_product( get_the_ID() );
                if ( ! $product ) continue;
 
                $id  = $product->get_id();
 
                echo '<tr>';
                echo '<td><input type="checkbox" class="wc-abm-rowcheck" name="product_ids[]" value="'.esc_attr($id).'"/></td>';
                echo '<td>'.esc_html($id).'</td>';
                echo '<td><a href="'.esc_url( get_edit_post_link($id) ).'">'.esc_html( get_the_title() ).'</a></td>';
 
                if ( $attribute_taxonomies ) {
                    foreach ( $attribute_taxonomies as $at ) {
                        $taxonomy = wc_attribute_taxonomy_name( $at->attribute_name ); // pa_x
                        echo '<td>'.$this->render_terms_chips( $id, $taxonomy ).'</td>';
                    }
                }
 
                echo '</tr>';
            }
        } else {
            $colspan = 3 + ( $attribute_taxonomies ? count($attribute_taxonomies) : 0 );
            echo '<tr><td colspan="'.intval($colspan).'">查無商品。</td></tr>';
        }
        wp_reset_postdata();
 
        echo '</tbody></table>';
        echo '</div>'; // .wc-abm-table-wrap
 
        // 分頁
        if ( $total_pages > 1 ) {
            $base_url = remove_query_arg( 'paged' );
            $base_url = add_query_arg( [
                'page'        => self::SLUG,
                'product_cat' => $cat,
                'product_tag' => $tag,
            ], admin_url('admin.php') );
            echo '<div class="tablenav wc-abm-pagination"><div class="tablenav-pages">';
            echo paginate_links( [
                'base'      => add_query_arg( 'paged', '%#%', $base_url ),
                'format'    => '',
                'prev_text' => '«',
                'next_text' => '»',
                'current'   => $paged,
                'total'     => $total_pages,
            ] );
            echo '</div></div>';
        }
 
        echo '</form>';
        echo '</div><!-- .wrap -->';
    }
 
    // 列表每格:維持不換行;項目順序維持預設(不強制以項目 slug 排序)
    private function render_terms_chips( int $product_id, string $taxonomy ) : string {
        if ( ! taxonomy_exists( $taxonomy ) ) return '—';
        $terms = get_the_terms( $product_id, $taxonomy );
        if ( is_wp_error($terms) || empty($terms) ) return '—';
 
        $html = '';
        foreach ( $terms as $t ) {
            $html .= '<span class="term-chip">'. esc_html($t->name) .'</span>';
        }
        return $html ?: '—';
    }
 
    public function handle_apply() {
        if ( ! current_user_can( 'manage_woocommerce' ) ) wp_die( '權限不足' );
        check_admin_referer( self::NONCE_ACTION, self::NONCE_FIELD );
 
        $product_ids = isset($_POST['product_ids']) ? array_map('intval', (array)$_POST['product_ids']) : [];
        $attr_key    = isset($_POST['attr']) ? sanitize_text_field($_POST['attr']) : '';   // 可能 'color' 或 'pa_color'
        $term_id     = isset($_POST['term_id']) ? intval($_POST['term_id']) : 0;
 
        // 保留原過濾與頁碼
        $keep_cat   = isset($_POST['_abm_keep_cat']) ? sanitize_text_field($_POST['_abm_keep_cat']) : '';
        $keep_tag   = isset($_POST['_abm_keep_tag']) ? sanitize_text_field($_POST['_abm_keep_tag']) : '';
        $keep_paged = isset($_POST['_abm_keep_paged']) ? intval($_POST['_abm_keep_paged']) : 1;
 
        if ( empty($product_ids) || empty($attr_key) || empty($term_id) ) {
            return $this->redirect_with_msg( '缺少必要參數或未勾選商品。', 'error', $keep_cat, $keep_tag, $keep_paged );
        }
 
        // 正規化 taxonomy
        $taxonomy = taxonomy_exists( 'pa_' . $attr_key ) ? ( 'pa_' . $attr_key ) : $attr_key;
        if ( ! taxonomy_exists( $taxonomy ) ) {
            return $this->redirect_with_msg( '指定的屬性 taxonomy 不存在:' . esc_html( $taxonomy ), 'error', $keep_cat, $keep_tag, $keep_paged );
        }
 
        // 對應 attribute_id(用於 WC_Product_Attribute)
        $attr_id = 0;
        $all_attr = wc_get_attribute_taxonomies();
        if ( $all_attr ) {
            foreach ( $all_attr as $a ) {
                $tax_name = wc_attribute_taxonomy_name( $a->attribute_name ); // pa_x
                if ( $tax_name === $taxonomy ) { $attr_id = intval( $a->attribute_id ); break; }
            }
        }
 
        $success = 0; $fail = 0;
        foreach ( $product_ids as $pid ) {
            $ok = $this->apply_to_product( $pid, $taxonomy, $attr_id, $term_id );
            if ( $ok ) $success++; else $fail++;
        }
 
        $msg = sprintf( '套用完成:成功 %d 筆,失敗 %d 筆。', $success, $fail );
        return $this->redirect_with_msg( $msg, $fail ? 'warning' : 'updated', $keep_cat, $keep_tag, $keep_paged );
    }
 
    private function apply_to_product( int $product_id, string $taxonomy, int $attr_id, int $term_id ) : bool {
        try {
            $product = wc_get_product( $product_id );
            if ( ! $product ) return false;
 
            // 1) 合併指派 term
            $current_terms = wp_get_object_terms( $product_id, $taxonomy, [ 'fields' => 'ids' ] );
            if ( is_wp_error( $current_terms ) ) $current_terms = [];
            if ( ! in_array( $term_id, $current_terms, true ) ) {
                $merged = array_unique( array_filter( array_merge( $current_terms, [ $term_id ] ) ) );
                wp_set_object_terms( $product_id, $merged, $taxonomy, false );
            }
 
            // 2) 確保 product attributes 內存在該屬性;若無→新增且排最後
            $attributes = $product->get_attributes(); // WC_Product_Attribute[]
            $found_key = null;
            foreach ( $attributes as $key => $attr ) {
                if ( $attr->get_name() === $taxonomy ) { $found_key = $key; break; }
            }
 
            if ( is_null( $found_key ) ) {
                $new = new WC_Product_Attribute();
                if ( $attr_id ) $new->set_id( $attr_id );
                $new->set_name( $taxonomy );
                $new->set_visible( true );
                $new->set_variation( false );
                $new->set_options( [ $term_id ] );
 
                $max_position = 0;
                foreach ( $attributes as $a ) {
                    $max_position = max( $max_position, intval( $a->get_position() ) );
                }
                $new->set_position( $max_position + 1 );
                $attributes[] = $new;
            } else {
                /** @var WC_Product_Attribute $exist */
                $exist = $attributes[ $found_key ];
                $opts  = $exist->is_taxonomy()
                    ? array_map('intval', (array)$exist->get_options())
                    : (array)$exist->get_options();
 
                if ( ! in_array( $term_id, $opts, true ) ) {
                    $opts[] = $term_id;
                    $exist->set_options( $opts );
                }
                $attributes[ $found_key ] = $exist;
            }
 
            $product->set_attributes( $attributes );
            $product->save();
            wc_delete_product_transients( $product_id );
            clean_post_cache( $product_id );
 
            return true;
        } catch ( Throwable $e ) {
            error_log( '[WC ABM] Failed on product '. $product_id .': '. $e->getMessage() );
            return false;
        }
    }
 
    private function redirect_with_msg( string $msg, string $type = 'updated', string $keep_cat = '', string $keep_tag = '', int $keep_paged = 1 ) {
        $args = [
            'page'        => self::SLUG,
            'abmmsg'      => rawurlencode( $msg ),
            'abmtype'     => $type,
        ];
        if ( $keep_cat )   $args['product_cat'] = $keep_cat;
        if ( $keep_tag )   $args['product_tag'] = $keep_tag;
        if ( $keep_paged && $keep_paged > 1 ) $args['paged'] = $keep_paged;
 
        $url = add_query_arg( $args, admin_url( 'admin.php' ) );
        wp_safe_redirect( $url );
        exit;
    }
}
 
new WC_Attribute_Bulk_Manager();
 
add_action( 'admin_notices', function(){
    if ( ! is_admin() ) return;
    if ( ! isset($_GET['page']) || $_GET['page'] !== WC_Attribute_Bulk_Manager::SLUG ) return;
    if ( empty($_GET['abmmsg']) ) return;
 
    $type = isset($_GET['abmtype']) ? sanitize_text_field($_GET['abmtype']) : 'updated';
    $msg  = wp_kses_post( wp_unslash( $_GET['abmmsg'] ) );
    printf( '<div class="notice notice-%s is-dismissible"><p>%s</p></div>', esc_attr($type), $msg );
});

wc-attribute-bulk-manager

在〈批次編輯商品屬性〉中有 1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料