From c2be36acd72095ab4445cca73aba8fd5a222d590 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:17:53 +0800 Subject: [PATCH 1/7] feat(search): fill sidebar widget search history like zibll Render local/remote history into [data-aksrt-search-history-list], mark widget forms for history capture, and style clear control. --- assets/css/frontend/widget-batch5.css | 174 +++++++++++++++++++++++++- assets/js/core/search.js | 103 +++++++++++++++ src/AKSRT/Widgets/SearchWidget.php | 34 ++--- 3 files changed, 292 insertions(+), 19 deletions(-) diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index ccb27575..7c8fded0 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -307,15 +307,35 @@ .aksrt-bookmark-links__desc { font-size: 11px; color: var(--muted-2-color, #999); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* Search keywords */ -.aksrt-search-widget__keywords { margin-top: 10px; } +/* Search keywords + history */ +.aksrt-search-widget__keywords, +.aksrt-search-widget__history { margin-top: 10px; } +.aksrt-search-widget__kw-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} .aksrt-search-widget__kw-title { font-size: 12px; color: var(--muted-2-color, #888); margin-bottom: 6px; } -.aksrt-search-widget__kw-list { display: flex; flex-wrap: wrap; gap: 6px; } -.aksrt-search-widget__kw { +.aksrt-search-widget__kw-head .aksrt-search-widget__kw-title { margin-bottom: 0; } +.aksrt-search-widget__history-clear { + border: 0; + background: transparent; + color: var(--muted-2-color, #999); + font-size: 12px; + cursor: pointer; + padding: 0; +} +.aksrt-search-widget__history-clear:hover { color: var(--color-primary, #3b82f6); } +.aksrt-search-widget__kw-list, +.aksrt-search-widget__history-list { display: flex; flex-wrap: wrap; gap: 6px; } +.aksrt-search-widget__kw, +.aksrt-search-widget__history-item { display: inline-block; padding: 3px 10px; border-radius: 999px; @@ -324,9 +344,110 @@ text-decoration: none; font-size: 12px; } +.aksrt-search-widget__history-empty { + font-size: 12px; + color: var(--muted-2-color, #999); +} .aksrt-search-widget__more { margin-top: 8px; text-align: right; font-size: 12px; } .aksrt-search-widget__more a { color: var(--color-primary, #3b82f6); text-decoration: none; } +/* Product list widgets (batch6) */ +.aksrt-product-widget { margin-bottom: 8px; } +.aksrt-product-widget__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.aksrt-product-widget--list .aksrt-product-widget__grid, +.aksrt-product-widget--mini .aksrt-product-widget__grid { + grid-template-columns: 1fr; +} +.aksrt-product-widget--oneline .aksrt-product-widget__grid { + display: flex; + overflow-x: auto; + gap: 10px; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + padding-bottom: 4px; +} +.aksrt-product-widget--oneline .aksrt-product-widget__item, +.aksrt-product-widget--oneline .aksrt-product-card { + flex: 0 0 42%; + min-width: 140px; + scroll-snap-align: start; +} +.aksrt-product-widget__item, +.aksrt-product-widget .aksrt-product-card { + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 10px; + overflow: hidden; + background: var(--surface-1, #fff); +} +.aksrt-product-widget__thumb, +.aksrt-product-widget .aksrt-product-card__image { + display: block; + aspect-ratio: 1 / 1; + background: var(--surface-2, #f3f4f6); + overflow: hidden; +} +.aksrt-product-widget__thumb img, +.aksrt-product-widget .aksrt-product-card__image img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.aksrt-product-widget__info, +.aksrt-product-widget .aksrt-product-card__body { padding: 8px 10px 10px; } +.aksrt-product-widget__title, +.aksrt-product-widget .aksrt-product-card__title { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 13px; + font-weight: 600; + line-height: 1.35; + color: inherit; + text-decoration: none; +} +.aksrt-product-widget__meta { + display: flex; + justify-content: space-between; + gap: 6px; + margin-top: 6px; + font-size: 12px; +} +.aksrt-product-widget__price, +.aksrt-product-widget .aksrt-product-card__price { + color: var(--color-primary, #ef4444); + font-weight: 700; +} +.aksrt-product-widget__sales { color: var(--muted-2-color, #999); font-size: 11px; } +.aksrt-product-widget__empty { font-size: 13px; color: var(--muted-2-color, #999); padding: 12px 0; } +.aksrt-product-widget .ajax-pag { margin-top: 12px; text-align: center; } +.aksrt-product-tab__headers { + display: flex; + gap: 6px; + overflow-x: auto; + margin-bottom: 12px; + -webkit-overflow-scrolling: touch; +} +.aksrt-product-tab__header { + flex: 0 0 auto; + border: 0; + background: var(--surface-2, #f3f4f6); + border-radius: 999px; + padding: 6px 12px; + font-size: 13px; + cursor: pointer; + color: inherit; +} +.aksrt-product-tab__header.is-active { + background: var(--color-primary, #3b82f6); + color: #fff; +} + /* Scroll notice */ .aksrt-scroll-notice { display: flex; @@ -552,3 +673,48 @@ color: #b45309; } .aksrt-author-card__badge img { border-radius: 50%; } + +/* Badge remote modal (batch6) */ +.aksrt-badge-modal { padding: 8px 4px 12px; } +.aksrt-badge-modal__head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + position: relative; + padding-right: 28px; +} +.aksrt-badge-modal__avatar { + width: 40px; height: 40px; border-radius: 50%; object-fit: cover; +} +.aksrt-badge-modal__title { font-weight: 700; font-size: 15px; flex: 1; min-width: 0; } +.aksrt-badge-modal__wear-tag { + display: inline-block; + margin-left: 6px; + font-size: 12px; + font-weight: 500; + color: var(--color-primary, #3b82f6); +} +.aksrt-badge-modal__list { max-height: 50vh; overflow: auto; } +.aksrt-badge-modal__item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 4px; + border-bottom: 1px solid var(--color-line, #eee); +} +.aksrt-badge-modal__item:last-child { border-bottom: 0; } +.aksrt-badge-modal__item .medal-icon { width: 40px; height: 40px; object-fit: contain; } +.aksrt-badge-modal__icon-fa { + width: 40px; height: 40px; border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + background: var(--surface-2, #f3f4f6); color: #f59e0b; +} +.aksrt-badge-modal__icon-fa.lg { width: 72px; height: 72px; font-size: 28px; } +.aksrt-badge-modal__meta { flex: 1; min-width: 0; } +.aksrt-badge-modal__name { font-weight: 600; font-size: 14px; } +.aksrt-badge-modal__empty { text-align: center; color: var(--muted-2-color, #999); padding: 24px 0; } +.aksrt-badge-modal--single { text-align: center; padding: 16px 8px 20px; } +.aksrt-badge-modal__card { padding: 12px 8px 8px; } +.aksrt-badge-modal__actions { margin-top: 12px; } +.aksrt-author-hero__badges { margin-top: 8px; } diff --git a/assets/js/core/search.js b/assets/js/core/search.js index 1d742aeb..ca26bd77 100644 --- a/assets/js/core/search.js +++ b/assets/js/core/search.js @@ -390,6 +390,109 @@ window.__aksrtOpenSearchPanel = function () { setOpen(true); }; window.__aksrtCloseSearchPanel = function () { setOpen(false); }; + + // Sidebar / widget search history (zibll widget_ui_search show_history). + initWidgetSearchHistory({ + historyEnabled: historyEnabled, + readHistory: readHistory, + writeHistory: writeHistory, + requestHistorySync: requestHistorySync, + pushHistory: pushHistory, + getSearchType: getSearchType, + searchConfig: searchConfig, + }); + } + + function initWidgetSearchHistory(ctx) { + var lists = document.querySelectorAll("[data-aksrt-search-history-list]"); + if (!lists.length) return; + if (!ctx.historyEnabled) { + document.querySelectorAll("[data-aksrt-search-history]").forEach(function (el) { + el.hidden = true; + }); + return; + } + + function escapeHtml(s) { + return String(s == null ? "" : s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function paint(items) { + var list = Array.isArray(items) ? items : []; + lists.forEach(function (wrap) { + var section = wrap.closest("[data-aksrt-search-history]") || wrap.parentElement; + var clearBtn = section ? section.querySelector("[data-aksrt-search-history-clear]") : null; + if (!list.length) { + wrap.innerHTML = '' + escapeHtml("暂无搜索历史") + ""; + if (clearBtn) clearBtn.hidden = true; + return; + } + if (clearBtn) clearBtn.hidden = false; + wrap.innerHTML = list + .map(function (item) { + var kw = item && item.keyword ? String(item.keyword) : ""; + if (!kw) return ""; + var type = (item && item.type) || "post"; + var href = "?s=" + encodeURIComponent(kw); + return ( + '' + + escapeHtml(kw) + + "" + ); + }) + .join(""); + }); + } + + function load() { + if (window.aksrtData && aksrtData.isLoggedIn) { + ctx.requestHistorySync("get", null, function (items) { + paint(items); + }); + return; + } + paint(ctx.readHistory()); + } + + document.querySelectorAll("[data-aksrt-search-history-clear]").forEach(function (btn) { + btn.addEventListener("click", function (e) { + e.preventDefault(); + if (window.aksrtData && aksrtData.isLoggedIn) { + ctx.requestHistorySync("save", [], function (items) { + paint(items); + }); + return; + } + ctx.writeHistory([]); + paint([]); + }); + }); + + // Also push history when submitting bare widget forms without data-aksrt-search-form. + document.querySelectorAll(".aksrt-search-widget form").forEach(function (form) { + if (form.hasAttribute("data-aksrt-search-form")) return; + form.addEventListener("submit", function () { + var input = form.querySelector('input[name="s"]'); + var typeInput = form.querySelector('input[name="aksrt_search_type"]'); + var keyword = input ? (input.value || "").trim() : ""; + var type = typeInput + ? typeInput.value || ctx.searchConfig.defaultType || "post" + : ctx.searchConfig.defaultType || "post"; + if (keyword) ctx.pushHistory(keyword, type); + }); + }); + + load(); } window.AksrtFrontend.register("aksrt-search-panel", function () { initSearchPanel(); }); diff --git a/src/AKSRT/Widgets/SearchWidget.php b/src/AKSRT/Widgets/SearchWidget.php index 394da33f..080eb543 100644 --- a/src/AKSRT/Widgets/SearchWidget.php +++ b/src/AKSRT/Widgets/SearchWidget.php @@ -47,21 +47,25 @@ class SearchWidget extends \WP_Widget { } ?> '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + ?> + description) : ?>

description); ?>

From 7ac9f113dd9702ad2e5ad95ee575992d27648a79 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:20:55 +0800 Subject: [PATCH 4/7] feat(shop): multi-tab product widget lazy REST load First tab SSR via shared payload; other tabs fetch /shop/frontend/widgets/products on switch like zibll tab product. --- src/AKSRT/Widgets/WidgetProductTab.php | 315 +++++++++++++------------ 1 file changed, 170 insertions(+), 145 deletions(-) diff --git a/src/AKSRT/Widgets/WidgetProductTab.php b/src/AKSRT/Widgets/WidgetProductTab.php index a54e4a9d..8bece9e6 100644 --- a/src/AKSRT/Widgets/WidgetProductTab.php +++ b/src/AKSRT/Widgets/WidgetProductTab.php @@ -8,163 +8,188 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; /** - * Multi-tab product widget — equivalent to zibll `zib_shop_widget_ui_tab_product`. - * Each tab has independent category/discount filter. + * Multi-tab product widget — zibll `zib_shop_widget_ui_tab_product` lite. + * First tab renders immediately via shared payload; other tabs lazy-load via REST. */ class WidgetProductTab extends \WP_Widget { - public function __construct() { - parent::__construct( - 'aksrt_product_tab', - __('多TAB商品', 'aksrt'), - ['description' => __('AKSRT 多 TAB 商品列表组件(每 tab 独立筛选)。', 'aksrt')] - ); - } + public function __construct() { + parent::__construct( + 'aksrt_product_tab', + __('多TAB商品', 'aksrt'), + ['description' => __('AKSRT 多 TAB 商品列表组件(每 tab 独立筛选 + AJAX)。', 'aksrt')] + ); + } - public function widget($args, $instance): void { - $title = apply_filters('widget_title', $instance['title'] ?? ''); - $tabs_raw = (string) ($instance['tabs'] ?? ''); - $per_page = max(1, min(20, (int) ($instance['per_page'] ?? 8))); + /** + * @return list + */ + private static function parse_tabs(string $tabs_raw): array { + $tabs = []; + foreach (array_filter(explode("\n", $tabs_raw)) as $line) { + $parts = explode('|', trim($line)); + if (count($parts) >= 1 && trim($parts[0]) !== '') { + $tabs[] = [ + 'label' => trim($parts[0]), + 'cat_ids' => trim($parts[1] ?? ''), + 'discount_ids' => trim($parts[2] ?? ''), + 'tag_ids' => trim($parts[3] ?? ''), + 'orderby' => trim($parts[4] ?? 'date'), + ]; + } + } + return $tabs; + } - // Parse tabs: "Tab Name|cat_ids|discount_ids\n..." - $tabs = []; - foreach (array_filter(explode("\n", $tabs_raw)) as $line) { - $parts = explode('|', trim($line)); - if (count($parts) >= 1 && trim($parts[0]) !== '') { - $tabs[] = [ - 'label' => trim($parts[0]), - 'cat_ids' => trim($parts[1] ?? ''), - 'discount_ids' => trim($parts[2] ?? ''), - ]; - } - } - if (empty($tabs)) { - return; - } + public function widget($args, $instance): void { + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + $tabs_raw = (string) ($instance['tabs'] ?? ''); + $per_page = max(1, min(20, (int) ($instance['per_page'] ?? 8))); + $style = sanitize_key((string) ($instance['style'] ?? 'default')); + $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); + $tabs = self::parse_tabs($tabs_raw); + if (!$tabs) { + return; + } - echo $args['before_widget']; - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } + 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 + } - echo '
'; + $rest = rest_url('aksrt/v1/shop/frontend/widgets/products'); + echo '
'; - // Tab headers - echo '
'; - foreach ($tabs as $i => $tab) { - $active = $i === 0 ? ' is-active' : ''; - echo ''; - } - echo '
'; + echo '
'; + foreach ($tabs as $i => $tab) { + $active = $i === 0 ? ' is-active' : ''; + echo ''; + } + echo '
'; - // Tab panels - foreach ($tabs as $i => $tab) { - $hidden = $i > 0 ? ' style="display:none"' : ''; - echo '
'; + foreach ($tabs as $i => $tab) { + $hidden = $i > 0 ? ' style="display:none"' : ''; + $loaded = $i === 0 ? '1' : '0'; + echo ''; + } - $tax_query = []; - $cat_ids = array_filter(array_map('intval', explode(',', $tab['cat_ids']))); - $discount_ids = array_filter(array_map('intval', explode(',', $tab['discount_ids']))); - if (!empty($cat_ids)) { - $tax_query[] = ['taxonomy' => 'aksrt_product_cat', 'field' => 'term_id', 'terms' => $cat_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; - } + // Lightweight tab switch + lazy REST load (no inline product HTML duplication). + echo ''; - $products = new \WP_Query($query_args); - if ($products->have_posts()) { - echo '
'; - 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) - : []; - $price = (float) ($card['price'] ?? 0); - $image = $card['image'] ?? ''; - echo '
'; - if ($image) { - echo ''; - echo '' . esc_attr(get_the_title($pid)) . ''; - echo ''; - } - echo '
'; - echo '' . esc_html(get_the_title($pid)) . ''; - echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; - echo '
'; - } - echo '
'; - } else { - echo '

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

'; - } + echo '
'; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - echo '
'; - wp_reset_postdata(); - } + public function form($instance): void { + $defaults = [ + 'title' => __('商品精选', 'aksrt'), + 'tabs' => "热门推荐|\n最新上架|", + 'per_page' => 8, + 'style' => 'default', + 'paginate' => 'ajax', + ]; + $instance = wp_parse_args($instance, $defaults); + ?> +

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+ '; - echo '(function(){'; - echo 'var w=document.currentScript.parentElement;'; - echo 'w.querySelectorAll(".aksrt-product-tab__header").forEach(function(btn){'; - echo 'btn.addEventListener("click",function(){'; - echo 'w.querySelectorAll(".aksrt-product-tab__header").forEach(function(b){b.classList.remove("is-active")});'; - echo 'w.querySelectorAll(".aksrt-product-tab__panel").forEach(function(p){p.style.display="none"});'; - echo 'btn.classList.add("is-active");'; - echo 'var idx=btn.getAttribute("data-tab-index");'; - echo 'var panel=w.querySelector(".aksrt-product-tab__panel[data-tab-index=\'\"+idx+\"\']");'; - echo 'if(panel)panel.style.display="";'; - echo '});'; - echo '});'; - echo '})();'; - echo ''; - - echo '
'; - echo $args['after_widget']; - } - - public function form($instance): void { - $defaults = [ - 'title' => __('商品精选', 'aksrt'), - 'tabs' => "热门推荐|\n最新上架|", - 'per_page' => 8, - ]; - $instance = wp_parse_args($instance, $defaults); - ?> -

- - -

-

- - -

-

- - -

- sanitize_text_field($new_instance['title'] ?? ''), - 'tabs' => sanitize_textarea_field($new_instance['tabs'] ?? ''), - 'per_page' => max(1, min(20, (int) ($new_instance['per_page'] ?? 8))), - ]; - } + public function update($new_instance, $old_instance): array { + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')), + 'per_page' => max(1, min(20, (int) ($new_instance['per_page'] ?? 8))), + 'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')), + 'paginate' => sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), + ]; + } } From 43fb65dcdde4a7c18b5442478ab4408d336985b9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:22:53 +0800 Subject: [PATCH 5/7] fix(shop): flatten widget products REST payload for ajax-list success() already nests fields; drop extra data wrapper so normalizeJson/tab lazy-load read html/paginate_html directly. --- .../Admin/ShopControllerFrontendFlowOperations.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php index 657d7b6b..4451f987 100644 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php @@ -532,14 +532,12 @@ trait ShopControllerFrontendFlowOperations { $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), - ], + '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), ]); } From 5cb6928b1eb2b45e5b708c0d6a3a8e4c200beb1c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:25:59 +0800 Subject: [PATCH 6/7] feat(forum): topic list widget AJAX load-more like zibll Shared query_payload + REST /forum/frontend/widgets/topics wired to ajax-list for sidebar forum posts lists. --- src/AKSRT/Forum/Forum.php | 35 ++++ src/AKSRT/Widgets/WidgetForumTopicLists.php | 210 +++++++++++++++----- 2 files changed, 192 insertions(+), 53 deletions(-) diff --git a/src/AKSRT/Forum/Forum.php b/src/AKSRT/Forum/Forum.php index 5c504996..f04561e8 100644 --- a/src/AKSRT/Forum/Forum.php +++ b/src/AKSRT/Forum/Forum.php @@ -246,6 +246,40 @@ class Forum { $this->view_builder()->ajax_board_topics(); } + public function register_widget_routes(): void { + register_rest_route('aksrt/v1', '/forum/frontend/widgets/topics', [ + 'methods' => 'GET', + 'callback' => [$this, 'rest_widget_topics'], + 'permission_callback' => '__return_true', + ]); + } + + public function rest_widget_topics(\WP_REST_Request $request) { + if (!class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { + return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500); + } + $instance = [ + 'count' => max(1, min(30, (int) ($request->get_param('count') ?? 8))), + 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')), + 'plate_id' => max(0, (int) ($request->get_param('plate_id') ?? 0)), + '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\WidgetForumTopicLists::query_payload($instance, $page); + return new \WP_REST_Response([ + 'success' => true, + '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), + ], 200); + } + + + public function render_board_cards(array $boards): void { $this->view_builder()->render_board_cards($boards); } @@ -396,6 +430,7 @@ class Forum { // Public plate/board topic list (sort tabs) add_action('wp_ajax_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); add_action('wp_ajax_nopriv_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); + add_action('rest_api_init', [$this, 'register_widget_routes']); add_action('admin_init', [$this, 'maybe_migrate_boards'], 25); add_filter('template_include', [$this, 'template_routing']); diff --git a/src/AKSRT/Widgets/WidgetForumTopicLists.php b/src/AKSRT/Widgets/WidgetForumTopicLists.php index 72d559e1..b12ae482 100644 --- a/src/AKSRT/Widgets/WidgetForumTopicLists.php +++ b/src/AKSRT/Widgets/WidgetForumTopicLists.php @@ -1,6 +1,6 @@ esc_html__('按排序/版块筛选展示论坛帖子。', 'aksrt'), + 'description' => esc_html__('按排序/版块筛选展示论坛帖子(支持 AJAX 加载更多)。', 'aksrt'), 'classname' => 'widget_aksrt_forum_topic_lists', ] ); } - public function widget($args, $instance): void { - if (!post_type_exists('aksrt_forum_topic')) { - return; - } - - $count = max(1, min(30, (int) ($instance['count'] ?? 8))); - $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); - $style = sanitize_key((string) ($instance['style'] ?? 'mini')); + /** + * @param array $instance + * @param int $paged + * @return array + */ + public static function build_query_args(array $instance, int $paged = 1): array { + $count = max(1, min(30, (int) ($instance['count'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); $plate_id = (int) ($instance['plate_id'] ?? 0); - $current_plate = !empty($instance['current_plate']); - - if ($current_plate) { - $resolved = $this->resolve_current_plate_id(); - if ($resolved <= 0) { - return; - } - $plate_id = $resolved; - } $query = [ 'post_type' => 'aksrt_forum_topic', 'post_status' => 'publish', 'posts_per_page' => $count, + 'paged' => max(1, $paged), 'order' => 'DESC', ]; @@ -59,35 +51,41 @@ class WidgetForumTopicLists extends \WP_Widget { case 'modified': $query['orderby'] = 'modified'; break; + case 'essence': + $query['meta_query'] = [[ + 'key' => '_aksrt_essence', + 'value' => '1', + ]]; + $query['orderby'] = 'date'; + break; default: $query['orderby'] = 'date'; } if ($plate_id > 0) { - $query['meta_query'] = [[ - 'key' => '_aksrt_plate_id', - 'value' => $plate_id, - ]]; + $query['meta_query'] = array_values(array_filter(array_merge( + isset($query['meta_query']) ? $query['meta_query'] : [], + [[ + 'key' => '_aksrt_plate_id', + 'value' => $plate_id, + ]] + ))); } - $q = new \WP_Query($query); + return $query; + } + + public static function render_items_html(\WP_Query $q, string $style = 'mini'): string { if (!$q->have_posts()) { - wp_reset_postdata(); - return; + return ''; } - - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); - if ($title !== '') { - echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - $list_class = $style === 'detail' ? 'aksrt-ftopic-list is-detail' : 'aksrt-ftopic-list is-mini'; + ob_start(); echo '
    '; while ($q->have_posts()) { $q->the_post(); $views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true); - echo '
  • '; + echo '
  • '; echo '' . esc_html(get_the_title()) . ''; if ($style === 'detail') { echo '
    '; @@ -102,6 +100,96 @@ class WidgetForumTopicLists extends \WP_Widget { } echo '
'; wp_reset_postdata(); + return (string) ob_get_clean(); + } + + /** + * @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); + $style = sanitize_key((string) ($instance['style'] ?? 'mini')); + $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); + $q = new \WP_Query(self::build_query_args($instance, $page)); + $html = self::render_items_html($q, $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) $q->max_num_pages); + $total = (int) $q->found_posts; + $has_more = $page < $max_pages; + $paginate_html = ''; + if ($paginate === 'ajax' && $has_more) { + $next = add_query_arg( + array_filter([ + 'page' => $page + 1, + 'count' => max(1, min(30, (int) ($instance['count'] ?? 8))), + 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), + 'style' => $style, + 'plate_id' => (int) ($instance['plate_id'] ?? 0), + 'paginate' => 'ajax', + ]), + rest_url('aksrt/v1/forum/frontend/widgets/topics') + ); + $paginate_html = function_exists('aksrt_get_ajax_next_paginate') + ? aksrt_get_ajax_next_paginate($total, $page, max(1, (int) ($instance['count'] ?? 8)), $next) + : ''; + } 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 { + if (!post_type_exists('aksrt_forum_topic')) { + return; + } + + $count = max(1, min(30, (int) ($instance['count'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $style = sanitize_key((string) ($instance['style'] ?? 'mini')); + $plate_id = (int) ($instance['plate_id'] ?? 0); + $current_plate = !empty($instance['current_plate']); + $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); + + if ($current_plate) { + $resolved = $this->resolve_current_plate_id(); + if ($resolved <= 0) { + return; + } + $plate_id = $resolved; + } + + $instance['count'] = $count; + $instance['orderby'] = $orderby; + $instance['style'] = $style; + $instance['plate_id'] = $plate_id; + $instance['paginate'] = $paginate; + + $payload = self::query_payload($instance, 1); + $rest = rest_url('aksrt/v1/forum/frontend/widgets/topics'); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo '
'; + echo '
'; + echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo ''; + echo '
'; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @@ -119,58 +207,74 @@ class WidgetForumTopicLists extends \WP_Widget { } public function form($instance): void { + $defaults = [ + 'title' => __('帖子列表', 'aksrt'), + 'count' => 8, + 'orderby' => 'date', + 'style' => 'mini', + 'plate_id' => 0, + 'current_plate' => 0, + 'paginate' => 'ajax', + ]; + $instance = wp_parse_args($instance, $defaults); ?>

-

- + +

+

+

sanitize_text_field((string) ($new_instance['title'] ?? '')), 'count' => max(1, min(30, (int) ($new_instance['count'] ?? 8))), - 'orderby' => $orderby, - 'style' => ($new_instance['style'] ?? '') === 'detail' ? 'detail' : 'mini', + 'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')), + 'style' => sanitize_key((string) ($new_instance['style'] ?? 'mini')), 'plate_id' => max(0, (int) ($new_instance['plate_id'] ?? 0)), 'current_plate' => !empty($new_instance['current_plate']) ? 1 : 0, + 'paginate' => sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ]; } } From d4797238140d9a6aa2c863c41708f5fe2b6f04f5 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:27:18 +0800 Subject: [PATCH 7/7] feat(forum): multi-tab topic widget lazy REST load SSR first tab; other tabs fetch /forum/frontend/widgets/topics on switch, with shared list markup and styles. --- assets/css/frontend/widget-batch5.css | 58 +++++++ src/AKSRT/Widgets/WidgetForumTabTopics.php | 172 ++++++++++----------- 2 files changed, 142 insertions(+), 88 deletions(-) diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index 7c8fded0..a8c0a29e 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -718,3 +718,61 @@ .aksrt-badge-modal__card { padding: 12px 8px 8px; } .aksrt-badge-modal__actions { margin-top: 12px; } .aksrt-author-hero__badges { margin-top: 8px; } + +/* Forum tab topics (batch6) */ +.aksrt-ftab__nav { + display: flex; + gap: 6px; + list-style: none; + margin: 0 0 10px; + padding: 0; + overflow-x: auto; +} +.aksrt-ftab__btn { + border: 0; + background: var(--surface-2, #f3f4f6); + border-radius: 999px; + padding: 5px 12px; + font-size: 13px; + cursor: pointer; + color: inherit; +} +.aksrt-ftab__btn.is-active { + background: var(--color-primary, #3b82f6); + color: #fff; +} +.aksrt-ftab__panel { display: none; } +.aksrt-ftab__panel.is-active { display: block; } +.aksrt-ftab__empty { font-size: 13px; color: var(--muted-2-color, #999); padding: 8px 0; } +.aksrt-ftopic-list { list-style: none; margin: 0; padding: 0; } +.aksrt-ftopic-list__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 7px 0; + border-bottom: 1px dashed var(--color-line, #eee); + font-size: 13px; +} +.aksrt-ftopic-list__item:last-child { border-bottom: 0; } +.aksrt-ftopic-list__link { + color: inherit; + text-decoration: none; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-ftopic-list__views { color: var(--muted-2-color, #999); font-size: 12px; flex-shrink: 0; } +.aksrt-ftopic-list__meta { + display: flex; + gap: 8px; + font-size: 11px; + color: var(--muted-2-color, #999); + margin-top: 4px; +} +.aksrt-ftopic-list.is-detail .aksrt-ftopic-list__item { + flex-direction: column; + align-items: flex-start; +} diff --git a/src/AKSRT/Widgets/WidgetForumTabTopics.php b/src/AKSRT/Widgets/WidgetForumTabTopics.php index 1ffbfb60..c96212b6 100644 --- a/src/AKSRT/Widgets/WidgetForumTabTopics.php +++ b/src/AKSRT/Widgets/WidgetForumTabTopics.php @@ -1,6 +1,6 @@ esc_html__('热门/最新/精华等栏目切换的帖子列表。', 'aksrt'), + 'description' => esc_html__('热门/最新/精华等栏目切换的帖子列表(懒加载)。', 'aksrt'), 'classname' => 'widget_aksrt_forum_tab_topics', ] ); @@ -36,6 +36,7 @@ class WidgetForumTabTopics extends \WP_Widget { } $count = max(1, min(20, (int) ($instance['count'] ?? 6))); $uid = 'aksrt-ftab-' . esc_attr($this->id); + $rest = rest_url('aksrt/v1/forum/frontend/widgets/topics'); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); @@ -43,32 +44,60 @@ class WidgetForumTabTopics extends \WP_Widget { echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - echo '
'; + echo '
'; echo '
    '; foreach ($tabs as $i => $tab) { $cls = $i === 0 ? ' is-active' : ''; - echo '
  • '; + echo '
  • '; } echo '
'; foreach ($tabs as $i => $tab) { $cls = $i === 0 ? ' is-active' : ''; - echo '
'; - $this->render_list($tab, $count); + $loaded = $i === 0 ? '1' : '0'; + echo '
'; + if ($i === 0) { + $payload = WidgetForumTopicLists::query_payload([ + 'count' => $count, + 'orderby' => $tab['orderby'], + 'style' => 'mini', + 'paginate' => 'none', + ], 1); + echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + echo '
'; + } echo '
'; } echo '
'; - ?>