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 ); }); |
很棒的网站!感谢分享,谢谢站长!!@天天下载Ttzip