__('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] ); } /** * Build WP_Query args from instance + page. * * @param array $instance * @param int $paged * @return array */ public static function build_query_args(array $instance, int $paged = 1): array { $per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8))); $orderby = sanitize_key($instance['orderby'] ?? 'date'); $order = in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $instance['order']) : 'DESC'; $cat_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? ''))))); $tag_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? ''))))); $discount_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? ''))))); $exclude_cat = !empty($instance['exclude_cat']); $query_args = [ 'post_type' => 'aksrt_product', 'post_status' => 'publish', 'posts_per_page' => $per_page, 'paged' => max(1, $paged), 'orderby' => $orderby === 'sales' ? 'meta_value_num' : ($orderby === 'views' ? 'meta_value_num' : $orderby), 'order' => $order, 'ignore_sticky_posts' => true, ]; if ($orderby === 'sales') { $query_args['meta_key'] = '_aksrt_sales'; } elseif ($orderby === 'views') { $query_args['meta_key'] = 'aksrt_post_views'; } $tax_query = []; if (!empty($cat_ids)) { $tax_query[] = [ 'taxonomy' => 'aksrt_product_cat', 'field' => 'term_id', 'terms' => $cat_ids, 'operator' => $exclude_cat ? 'NOT IN' : 'IN', ]; } if (!empty($tag_ids)) { $tax_query[] = [ 'taxonomy' => 'aksrt_product_tag', 'field' => 'term_id', 'terms' => $tag_ids, ]; } if (!empty($discount_ids)) { $tax_query[] = [ 'taxonomy' => 'shop_discount', 'field' => 'term_id', 'terms' => $discount_ids, ]; } if (!empty($tax_query)) { $query_args['tax_query'] = $tax_query; } return $query_args; } /** * Render product cards HTML for a query. */ public static function render_items_html(\WP_Query $products, string $style = 'default'): string { if (!$products->have_posts()) { return ''; } ob_start(); while ($products->have_posts()) { $products->the_post(); $pid = get_the_ID(); $card = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid) : []; if (function_exists('get_template_part') && !empty($card['id'])) { get_template_part('templates/parts/shop/product', 'card', [ 'product' => $card, 'show_sales' => true, ]); continue; } // Fallback mini card. $price = (float) ($card['price'] ?? 0); $image = (string) ($card['image'] ?? ''); echo '
'; if ($image !== '') { echo ''; echo '' . esc_attr(get_the_title($pid)) . ''; echo ''; } echo '
'; echo '' . esc_html(get_the_title($pid)) . ''; echo '
'; echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; if (!empty($card['sales'])) { echo '' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . ''; } echo '
'; } wp_reset_postdata(); return (string) ob_get_clean(); } /** * Shared payload for REST widget list. * * @param array $instance * @param int $page * @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int} */ public static function query_payload(array $instance, int $page = 1): array { $page = max(1, $page); $args = self::build_query_args($instance, $page); $style = sanitize_key((string) ($instance['style'] ?? 'default')); $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); $products = new \WP_Query($args); $html = self::render_items_html($products, $style); if ($html === '' && $page === 1) { $html = function_exists('aksrt_get_ajax_null') ? aksrt_get_ajax_null(__('暂无商品', 'aksrt')) : '

' . esc_html__('暂无商品', 'aksrt') . '

'; } $max_pages = max(1, (int) $products->max_num_pages); $total = (int) $products->found_posts; $has_more = $page < $max_pages; $paginate_html = ''; if ($paginate === 'ajax' && $has_more) { $next = add_query_arg( array_filter([ 'page' => $page + 1, 'per_page' => max(1, (int) ($instance['per_page'] ?? 8)), 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), 'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')), 'cat_ids' => (string) ($instance['cat_ids'] ?? ''), 'tag_ids' => (string) ($instance['tag_ids'] ?? ''), 'discount_ids' => (string) ($instance['discount_ids'] ?? ''), 'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0, 'style' => sanitize_key((string) ($instance['style'] ?? 'default')), 'paginate' => 'ajax', ], static function ($v) { return $v !== '' && $v !== null; }), rest_url('aksrt/v1/shop/frontend/widgets/products') ); if (function_exists('aksrt_get_ajax_next_paginate')) { $paginate_html = aksrt_get_ajax_next_paginate( $total, $page, max(1, (int) ($instance['per_page'] ?? 8)), $next ); } else { $paginate_html = '
' . esc_html__('加载更多', 'aksrt') . '
'; } } elseif ($paginate === 'number' && $max_pages > 1) { $paginate_html = '
' . paginate_links([ 'base' => add_query_arg('aksrt_product_page', '%#%'), 'format' => '', 'current' => $page, 'total' => $max_pages, 'prev_text' => '«', 'next_text' => '»', ]) . '
'; } else { $paginate_html = '
'; } return [ 'html' => $html, 'paginate_html' => $paginate_html, 'has_more' => $has_more, 'page' => $page, 'max_pages' => $max_pages, 'total' => $total, ]; } public function widget($args, $instance): void { $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); $style = sanitize_key((string) ($instance['style'] ?? 'default')); $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); $page = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $cfg = [ 'per_page' => max(1, min(50, (int) ($instance['per_page'] ?? 8))), 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), 'order' => in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $instance['order']) : 'DESC', 'cat_ids' => (string) ($instance['cat_ids'] ?? ''), 'tag_ids' => (string) ($instance['tag_ids'] ?? ''), 'discount_ids' => (string) ($instance['discount_ids'] ?? ''), 'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0, 'style' => $style, 'paginate' => $paginate, ]; $payload = self::query_payload($instance, $page); $ajax_url = rest_url('aksrt/v1/shop/frontend/widgets/products'); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($title !== '') { echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } $style_class = 'aksrt-product-widget--' . $style; echo '
'; // Items sit directly under .ajaxpager so ajax-list inject/append works (zibll product-lists-row). echo '
'; echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — cards escaped upstream echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; echo '
'; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } public function form($instance): void { $defaults = [ 'title' => __('商品推荐', 'aksrt'), 'per_page' => 8, 'orderby' => 'date', 'order' => 'DESC', 'cat_ids' => '', 'tag_ids' => '', 'discount_ids' => '', 'exclude_cat' => false, 'style' => 'default', 'paginate' => 'ajax', ]; $instance = wp_parse_args($instance, $defaults); ?>

sanitize_text_field((string) ($new_instance['title'] ?? '')), 'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))), 'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')), 'order' => in_array(strtoupper((string) ($new_instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $new_instance['order']) : 'DESC', 'cat_ids' => sanitize_text_field((string) ($new_instance['cat_ids'] ?? '')), 'tag_ids' => sanitize_text_field((string) ($new_instance['tag_ids'] ?? '')), 'discount_ids' => sanitize_text_field((string) ($new_instance['discount_ids'] ?? '')), 'exclude_cat' => !empty($new_instance['exclude_cat']) ? 1 : 0, 'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')), 'paginate' => sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ]; } }