From c0b7cb504b3e93fff327e4d2c3972b2c0d70773d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Jul 2026 08:03:25 +0800 Subject: [PATCH 1/3] feat(widgets): wire PostsNavs TOC shell to page-template scrollspy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match built-in single sidebar .posts-nav-box.toc-widget/.toc-list so the sidebar widget is filled (≥3 headings), supports multiple TOC shells, and removes empty chrome. --- .../css/frontend/single-post/related-toc.css | 28 +++++++ assets/js/page-template.js | 76 ++++++++++++++----- src/AKSRT/Widgets/WidgetPostsNavs.php | 29 +++++-- 3 files changed, 108 insertions(+), 25 deletions(-) diff --git a/assets/css/frontend/single-post/related-toc.css b/assets/css/frontend/single-post/related-toc.css index 277cd053..827f1d6e 100644 --- a/assets/css/frontend/single-post/related-toc.css +++ b/assets/css/frontend/single-post/related-toc.css @@ -364,6 +364,10 @@ html.dark .article-end-separator span { color: var(--color-primary); } +.toc-list .toc-h1 a { + font-weight: 700; +} + .toc-list .toc-h3 a { padding-left: 12px; } @@ -372,6 +376,30 @@ html.dark .article-end-separator span { padding-left: 24px; } +/* WidgetPostsNavs + built-in shell share .posts-nav-box.toc-widget */ +.widget .posts-nav-box.toc-widget, +.widget_aksrt_posts_navs .posts-nav-box.toc-widget { + margin: 0; + box-shadow: none; + background: transparent; + padding: 0; + border-radius: 0; +} + +.widget .posts-nav-box.toc-widget > h3, +.widget .posts-nav-box.toc-widget .toc-widget__title, +.widget_aksrt_posts_navs .posts-nav-box.toc-widget > h3 { + margin: 0 0 10px; + font-size: 15px; + font-weight: 700; +} + +body.single .sidebar .posts-nav-box.toc-widget.is-affix, +body.single .sidebar .posts-nav-box.toc-widget[data-affix="true"] { + position: sticky; + top: calc(var(--nav-height, 60px) + 16px); +} + .toc-float-btn { position: fixed; right: 18px; diff --git a/assets/js/page-template.js b/assets/js/page-template.js index b532e3cd..d87794c2 100644 --- a/assets/js/page-template.js +++ b/assets/js/page-template.js @@ -19,9 +19,31 @@ } function initPostToc() { - var content = document.querySelector('.wp-posts-content[data-nav="posts"][data-aksrt-toc="1"]'); - var tocWidget = document.querySelector('.posts-nav-box.toc-widget'); - var tocList = tocWidget ? tocWidget.querySelector('.toc-list') : null; + // Accept either explicit data-aksrt-toc="1" or any [data-nav] article body. + var content = + document.querySelector('.wp-posts-content[data-nav="posts"][data-aksrt-toc="1"]') || + document.querySelector('.wp-posts-content[data-nav]') || + document.querySelector('[data-nav="posts"]') || + document.querySelector('[data-nav]'); + + // Support multiple TOC shells (built-in single sidebar + WidgetPostsNavs). + var tocWidgets = Array.prototype.slice.call( + document.querySelectorAll('.posts-nav-box.toc-widget, .posts-nav-box[data-title]') + ); + var tocLists = []; + tocWidgets.forEach(function (box) { + if (!box.classList.contains('toc-widget')) { + box.classList.add('toc-widget'); + } + var list = box.querySelector('.toc-list'); + if (!list) { + list = document.createElement('ol'); + list.className = 'toc-list'; + box.appendChild(list); + } + tocLists.push(list); + }); + var mobileToggle = document.querySelector('.toc-float-btn'); var drawer = document.querySelector('.toc-drawer'); var overlay = document.querySelector('.toc-drawer-overlay'); @@ -29,16 +51,25 @@ var closeBtn = drawer ? drawer.querySelector('.toc-drawer__close') : null; if (!content || content.dataset.aksrtTocBound === '1') return; - if (!tocList && !drawerList && !mobileToggle) return; + if (!tocLists.length && !drawerList && !mobileToggle) return; content.dataset.aksrtTocBound = '1'; var maxDepth = parseInt(content.dataset.aksrtTocDepth || '3', 10) || 3; + // zibll section-navs: only fill when heading count > 2 (≥3). + var MIN_HEADINGS = 3; function getHeadings() { - return Array.prototype.slice.call(content.querySelectorAll('h2, h3, h4')).filter(function (heading) { + // zibll scans h1–h4 inside [data-nav]; depth setting still caps lower levels. + return Array.prototype.slice.call(content.querySelectorAll('h1, h2, h3, h4')).filter(function (heading) { + if (heading.closest('.no-nav') || heading.classList.contains('item-heading')) { + return false; + } var level = parseInt((heading.tagName || 'H2').replace('H', ''), 10) || 2; - return level <= maxDepth; + // toc_depth is typically 2–4 meaning deepest heading level from h2 baseline; + // allow h1 always, then h2..h(1+maxDepth) capped at h4. + if (level === 1) return true; + return level <= Math.min(4, maxDepth + 1); }); } @@ -46,7 +77,7 @@ if (!container) return false; var headings = getHeadings(); - if (!headings.length) return false; + if (headings.length < MIN_HEADINGS) return false; var html = ''; headings.forEach(function (heading) { @@ -55,7 +86,9 @@ } var levelClass = ''; - if (heading.tagName === 'H3') { + if (heading.tagName === 'H1') { + levelClass = 'toc-h1'; + } else if (heading.tagName === 'H3') { levelClass = 'toc-h3'; } else if (heading.tagName === 'H4') { levelClass = 'toc-h4'; @@ -68,13 +101,24 @@ return true; } - var hasToc = buildToc(tocList); - if (drawerList && drawerList !== tocList) { - buildToc(drawerList); + var hasToc = false; + tocLists.forEach(function (list) { + if (buildToc(list)) hasToc = true; + }); + if (drawerList && tocLists.indexOf(drawerList) === -1) { + if (buildToc(drawerList)) hasToc = true; } if (!hasToc) { - if (tocWidget) tocWidget.remove(); + tocWidgets.forEach(function (box) { + var host = box.closest('.widget') || box.parentElement; + // Prefer removing the whole widget chrome when empty. + if (host && host !== document.body && host.querySelectorAll('.posts-nav-box').length <= 1) { + host.remove(); + } else { + box.remove(); + } + }); if (mobileToggle) mobileToggle.remove(); if (drawer) drawer.remove(); if (overlay) overlay.remove(); @@ -94,7 +138,7 @@ } function trackActiveHeading() { - var headings = content.querySelectorAll('h2[id], h3[id], h4[id]'); + var headings = content.querySelectorAll('h1[id], h2[id], h3[id], h4[id]'); if (!headings.length) return; var activeId = ''; @@ -117,10 +161,8 @@ if (!target) return; event.preventDefault(); - target.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); + var top = target.getBoundingClientRect().top + window.pageYOffset - 105; + window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); closeDrawer(); }); }); diff --git a/src/AKSRT/Widgets/WidgetPostsNavs.php b/src/AKSRT/Widgets/WidgetPostsNavs.php index c8873898..3567c6e5 100644 --- a/src/AKSRT/Widgets/WidgetPostsNavs.php +++ b/src/AKSRT/Widgets/WidgetPostsNavs.php @@ -2,7 +2,8 @@ /** * In-article / topic TOC shell (zibll widget_ui_posts_navs parity). * - * Frontend JS fills `.posts-nav-box` from heading outline when present. + * Shell matches the built-in single sidebar TOC so page-template.js can fill + * `.posts-nav-box.toc-widget > .toc-list` from `[data-nav]` headings. * * @package AKSRT */ @@ -18,8 +19,8 @@ class WidgetPostsNavs extends \WP_Widget { 'aksrt_posts_navs', esc_html__('AKSRT 文章目录树', 'aksrt'), [ - 'description' => esc_html__('显示当前文章/帖子标题目录,非详情页不输出。标题超过 3 个才显示。', 'aksrt'), - 'classname' => 'widget_aksrt_posts_navs', + 'description' => esc_html__('显示当前文章/帖子标题目录,非详情页不输出。标题超过 2 个才填充。', 'aksrt'), + 'classname' => 'widget_aksrt_posts_navs', ] ); } @@ -35,16 +36,28 @@ class WidgetPostsNavs extends \WP_Widget { $data_title = $title; if ($mini_title !== '') { - $data_title .= ' · ' . $mini_title; + $data_title .= ($data_title !== '' ? ' · ' : '') . $mini_title; + } + if ($data_title === '') { + $data_title = __('文章目录', 'aksrt'); + } + + // Mirror aksrt-single-render built-in TOC shell so page-template.js binds. + $classes = 'zib-widget toc-widget posts-nav-box'; + if ($in_affix) { + $classes .= ' is-affix'; } echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '
'; printf( - '
', + '
', + esc_attr($classes), $in_affix ? ' data-affix="true"' : '', - $data_title !== '' ? ' data-title="' . esc_attr($data_title) . '"' : '' + esc_attr($data_title), + esc_attr($data_title) ); + echo '

' . esc_html($data_title) . '

'; + echo '
    '; echo '
    '; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } @@ -68,7 +81,7 @@ class WidgetPostsNavs extends \WP_Widget {

    -

    +

    Date: Tue, 21 Jul 2026 08:04:03 +0800 Subject: [PATCH 2/3] feat(widgets): author/user card more-posts strip + ranking empty/TOP Add shared aksrt-user-card helpers (mini list + swiper-scroll card), wire AuthorCard/UserCard show_posts options, ranking empty state and top_badge. --- assets/css/frontend/widget-batch5.css | 120 ++++++++++++++++++++ inc/bootstrap.php | 1 + inc/helpers/aksrt-user-card.php | 140 ++++++++++++++++++++++++ src/AKSRT/Widgets/WidgetAuthorCard.php | 99 ++++++++++++++--- src/AKSRT/Widgets/WidgetUserCard.php | 120 +++++++++++++++++--- src/AKSRT/Widgets/WidgetUserRanking.php | 29 ++++- 6 files changed, 473 insertions(+), 36 deletions(-) create mode 100644 inc/helpers/aksrt-user-card.php diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index 3fc342b3..c0b1dcb4 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -883,3 +883,123 @@ .aksrt-tag-cloud .c-red-2 { background: #fecaca; color: #991b1b; } .aksrt-home-widget__ajax .ajax-pag { margin-top: 12px; text-align: center; } + +/* ---- batch10: user-card more-posts + slider wrap + ranking top badge ---- */ +.user-card-posts .more-posts-mini .item { + padding: 6px 0; + border-bottom: 1px dashed var(--color-line, rgba(0, 0, 0, 0.06)); +} +.user-card-posts .more-posts-mini .item:last-child { border-bottom: 0; } +.user-card-posts .more-posts-mini .item a { + display: block; + color: inherit; + text-decoration: none; + font-size: 13px; +} +.user-card-posts .more-posts-mini .item a:hover { + color: var(--color-primary, #3b82f6); +} +.user-card-posts .more-posts.swiper-container { + margin: 8px 0 0; + padding: 0 2px 4px; + overflow: hidden; +} +.user-card-posts .more-posts .swiper-slide { + width: auto; + max-width: 46%; +} +.aksrt-more-post-card { + display: block; + color: inherit; + text-decoration: none; + border-radius: 8px; + overflow: hidden; + background: var(--surface-2, #f8fafc); +} +.aksrt-more-post-card__thumb { + display: block; + aspect-ratio: 16 / 10; + overflow: hidden; + background: var(--surface-3, #eef2f7); +} +.aksrt-more-post-card__thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.aksrt-more-post-card__body { + display: block; + padding: 6px 8px 8px; +} +.aksrt-more-post-card__title { + display: block; + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} +.aksrt-more-post-card__meta { + display: flex; + justify-content: space-between; + gap: 6px; + margin-top: 4px; + font-size: 11px; +} + +.aksrt-widget-slider-wrap { + border-radius: 10px; + overflow: hidden; +} +.aksrt-widget-slider-wrap .new-swiper { + --pc-height: 220px; + --m-height: 180px; + height: var(--pc-height); +} +@media (max-width: 768px) { + .aksrt-widget-slider-wrap .new-swiper { + height: var(--m-height); + } +} +.aksrt-widget-slider-wrap .swiper-slide img.fit-cover { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.aksrt-widget-slider-wrap .aksrt-slide-text { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 18px 14px 12px; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.55)); + color: #fff; + pointer-events: none; +} +.aksrt-widget-slider-wrap .aksrt-slide-text__title { + font-size: 15px; + font-weight: 700; +} +.aksrt-widget-slider-wrap .aksrt-slide-text__desc { + font-size: 12px; + opacity: 0.9; + margin-top: 2px; +} + +.muted-box.c-red, +.c-red.muted-box { + color: #b91c1c; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 8px; + padding: 12px 14px; + font-size: 13px; +} + +/* ranking empty state */ +.aksrt-user-ranking__empty { + margin: 8px 0 0; + font-size: 13px; + color: var(--muted-2-color, #999); + text-align: center; +} diff --git a/inc/bootstrap.php b/inc/bootstrap.php index cc8d656e..aa2e6c2d 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -48,6 +48,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-header-userbox.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-header-menu-buttons.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-runtime-win.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-identity.php'; +require_once AKSRT_DIR . '/inc/helpers/aksrt-user-card.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-rewards.php'; require_once AKSRT_DIR . '/inc/helpers/menu-decoration.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-transactions.php'; diff --git a/inc/helpers/aksrt-user-card.php b/inc/helpers/aksrt-user-card.php new file mode 100644 index 00000000..81e3c9e0 --- /dev/null +++ b/inc/helpers/aksrt-user-card.php @@ -0,0 +1,140 @@ + $args + * @return list<\WP_Post> + */ + function aksrt_user_card_query_posts(array $args): array { + $user_id = (int) ($args['user_id'] ?? 0); + $limit = max(0, min(20, (int) ($args['limit'] ?? 6))); + if ($user_id <= 0 || $limit <= 0) { + return []; + } + + $orderby = sanitize_key((string) ($args['orderby'] ?? 'date')); + $post_type = sanitize_key((string) ($args['post_type'] ?? 'post')); + if (!post_type_exists($post_type)) { + $post_type = 'post'; + } + + $query_args = [ + 'post_status' => 'publish', + 'post_type' => $post_type, + 'author' => $user_id, + 'posts_per_page' => $limit, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ]; + + $exclude = (int) ($args['exclude'] ?? 0); + if ($exclude <= 0) { + $exclude = (int) get_the_ID(); + } + if ($exclude > 0) { + $query_args['post__not_in'] = [$exclude]; + } + + switch ($orderby) { + case 'views': + case 'view': + $query_args['meta_key'] = 'aksrt_post_views'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + case 'comment_count': + case 'comments': + $query_args['orderby'] = 'comment_count'; + $query_args['order'] = 'DESC'; + break; + case 'modified': + $query_args['orderby'] = 'modified'; + $query_args['order'] = 'DESC'; + break; + case 'rand': + case 'random': + $query_args['orderby'] = 'rand'; + break; + default: + $query_args['orderby'] = 'date'; + $query_args['order'] = 'DESC'; + } + + $q = new WP_Query($query_args); + $posts = $q->posts; + wp_reset_postdata(); + return is_array($posts) ? array_values(array_filter($posts, static fn($p) => $p instanceof WP_Post)) : []; + } +endif; + +if (!function_exists('aksrt_render_user_card_posts')) : + /** + * Render more-posts strip (card swiper-scroll or mini list). + * + * @param array $args + */ + function aksrt_render_user_card_posts(array $args): string { + $style = sanitize_key((string) ($args['post_style'] ?? $args['style'] ?? 'mini')); + if (!in_array($style, ['card', 'mini'], true)) { + $style = 'mini'; + } + $posts = aksrt_user_card_query_posts($args); + $show_empty = !empty($args['show_empty']); + + if (!$posts) { + if (!$show_empty) { + return ''; + } + return '
    ' . + esc_html__('暂无更多文章', 'aksrt') . + '
    '; + } + + ob_start(); + echo '
    '; + if ($style === 'card') { + echo ''; + } else { + echo ''; + } + echo '
    '; + return (string) ob_get_clean(); + } +endif; diff --git a/src/AKSRT/Widgets/WidgetAuthorCard.php b/src/AKSRT/Widgets/WidgetAuthorCard.php index 6a523eda..9afe9744 100644 --- a/src/AKSRT/Widgets/WidgetAuthorCard.php +++ b/src/AKSRT/Widgets/WidgetAuthorCard.php @@ -1,6 +1,6 @@ esc_html__('在文章/帖子侧栏显示当前内容作者信息。', 'aksrt'), + 'description' => esc_html__('在文章/帖子侧栏显示当前内容作者信息与更多文章。', 'aksrt'), 'classname' => 'widget_aksrt_author_card', ] ); @@ -41,8 +41,20 @@ class WidgetAuthorCard extends \WP_Widget { return; } - $show_bg = !isset($instance['show_img_bg']) || !empty($instance['show_img_bg']); + $show_bg = !isset($instance['show_img_bg']) || !empty($instance['show_img_bg']); $show_stats = !isset($instance['show_stats']) || !empty($instance['show_stats']); + $show_posts = !isset($instance['show_posts']) || !empty($instance['show_posts']); + $post_style = sanitize_key((string) ($instance['post_style'] ?? 'mini')); + if (!in_array($post_style, ['card', 'mini'], true)) { + // Legacy show_img checkbox from zibll. + if (isset($instance['show_img'])) { + $post_style = !empty($instance['show_img']) ? 'card' : 'mini'; + } else { + $post_style = 'mini'; + } + } + $limit = max(0, min(12, (int) ($instance['limit'] ?? 6))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); $avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id, ['size' => 120]) @@ -68,35 +80,39 @@ class WidgetAuthorCard extends \WP_Widget { $followers = \AKSRT\User\Follow::get_instance()->get_follower_count($author_id); } + $name_html = function_exists('aksrt_get_user_name') + ? aksrt_get_user_name(['id' => $author_id, 'follow' => false, 'class' => 'flex ac jc']) + : '' . esc_html($user->display_name) . ''; + 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 '
    '; if ($show_bg) { $bg = $cover !== '' ? ' style="background-image:url(' . esc_url($cover) . ')"' : ''; - echo '
    '; + echo '
    '; } - echo '
    '; - echo ''; - echo '' . esc_html($user->display_name) . ''; + echo '
    '; + echo '
    '; + echo ''; + echo '
    ' . $name_html . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($bio !== '') { - echo '
    ' . esc_html(wp_trim_words($bio, 28)) . '
    '; + echo '
    ' . esc_html(wp_trim_words($bio, 28)) . '
    '; } $show_badge = !isset($instance['show_badge']) || !empty($instance['show_badge']); if ($show_badge && class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::public_display()) { $wear = \AKSRT\User\Badges::get_wear($author_id); if ($wear === '') { - // zibll alias fallback $wear = sanitize_key((string) get_user_meta($author_id, 'wear_medal', true)); } if ($wear !== '') { $map = \AKSRT\User\Badges::get_dictionary_map(); if (isset($map[$wear])) { $badge = $map[$wear]; - echo '
    '; + echo '
    '; if (!empty($badge['icon_url'])) { echo ' '; } else { @@ -118,18 +134,37 @@ class WidgetAuthorCard extends \WP_Widget { } $cuid = get_current_user_id(); $show_follow = !isset($instance['show_follow']) || !empty($instance['show_follow']); + echo '
    '; if ($show_follow && $cuid > 0 && $cuid !== $author_id && class_exists('\AKSRT\User\Follow')) { $following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $author_id); $label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt'); - echo ''; + echo ''; } - echo '' . esc_html__('查看主页', 'aksrt') . ''; + echo '' . esc_html__('查看主页', 'aksrt') . ''; + echo '
    '; + echo '
    '; // .user-content + + if ($show_posts && $limit > 0 && function_exists('aksrt_render_user_card_posts')) { + echo aksrt_render_user_card_posts([ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'user_id' => $author_id, + 'limit' => $limit, + 'orderby' => $orderby, + 'post_style' => $post_style, + 'post_type' => 'post', + 'show_empty' => true, + ]); + } + echo '
    '; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } public function form($instance): void { + $post_style = sanitize_key((string) ($instance['post_style'] ?? 'mini')); + if (!in_array($post_style, ['card', 'mini'], true)) { + $post_style = !empty($instance['show_img']) ? 'card' : 'mini'; + } ?>

    @@ -146,12 +181,44 @@ class WidgetAuthorCard extends \WP_Widget {


    - +
    + +

    +

    + +

    +

    + +

    +

    +

    sanitize_text_field((string) ($new_instance['title'] ?? '')), 'user_id' => max(0, (int) ($new_instance['user_id'] ?? 0)), @@ -159,6 +226,10 @@ class WidgetAuthorCard extends \WP_Widget { 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, 'show_follow' => !empty($new_instance['show_follow']) ? 1 : 0, 'show_badge' => !empty($new_instance['show_badge']) ? 1 : 0, + 'show_posts' => !empty($new_instance['show_posts']) ? 1 : 0, + 'limit' => max(0, min(12, (int) ($new_instance['limit'] ?? 6))), + 'post_style' => $post_style, + 'orderby' => $orderby, ]; } } diff --git a/src/AKSRT/Widgets/WidgetUserCard.php b/src/AKSRT/Widgets/WidgetUserCard.php index c47afeea..61f42ac8 100644 --- a/src/AKSRT/Widgets/WidgetUserCard.php +++ b/src/AKSRT/Widgets/WidgetUserCard.php @@ -15,7 +15,7 @@ class WidgetUserCard extends \WP_Widget { parent::__construct( 'aksrt_user_card', __('用户卡片', 'aksrt'), - ['description' => __('未登录显示登录入口;登录后显示当前用户卡片、签到与快捷按钮。', 'aksrt')] + ['description' => __('未登录显示登录入口;登录后显示当前用户卡片、签到、快捷按钮与更多文章。', 'aksrt')] ); } @@ -24,9 +24,18 @@ class WidgetUserCard extends \WP_Widget { $show_stats = !empty($instance['show_stats']) || !isset($instance['show_stats']); $show_actions = !empty($instance['show_button']) || !isset($instance['show_button']); $show_checkin = !empty($instance['show_checkin']) || !isset($instance['show_checkin']); + // zibll user widget forces show_posts=false; author widget enables it. + // AKSRT keeps optional show_posts for current-user card (default off). + $show_posts = !empty($instance['show_posts']); $loged_title = (string) ($instance['loged_title'] ?? __('Hi!请登录', 'aksrt')); $btn1_text = (string) ($instance['button_1_text'] ?? __('发布文章', 'aksrt')); $btn2_text = (string) ($instance['button_2_text'] ?? __('用户中心', 'aksrt')); + $post_style = sanitize_key((string) ($instance['post_style'] ?? 'mini')); + if (!in_array($post_style, ['card', 'mini'], true)) { + $post_style = 'mini'; + } + $limit = max(0, min(12, (int) ($instance['limit'] ?? 6))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); // Fixed user id wins; else current user. $user_id = !empty($instance['user_id']) ? (int) $instance['user_id'] : 0; @@ -63,7 +72,6 @@ class WidgetUserCard extends \WP_Widget { ? aksrt_get_user_center_url() : $profile_url; $compose_url = home_url('/'); - // Prefer front submit / write page when available. if (function_exists('get_pages')) { $pages = get_pages(['meta_key' => '_wp_page_template', 'meta_value' => 'templates/pages/page-submit.php', 'number' => 1]); if (!empty($pages[0])) { @@ -90,14 +98,32 @@ class WidgetUserCard extends \WP_Widget { $following_count = \AKSRT\User\Follow::get_instance()->get_following_count($user_id); } + $name_html = function_exists('aksrt_get_user_name') + ? aksrt_get_user_name(['id' => $user_id, 'follow' => !$is_self, 'class' => 'flex ac jc']) + : '' . esc_html($user->display_name) . ''; + echo '
    '; if ($show_cover) { - $bg = $cover !== '' ? ' style="background-image:url(' . esc_url($cover) . ')"' : ''; - echo '
    '; + if ($cover !== '') { + echo '
    '; + } else { + // Prefer site default cover image when available. + $default_cover = ''; + if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_cover_default')) { + $default_cover = (string) \AKSRT\Settings\Settings::get_user_cover_default(); + } + if ($default_cover === '' && function_exists('get_theme_mod')) { + $default_cover = (string) get_theme_mod('aksrt_user_cover', ''); + } + if ($default_cover !== '') { + echo '
    '; + } else { + echo '
    '; + } + } } echo '
    '; if ($show_checkin && $is_self && function_exists('aksrt_header_user_checkin_btn')) { - // Reuse compact check-in button markup (without details if already long). $btn = aksrt_header_user_checkin_btn($user_id); if ($btn !== '') { echo '
    ' . $btn . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped @@ -106,7 +132,7 @@ class WidgetUserCard extends \WP_Widget { echo '
    '; echo '' . esc_attr($user->display_name) . ''; echo '
    '; - echo '

    ' . esc_html($user->display_name) . '

    '; + echo '

    ' . $name_html . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($bio !== '') { echo '

    ' . esc_html(wp_trim_words($bio, 24)) . '

    '; } @@ -125,6 +151,18 @@ class WidgetUserCard extends \WP_Widget { } elseif ($show_actions) { echo ''; } + + if ($show_posts && $limit > 0 && function_exists('aksrt_render_user_card_posts')) { + echo aksrt_render_user_card_posts([ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'user_id' => $user_id, + 'limit' => $limit, + 'orderby' => $orderby, + 'post_style' => $post_style, + 'post_type' => 'post', + 'show_empty' => false, + ]); + } + echo '
    '; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped @@ -149,7 +187,15 @@ class WidgetUserCard extends \WP_Widget { echo '
    '; if ($show_cover) { - echo '
    '; + $default_cover = ''; + if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_cover_default')) { + $default_cover = (string) \AKSRT\Settings\Settings::get_user_cover_default(); + } + if ($default_cover !== '') { + echo '
    '; + } else { + echo '
    '; + } } echo '
    '; echo '
    '; @@ -167,6 +213,10 @@ class WidgetUserCard extends \WP_Widget { } public function form($instance): void { + $post_style = sanitize_key((string) ($instance['post_style'] ?? 'mini')); + if (!in_array($post_style, ['card', 'mini'], true)) { + $post_style = 'mini'; + } ?>

    +

    + +

    +

    + +

    +

    +

    sanitize_text_field((string) ($new_instance['title'] ?? '')), - 'user_id' => max(0, (int) ($new_instance['user_id'] ?? 0)), - 'loged_title' => sanitize_text_field((string) ($new_instance['loged_title'] ?? '')), - 'button_1_text' => sanitize_text_field((string) ($new_instance['button_1_text'] ?? '')), - 'button_2_text' => sanitize_text_field((string) ($new_instance['button_2_text'] ?? '')), - 'show_img_bg' => !empty($new_instance['show_img_bg']) ? 1 : 0, - 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, - 'show_button' => !empty($new_instance['show_button']) ? 1 : 0, - 'show_checkin' => !empty($new_instance['show_checkin']) ? 1 : 0, + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'user_id' => max(0, (int) ($new_instance['user_id'] ?? 0)), + 'loged_title' => sanitize_text_field((string) ($new_instance['loged_title'] ?? '')), + 'button_1_text' => sanitize_text_field((string) ($new_instance['button_1_text'] ?? '')), + 'button_2_text' => sanitize_text_field((string) ($new_instance['button_2_text'] ?? '')), + 'show_img_bg' => !empty($new_instance['show_img_bg']) ? 1 : 0, + 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, + 'show_button' => !empty($new_instance['show_button']) ? 1 : 0, + 'show_checkin' => !empty($new_instance['show_checkin']) ? 1 : 0, + 'show_posts' => !empty($new_instance['show_posts']) ? 1 : 0, + 'limit' => max(0, min(12, (int) ($new_instance['limit'] ?? 6))), + 'post_style' => $post_style, + 'orderby' => $orderby, ]; } } diff --git a/src/AKSRT/Widgets/WidgetUserRanking.php b/src/AKSRT/Widgets/WidgetUserRanking.php index b8fb394d..4cf6eada 100644 --- a/src/AKSRT/Widgets/WidgetUserRanking.php +++ b/src/AKSRT/Widgets/WidgetUserRanking.php @@ -25,10 +25,9 @@ class WidgetUserRanking extends \WP_Widget { $desc_mode = sanitize_key((string) ($instance['desc'] ?? 'desc')); $users = $this->get_ranking($rank_by, $count, $desc_mode); - if (empty($users)) return; - echo $args['before_widget']; - echo $args['before_title'] . esc_html($title) . $args['after_title']; + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if (!empty($instance['checkin_btn']) && is_user_logged_in() && function_exists('aksrt_header_user_checkin_btn')) { $cbtn = aksrt_header_user_checkin_btn(get_current_user_id()); @@ -37,6 +36,14 @@ class WidgetUserRanking extends \WP_Widget { } } + if (empty($users)) { + echo '

    ' . esc_html__('暂无排行数据', 'aksrt') . '

    '; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + + $top_badge = !empty($instance['top_badge']); + echo '
      '; $rank = 0; foreach ($users as $user) { @@ -46,7 +53,12 @@ class WidgetUserRanking extends \WP_Widget { ? aksrt_get_user_home_url($user['id']) : get_author_posts_url($user['id']); - echo '
    • '; + echo '
    • '; + + if ($top_badge && $rank <= 3) { + $tone = $rank === 1 ? 'jb-red' : ($rank === 2 ? 'jb-yellow' : 'b-gray'); + echo 'TOP' . (int) $rank . ''; + } // 排名 echo '' . esc_html((string) $rank) . ''; @@ -76,7 +88,7 @@ class WidgetUserRanking extends \WP_Widget { } echo '
    '; - echo $args['after_widget']; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } private function get_ranking(string $rank_by, int $count, string $desc_mode = 'desc'): array { @@ -287,6 +299,12 @@ class WidgetUserRanking extends \WP_Widget {

    +

    + +

    Date: Tue, 21 Jul 2026 08:04:03 +0800 Subject: [PATCH 3/3] feat(widgets): route WidgetSlider through shared aksrt_render_slider Replace one-off fade/carousel inline script with .new-swiper stack, dual height/effect options, and admin empty-state for misconfigured slides. --- src/AKSRT/Widgets/WidgetSlider.php | 173 ++++++++++++++--------------- 1 file changed, 85 insertions(+), 88 deletions(-) diff --git a/src/AKSRT/Widgets/WidgetSlider.php b/src/AKSRT/Widgets/WidgetSlider.php index fa9ad2b6..4dc35535 100644 --- a/src/AKSRT/Widgets/WidgetSlider.php +++ b/src/AKSRT/Widgets/WidgetSlider.php @@ -1,6 +1,6 @@ esc_html__('多图轮播,支持链接与标题。', 'aksrt'), - 'classname' => 'widget_aksrt_slider', + 'description' => esc_html__('多图轮播(共享 new-swiper / Swiper 栈),支持链接与标题。', 'aksrt'), + 'classname' => 'widget_aksrt_slider slide-widget', ] ); } @@ -25,14 +25,38 @@ class WidgetSlider extends \WP_Widget { public function widget($args, $instance): void { $slides = $this->normalize_slides($instance['slides'] ?? []); if (!$slides) { + if (current_user_can('manage_options')) { + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
    ' . esc_html__('幻灯片模块:', 'aksrt') . '' . + esc_html__('当前配置下没有可显示的内容,请正确配置或移出该模块', 'aksrt') . '
    '; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } return; } + $interval = max(2, min(20, (int) ($instance['interval'] ?? 4))); - $loop = !empty($instance['loop']); - $dots = !empty($instance['pagination']); - $nav = !empty($instance['button']); + $loop = !isset($instance['loop']) || !empty($instance['loop']); + $dots = !isset($instance['pagination']) || !empty($instance['pagination']); + $nav = !isset($instance['button']) || !empty($instance['button']); $blank = !empty($instance['blank']); $height = max(80, min(800, (int) ($instance['height'] ?? 220))); + $m_height = max(60, min(800, (int) ($instance['m_height'] ?? $height))); + $effect = sanitize_key((string) ($instance['effect'] ?? 'slide')); + if (!in_array($effect, ['slide', 'fade', 'cube', 'coverflow', 'flip'], true)) { + $effect = 'slide'; + } + + $slide_payload = []; + foreach ($slides as $slide) { + $slide_payload[] = [ + 'image' => $slide['image'], + 'link' => $slide['href'], + 'title' => $slide['title'], + 'desc' => $slide['desc'], + 'alt' => $slide['title'], + 'target' => $blank ? '_blank' : '_self', + ]; + } echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); @@ -40,97 +64,47 @@ class WidgetSlider extends \WP_Widget { echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - $uid = 'aksrt-slider-' . esc_attr($this->id); - printf( - '
    ', - $uid, - $interval * 1000, - $loop ? '1' : '0', - $height - ); - echo '
    '; - foreach ($slides as $i => $slide) { - $active = $i === 0 ? ' is-active' : ''; - echo '
    '; - $img = '' . esc_attr($slide['title']) . ''; - if ($slide['href'] !== '') { - printf( - '%s', - esc_url($slide['href']), - $blank ? ' target="_blank" rel="noopener noreferrer"' : '', - $img // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - ); - } else { - echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - if ($slide['title'] !== '' || $slide['desc'] !== '') { - echo '
    '; - if ($slide['title'] !== '') { - echo '' . esc_html($slide['title']) . ''; - } - if ($slide['desc'] !== '') { - echo '' . esc_html($slide['desc']) . ''; + if (function_exists('aksrt_render_slider')) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo aksrt_render_slider([ + 'slides' => $slide_payload, + 'effect' => $effect, + 'loop' => $loop, + 'autoplay' => true, + 'interval' => $interval * 1000, + 'pc_height' => $height . 'px', + 'm_height' => $m_height . 'px', + 'show_nav' => $nav, + 'show_pagination' => $dots, + 'slider_class' => 'slide-widget', + 'wrapper_class' => 'aksrt-widget-slider-wrap', + ]); + } else { + // Minimal fallback without helper. + echo '
    '; + foreach ($slide_payload as $i => $slide) { + $active = $i === 0 ? ' is-active' : ''; + echo '
    '; + $img = '' . esc_attr($slide['title']) . ''; + if ($slide['link'] !== '') { + echo '' . $img . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo '
    '; } echo '
    '; } - echo '
    '; - if ($nav) { - echo ''; - echo ''; - } - if ($dots) { - echo '
    '; - foreach ($slides as $i => $slide) { - printf( - '', - $i === 0 ? ' is-active' : '', - (int) $i, - esc_attr(sprintf(__('第 %d 张', 'aksrt'), $i + 1)) - ); - } - echo '
    '; - } - echo '
    '; - echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - // Inline init once per widget (no extra enqueue dependency). - ?> - - normalize_slides($instance['slides'] ?? []); if (!$slides) { $slides = [ @@ -150,10 +124,26 @@ class WidgetSlider extends \WP_Widget {

    -

    +

    + +

    +

    + +



    @@ -177,9 +167,7 @@ class WidgetSlider extends \WP_Widget {

    -

    +

    + normalize_slides($new_instance['slides'] ?? []); + $effect = sanitize_key((string) ($new_instance['effect'] ?? 'slide')); + if (!in_array($effect, ['slide', 'fade', 'cube', 'coverflow', 'flip'], true)) { + $effect = 'slide'; + } return [ 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), 'interval' => max(2, min(20, (int) ($new_instance['interval'] ?? 4))), 'height' => max(80, min(800, (int) ($new_instance['height'] ?? 220))), + 'm_height' => max(60, min(800, (int) ($new_instance['m_height'] ?? ($new_instance['height'] ?? 220)))), + 'effect' => $effect, 'loop' => !empty($new_instance['loop']) ? 1 : 0, 'button' => !empty($new_instance['button']) ? 1 : 0, 'pagination' => !empty($new_instance['pagination']) ? 1 : 0,