diff --git a/src/AKSRT/Admin/ShopController.php b/src/AKSRT/Admin/ShopController.php index 7bf18636..9fd48104 100644 --- a/src/AKSRT/Admin/ShopController.php +++ b/src/AKSRT/Admin/ShopController.php @@ -379,6 +379,13 @@ class ShopController extends ControllerBase { 'permission_callback' => '__return_true', ]); + // Product list widget AJAX (zibll ajax_widget_ui product lists). + register_rest_route(self::API_NAMESPACE, '/shop/frontend/widgets/products', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_frontend_widget_products'], + 'permission_callback' => '__return_true', + ]); + register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [ 'methods' => 'GET', 'callback' => [$this, 'get_frontend_revenue'], diff --git a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php index 6c846aa7..657d7b6b 100644 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php @@ -509,6 +509,40 @@ trait ShopControllerFrontendFlowOperations { ]); } + /** + * GET /shop/frontend/widgets/products — product list widget AJAX pager. + */ + public function get_frontend_widget_products(\WP_REST_Request $request): \WP_REST_Response { + if (!class_exists('\AKSRT\Widgets\WidgetProductList')) { + return $this->error('商品组件不可用', 500); + } + + $instance = [ + 'per_page' => max(1, min(50, (int) ($request->get_param('per_page') ?? 8))), + 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')), + 'cat_ids' => sanitize_text_field((string) ($request->get_param('cat_ids') ?? '')), + 'tag_ids' => sanitize_text_field((string) ($request->get_param('tag_ids') ?? '')), + 'discount_ids' => sanitize_text_field((string) ($request->get_param('discount_ids') ?? '')), + 'exclude_cat' => !empty($request->get_param('exclude_cat')) ? 1 : 0, + 'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')), + 'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), + ]; + $page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1)); + $payload = \AKSRT\Widgets\WidgetProductList::query_payload($instance, $page); + + return $this->success([ + 'data' => [ + 'html' => (string) ($payload['html'] ?? ''), + 'paginate_html' => (string) ($payload['paginate_html'] ?? ''), + 'has_more' => !empty($payload['has_more']), + 'page' => (int) ($payload['page'] ?? $page), + 'max_pages' => (int) ($payload['max_pages'] ?? 1), + 'total' => (int) ($payload['total'] ?? 0), + ], + ]); + } + /** * Render cat filter bar HTML for AJAX header replace. */ diff --git a/src/AKSRT/Widgets/WidgetProductList.php b/src/AKSRT/Widgets/WidgetProductList.php index eec012dc..8a76c6c3 100644 --- a/src/AKSRT/Widgets/WidgetProductList.php +++ b/src/AKSRT/Widgets/WidgetProductList.php @@ -8,225 +8,330 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; /** - * Shop product widgets (§6.9) - * - * - WidgetProductList: product list widget (filter/sort/paginate/AJAX) - * - WidgetProductOneline: horizontal swiper product widget - * - WidgetProductTab: multi-tab product list widget - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -/** - * Product list widget — equivalent to zibll `zib_shop_widget_ui_product_lists`. + * Shop product list widget — zibll `zib_shop_widget_ui_product_lists` lite. * * Supports: include/exclude category, discount, tag; sort order; list style; - * AJAX pagination; per-page count. + * AJAX load-more (via rest widget list endpoint); per-page count. */ class WidgetProductList extends \WP_Widget { - public function __construct() { - parent::__construct( - 'aksrt_product_list', - __('商品列表', 'aksrt'), - ['description' => __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] - ); - } + public function __construct() { + parent::__construct( + 'aksrt_product_list', + __('商品列表', 'aksrt'), + ['description' => __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] + ); + } - public function widget($args, $instance): void { - $title = apply_filters('widget_title', $instance['title'] ?? ''); - $per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8))); - $orderby = sanitize_key($instance['orderby'] ?? 'date'); - $order = in_array(strtoupper($instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($instance['order']) : 'DESC'; - $cat_ids = array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? '')))); - $tag_ids = array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? '')))); - $discount_ids = array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? '')))); - $exclude_cat = !empty($instance['exclude_cat']); - $style = sanitize_key($instance['style'] ?? 'default'); - $paged = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); + /** + * 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' => $paged, - 'orderby' => $orderby === 'sales' ? 'meta_value_num' : $orderby, - 'order' => $order, - ]; + $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'; - } + if ($orderby === 'sales') { + $query_args['meta_key'] = '_aksrt_sales'; + } elseif ($orderby === 'views') { + $query_args['meta_key'] = 'aksrt_post_views'; + } - // Taxonomy filters - $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; - } + $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; + } - $products = new \WP_Query($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 '
'; + } + wp_reset_postdata(); + return (string) ob_get_clean(); + } - echo $args['before_widget']; - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } + /** + * 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')) + : ''; + } + $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 = '- - -
-- - -
-- - -
-- - -
-- - -
-- -
-- - -
-- - -
-- - -
- '; + // Items sit directly under .ajaxpager so ajax-list inject/append works (zibll product-lists-row). + echo ''; - public function update($new_instance, $old_instance): array { - return [ - 'title' => sanitize_text_field($new_instance['title'] ?? ''), - 'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))), - 'orderby' => sanitize_key($new_instance['orderby'] ?? 'date'), - 'order' => in_array(strtoupper($new_instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($new_instance['order']) : 'DESC', - 'cat_ids' => sanitize_text_field($new_instance['cat_ids'] ?? ''), - 'tag_ids' => sanitize_text_field($new_instance['tag_ids'] ?? ''), - 'discount_ids' => sanitize_text_field($new_instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($new_instance['exclude_cat']), - 'style' => sanitize_key($new_instance['style'] ?? 'default'), - ]; - } + 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')), + ]; + } }