From 3fd02a93382f2f48e046d854fa56bf0ba76c95c9 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Jul 2026 08:54:46 +0800 Subject: [PATCH 1/3] feat(widgets): shared aksrt_graphic_card + author minicard helpers Unlock term cover cards and user-list minicards with zibll-parity graphic (style-1..4, lazy, mask, height_scale) and author-minicard (follow + identity chips + bio) helpers. --- inc/bootstrap.php | 1 + inc/helpers/aksrt-graphic-card.php | 276 +++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 inc/helpers/aksrt-graphic-card.php diff --git a/inc/bootstrap.php b/inc/bootstrap.php index aa2e6c2d..b19a0bea 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-graphic-card.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'; diff --git a/inc/helpers/aksrt-graphic-card.php b/inc/helpers/aksrt-graphic-card.php new file mode 100644 index 00000000..93952de6 --- /dev/null +++ b/inc/helpers/aksrt-graphic-card.php @@ -0,0 +1,276 @@ + $args { + * @type string $type style-1|style-2|style-3|style-4|''. + * @type string $class Extra classes on .graphic. + * @type string $img Image URL. + * @type string $video Optional video URL (desktop). + * @type string $alt Image alt. + * @type array{url?:string,target?:string}|string $link Link. + * @type string $text1 Primary text. + * @type string $text2 Secondary text. + * @type string $text3 Tertiary / badge text (HTML allowed for icon+count). + * @type string $more Extra absolute HTML (badge etc). + * @type bool $lazy Use aksrt-lazy data-src. + * @type int|float $height_scale Padding-bottom % (0 = default CSS). + * @type int|float $mask_opacity 0–100 overlay opacity. + * @type string $hide pc|m|'' hide on device. + * } + * @param bool $echo Echo instead of return. + */ + function aksrt_graphic_card(array $args = [], bool $echo = false): string { + $defaults = [ + 'type' => '', + 'hide' => '', + 'class' => 'mb10', + 'img' => '', + 'video' => '', + 'alt' => __('图片', 'aksrt'), + 'link' => [ + 'url' => '', + 'target' => '', + ], + 'text1' => '', + 'text2' => '', + 'text3' => '', + 'more' => '', + 'lazy' => true, + 'height_scale' => 0, + 'mask_opacity' => 0, + ]; + $args = wp_parse_args($args, $defaults); + + $img_url = trim((string) $args['img']); + $video_url = trim((string) $args['video']); + if ($img_url === '' && $video_url === '') { + return ''; + } + + // Device visibility. + $hide = sanitize_key((string) $args['hide']); + if ($hide === 'pc' || $hide === 'm') { + $is_mobile = function_exists('wp_is_mobile') ? wp_is_mobile() : false; + if ((!$is_mobile && $hide === 'pc') || ($is_mobile && $hide === 'm')) { + return ''; + } + } + + $type = sanitize_key((string) $args['type']); + if ($type !== '' && !in_array($type, ['style-1', 'style-2', 'style-3', 'style-4'], true)) { + // Accept bare "1"–"4". + if (preg_match('/^[1-4]$/', $type)) { + $type = 'style-' . $type; + } else { + $type = ''; + } + } + + $link = $args['link']; + if (is_string($link)) { + $link = ['url' => $link, 'target' => '']; + } + if (!is_array($link)) { + $link = ['url' => '', 'target' => '']; + } + $link_url = esc_url(trim((string) ($link['url'] ?? ''))); + $link_target = sanitize_text_field((string) ($link['target'] ?? '')); + + $lazy = !empty($args['lazy']); + $placeholder = get_template_directory_uri() . '/assets/img/placeholder.svg'; + $media = ''; + if ($video_url !== '' && !(function_exists('wp_is_mobile') && wp_is_mobile())) { + $poster = $img_url !== '' ? ' poster="' . esc_url($img_url) . '"' : ''; + if ($lazy) { + $media = ''; + } else { + $media = ''; + } + } elseif ($img_url !== '') { + $alt = (string) ($args['alt'] !== '' ? $args['alt'] : __('图片', 'aksrt')); + if ($lazy) { + $media = '' . esc_attr($alt) . ''; + } else { + $media = '' . esc_attr($alt) . ''; + } + } + if ($media === '') { + return ''; + } + + $mask_opacity = max(0, min(100, (int) $args['mask_opacity'])); + $mask = $mask_opacity > 0 + ? '
' + : ''; + + // text1/2/3 may contain small trusted HTML (icons); sanitize lightly. + $text1 = (string) $args['text1']; + $text2 = (string) $args['text2']; + $text3 = (string) $args['text3']; + $allow = [ + 'i' => ['class' => true, 'aria-hidden' => true], + 'span' => ['class' => true, 'title' => true], + 'item' => ['class' => true, 'data-toggle' => true, 'title' => true], + 'b' => [], + 'strong'=> [], + 'em' => [], + 'badge' => ['class' => true], + ]; + $text1_safe = $text1 !== '' ? wp_kses($text1, $allow) : ''; + $text2_safe = $text2 !== '' ? wp_kses($text2, $allow) : ''; + $text3_safe = $text3 !== '' ? wp_kses($text3, $allow) : ''; + $more = (string) $args['more']; + $more_safe = $more !== '' ? wp_kses($more, array_merge($allow, [ + 'div' => ['class' => true, 'style' => true], + 'badge' => ['class' => true], + ])) : ''; + + $text = ''; + if ($type === 'style-2') { + $text = '
'; + $text .= $text1_safe !== '' ? '
' . $text1_safe . '
' : ''; + $text .= $text2_safe !== '' ? '
' . $text2_safe . '
' : ''; + $text .= '
'; + if ($text3_safe !== '') { + $text .= '
' . $text3_safe . '
'; + } + } elseif ($type === 'style-3') { + $text = $text1_safe !== '' ? '
' . $text1_safe . '
' : ''; + $text .= '
'; + $text .= $text2_safe !== '' ? '
' . $text2_safe . '
' : ''; + $text .= $text3_safe !== '' ? '
' . $text3_safe . '
' : ''; + $text .= '
'; + } elseif ($type === 'style-4') { + if ($text3_safe !== '') { + $text = '
' . $text3_safe . '
'; + } + } else { + // style-1 / default. + $inner = $text1_safe !== '' ? '
' . $text1_safe . '
' : ''; + $inner .= $text3_safe !== '' ? '
' . $text3_safe . '
' : ''; + $inner .= $text2_safe !== '' ? '
' . $text2_safe . '
' : ''; + $text = $inner !== '' ? '
' . $inner . '
' : ''; + } + $text .= $more_safe; + + $class = trim('graphic hover-zoom-img ' . (string) $args['class'] . ' ' . $type); + $height_scale = max(0, (int) $args['height_scale']); + $scale_attr = $height_scale > 0 + ? ' style="padding-bottom:' . esc_attr((string) $height_scale) . '%!important;"' + : ''; + + $html = ''; + if ($link_url !== '') { + $tgt = $link_target !== '' ? ' target="' . esc_attr($link_target) . '" rel="noopener noreferrer"' : ''; + $html .= ''; + } + if ($type === 'style-4') { + $html .= '
'; + } + $html .= '
'; + $html .= $media; + $html .= $mask; + $html .= $text; + if ($type === 'style-4') { + $html .= '
'; // close .graphic early for style-4 body. + $html .= '
'; + $html .= $text1_safe !== '' ? '
' . $text1_safe . '
' : ''; + $html .= $text2_safe !== '' ? '
' . $text2_safe . '
' : ''; + $html .= '
'; // padding + main-shadow + } else { + $html .= ''; + } + if ($link_url !== '') { + $html .= '
'; + } + + if ($echo) { + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return ''; + } + return $html; + } +endif; + +if (!function_exists('aksrt_author_minicard')) : + /** + * Author mini card for user lists (zibll zib_author_card). + * + * @param int $user_id + * @param string $class + * @param bool $blur_bg + */ + function aksrt_author_minicard(int $user_id, string $class = '', bool $blur_bg = false): string { + if ($user_id <= 0) { + return ''; + } + $user = get_userdata($user_id); + if (!$user) { + return ''; + } + + $avatar_url = function_exists('aksrt_get_avatar_url') + ? aksrt_get_avatar_url($user_id, ['size' => 96]) + : get_avatar_url($user_id, ['size' => 96]); + $home = function_exists('aksrt_get_user_home_url') + ? aksrt_get_user_home_url($user_id) + : get_author_posts_url($user_id); + + $avatar_box = '
'; + + $name = function_exists('aksrt_get_user_name') + ? aksrt_get_user_name([ + 'id' => $user_id, + 'class' => 'flex ac flex1', + 'follow' => false, + 'medal' => true, + 'auth' => true, + 'level' => true, + ]) + : '' . esc_html($user->display_name) . ''; + + // Follow control (separate from name, zibll places both in dt). + $follow = ''; + $current = get_current_user_id(); + if ($current !== $user_id) { + if (is_user_logged_in()) { + $follow = ''; + } else { + $login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(); + $follow = ' ' . esc_html__('关注', 'aksrt') . ''; + } + } + + $desc = trim((string) get_user_meta($user_id, 'description', true)); + if ($desc === '') { + $desc = __('这家伙很懒,什么都没有写…', 'aksrt'); + } + + $blur = ''; + if ($blur_bg) { + $blur = '
'; + } + + return '
' + . $blur + . '
'; + } +endif; From 1fdd2301fd8f06614f63637703e7e124b4eb9a5d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Jul 2026 08:54:46 +0800 Subject: [PATCH 2/3] feat(widgets): OnelinePosts via swiper-scroll + shared post renderer Replace private track DOM with slider.js Type A shell and aksrt_render_post cards/mini/list slides, matching product oneline and zibll oneline posts. --- src/AKSRT/Widgets/WidgetOnelinePosts.php | 116 ++++++++++++++++++----- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/src/AKSRT/Widgets/WidgetOnelinePosts.php b/src/AKSRT/Widgets/WidgetOnelinePosts.php index f5299348..e942f9bb 100644 --- a/src/AKSRT/Widgets/WidgetOnelinePosts.php +++ b/src/AKSRT/Widgets/WidgetOnelinePosts.php @@ -1,6 +1,8 @@ esc_html__('横向滚动的文章卡片/迷你列表。', 'aksrt'), + 'description' => esc_html__('横向滚动的文章卡片/迷你列表(共享 swiper-scroll)。', 'aksrt'), 'classname' => 'widget_aksrt_oneline_posts', ] ); } + /** + * @param array $instance + * @return array + */ public static function build_query_args(array $instance): array { - $count = max(1, min(20, (int) ($instance['count'] ?? 8))); - $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); - $cat = (int) ($instance['cat'] ?? 0); + $count = max(1, min(20, (int) ($instance['count'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $cat = (int) ($instance['cat'] ?? 0); $limit_day = max(0, (int) ($instance['limit_day'] ?? 0)); - $query = [ + $query = [ 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => $count, @@ -43,28 +49,32 @@ class WidgetOnelinePosts extends \WP_Widget { switch ($orderby) { case 'views': $query['meta_key'] = 'aksrt_post_views'; - $query['orderby'] = 'meta_value_num'; - $query['order'] = 'DESC'; + $query['orderby'] = 'meta_value_num'; + $query['order'] = 'DESC'; break; case 'comments': $query['orderby'] = 'comment_count'; - $query['order'] = 'DESC'; + $query['order'] = 'DESC'; break; case 'rand': $query['orderby'] = 'rand'; break; default: $query['orderby'] = 'date'; - $query['order'] = 'DESC'; + $query['order'] = 'DESC'; } return $query; } + /** + * @param array $instance + */ public static function render_html(array $instance): string { $style = sanitize_key((string) ($instance['style'] ?? 'card')); if (!in_array($style, ['card', 'mini', 'list'], true)) { $style = 'card'; } + $q = new \WP_Query(self::build_query_args($instance)); if (!$q->have_posts()) { wp_reset_postdata(); @@ -72,29 +82,75 @@ class WidgetOnelinePosts extends \WP_Widget { ? aksrt_get_ajax_null(__('暂无文章', 'aksrt')) : '

' . esc_html__('暂无文章', 'aksrt') . '

'; } - ob_start(); - echo '
'; - while ($q->have_posts()) { - $q->the_post(); - $thumb = get_the_post_thumbnail_url(get_the_ID(), $style === 'card' ? 'medium' : 'thumbnail'); - echo ''; - if ($thumb) { - echo '
'; - } else { - echo '
'; + + $mode = $style === 'mini' ? 'mini' : ($style === 'list' ? 'list' : 'card'); + $cfg = [ + 'mode' => $mode, + 'context' => 'widget', + 'show_thumb' => true, + 'show_meta' => $style !== 'mini', + 'show_excerpt' => false, + 'show_author' => false, + 'show_comments' => false, + 'show_likes' => false, + 'show_tags' => false, + 'show_badges' => $style === 'card', + 'skip_wrapper' => true, + 'extra_class' => 'posts-item', + ]; + + $slides = ''; + if (function_exists('aksrt_render_post')) { + while ($q->have_posts()) { + $q->the_post(); + $item = aksrt_render_post($cfg); + if ($item === '') { + continue; + } + // Ensure freeMode width calc has a posts-item root when renderer wraps differently. + if (strpos($item, 'posts-item') === false && strpos($item, 'posts-mini') === false) { + $item = '
' . $item . '
'; + } + $slides .= '
' . $item . '
'; } - echo '
'; + wp_reset_postdata(); } + + if ($slides === '') { + return function_exists('aksrt_get_ajax_null') + ? aksrt_get_ajax_null(__('暂无文章', 'aksrt')) + : '

' . esc_html__('暂无文章', 'aksrt') . '

'; + } + + // zibll: .widget-main-post.style-{card|mini|list} + .swiper-container.swiper-scroll data-slideClass="posts-item". + ob_start(); + echo '
'; + echo '
'; + echo '
'; + echo $slides; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + echo '
'; + echo '
'; echo '
'; - wp_reset_postdata(); return (string) ob_get_clean(); } + /** + * @param array $args + * @param array $instance + */ public function widget($args, $instance): void { $html = self::render_html($instance); if ($html === '') { @@ -112,6 +168,9 @@ class WidgetOnelinePosts extends \WP_Widget { echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * @param array $instance + */ public function form($instance): void { ?>

@@ -156,6 +215,11 @@ class WidgetOnelinePosts extends \WP_Widget { $new_instance + * @param array $old_instance + * @return array + */ public function update($new_instance, $old_instance): array { $style = sanitize_key((string) ($new_instance['style'] ?? 'card')); if (!in_array($style, ['card', 'mini', 'list'], true)) { From d31b3c9f91023f43a2d4ab55897dfa0b4c701cac Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Jul 2026 08:54:47 +0800 Subject: [PATCH 3/3] feat(widgets): user minicards + term graphic cards/aggregation depth User lists render author-minicard rows; term cards/aggregation consume aksrt_graphic_card with style-1..4, meta CTA, and orderby-aware post rows. --- assets/css/frontend/widget-batch5.css | 270 ++++++++++++++++++++ src/AKSRT/Widgets/WidgetTermAggregation.php | 208 ++++++++++++--- src/AKSRT/Widgets/WidgetTermCards.php | 203 ++++++++++++--- src/AKSRT/Widgets/WidgetUserLists.php | 72 ++++-- 4 files changed, 644 insertions(+), 109 deletions(-) diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index a51c6701..ded92c65 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -1432,3 +1432,273 @@ .aksrt-product-widget--oneline .aksrt-product-widget__grid { /* legacy flex fallback if swiper unavailable */ } + +/* ===== Batch13: oneline posts swiper-scroll + user minicards + term graphic ===== */ + +.widget-main-post.aksrt-oneline-posts { + margin-bottom: 16px; +} +.aksrt-oneline-swiper { + position: relative; + overflow: hidden; + padding: 0 28px 4px; +} +.aksrt-oneline-swiper .swiper-wrapper { + display: flex; + align-items: stretch; +} +.aksrt-oneline-swiper .swiper-slide { + width: auto; + max-width: 46%; + min-width: 160px; + height: auto; +} +.widget-main-post.style-mini .aksrt-oneline-swiper .swiper-slide { + max-width: 280px; + min-width: 220px; +} +.widget-main-post.style-list .aksrt-oneline-swiper .swiper-slide { + max-width: 320px; + min-width: 240px; +} +.aksrt-oneline-swiper .posts-item, +.aksrt-oneline-swiper .posts-mini { + height: 100%; + margin: 0 8px 0 0; +} +.aksrt-oneline-swiper .swiper-button-prev, +.aksrt-oneline-swiper .swiper-button-next { + width: 28px; + height: 28px; + margin-top: 0; + top: 42%; + border-radius: 50%; + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + color: var(--color-ink, #111); + box-shadow: 0 2px 8px rgba(0,0,0,.06); +} +.aksrt-oneline-swiper .swiper-button-prev:after, +.aksrt-oneline-swiper .swiper-button-next:after { + font-size: 12px; + font-weight: 700; +} +.aksrt-oneline-swiper .swiper-button-prev { left: 0; } +.aksrt-oneline-swiper .swiper-button-next { right: 0; } + +/* User lists → author-minicard */ +.aksrt-user-lists .user_lists { + display: flex; + flex-direction: column; + gap: 10px; + text-align: left; +} +.aksrt-user-lists .author-minicard { + display: block; + padding: 12px 14px; + border-radius: 8px; + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + overflow: hidden; +} +.aksrt-user-lists.is-bare .author-minicard { + background: transparent; +} +.aksrt-user-lists .author-minicard > ul.list-inline { + display: flex; + align-items: center; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} +.aksrt-user-lists .author-minicard > ul.list-inline > li { + margin: 0; + list-style: none; +} +.aksrt-user-lists .author-minicard .avatar-img img.avatar { + width: 42px; + height: 42px; + border-radius: 50%; + object-fit: cover; + display: block; +} +.aksrt-user-lists .author-minicard dl { + margin: 0; + min-width: 0; +} +.aksrt-user-lists .author-minicard dt.flex { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; +} +.aksrt-user-lists .author-minicard dd { + margin: 0; + max-width: 100%; +} +.aksrt-user-lists .author-minicard .abs-blur-bg { + position: absolute; + inset: 0; + overflow: hidden; + z-index: 0; + opacity: .18; + pointer-events: none; +} +.aksrt-user-lists .author-minicard .abs-blur-bg img { + width: 100%; + height: 100%; + object-fit: cover; + filter: blur(18px); + transform: scale(1.2); +} +.aksrt-user-lists .author-minicard .relative { + position: relative; + z-index: 1; +} + +/* Graphic card polish */ +.graphic { + background: var(--surface-2, #f3f4f6); +} +.graphic .graphic-mask { + position: absolute; + inset: 0; + background: #000; + pointer-events: none; + z-index: 1; +} +.graphic .graphic-text, +.graphic .abs-center { + z-index: 2; + padding: 10px 12px; +} +.graphic .title-h-center { + font-size: 16px; + font-weight: 700; +} +.graphic badge.b-black, +.graphic .img-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + background: rgba(0,0,0,.55); + color: #fff; + font-size: 12px; + line-height: 1.4; +} +.graphic video.fit-cover { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} +.hover-zoom-img .graphic img.fit-cover, +.hover-zoom-img.graphic img.fit-cover { + transition: transform .45s ease; +} +.hover-zoom-img:hover .graphic img.fit-cover, +.hover-zoom-img.graphic:hover img.fit-cover { + transform: scale(1.06); +} +.aksrt-term-cards .row.gutters-5 { + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +} +.aksrt-term-cards .row.gutters-5 > [class*="col-"] { + padding: 0 4px; + box-sizing: border-box; +} +.aksrt-term-cards .col-sm-12 { width: 100%; } +.aksrt-term-cards .col-sm-6 { width: 50%; } +.aksrt-term-cards .col-sm-4 { width: 33.333%; } +.aksrt-term-cards .col-sm-3 { width: 25%; } +.aksrt-term-cards .col-sm-2 { width: 16.666%; } +@media (max-width: 767px) { + .aksrt-term-cards .col-xs-12 { width: 100%; } + .aksrt-term-cards .col-xs-6 { width: 50%; } + .aksrt-term-cards .col-xs-4 { width: 33.333%; } +} + +/* Term aggregation (zibll term-aggregation) */ +.term-aggregation.aksrt-term-agg__card, +.zib-widget.term-aggregation { + padding: 14px; + border-radius: 10px; + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + margin-bottom: 12px; +} +.aksrt-term-agg { + display: grid; + gap: 12px; + grid-template-columns: repeat(var(--aksrt-ta-cols, 1), minmax(0, 1fr)); +} +.term-aggregation .hover-zoom-img.flex { + display: flex; + align-items: stretch; + gap: 0; +} +.term-aggregation .term-img { + width: 38%; + max-width: 180px; + min-width: 110px; +} +.term-aggregation .term-img .graphic { + margin: 0; + border-radius: 8px; +} +.term-aggregation .term-title { + min-width: 0; + padding-left: 4px; +} +.term-aggregation .term-title > a { + font-weight: 700; + text-decoration: none; + color: inherit; +} +.term-aggregation .term-meta { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-top: 6px; +} +.term-aggregation .icon-circle { + position: relative; + padding-left: 14px; + color: inherit; + text-decoration: none; +} +.term-aggregation .icon-circle:before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--theme-color, var(--focus-color, #f04494)); + transform: translateY(-50%); +} +.term-aggregation .but.c-blue { + display: inline-flex; + align-items: center; + gap: 2px; + border-radius: 999px; + padding: 2px 10px; + font-size: 12px; + background: rgba(59,130,246,.1); + color: #2563eb; + text-decoration: none; +} +@media (max-width: 640px) { + .aksrt-term-agg { grid-template-columns: 1fr; } + .term-aggregation .term-img { max-width: 120px; min-width: 88px; width: 34%; } + .term-aggregation .hide-sm { display: none !important; } + .aksrt-oneline-swiper .swiper-slide { max-width: 72%; min-width: 140px; } +} diff --git a/src/AKSRT/Widgets/WidgetTermAggregation.php b/src/AKSRT/Widgets/WidgetTermAggregation.php index fed1a4f1..8f614013 100644 --- a/src/AKSRT/Widgets/WidgetTermAggregation.php +++ b/src/AKSRT/Widgets/WidgetTermAggregation.php @@ -1,6 +1,6 @@ esc_html__('分类封面 + 最新文章列表聚合卡。', 'aksrt'), + 'description' => esc_html__('分类封面 + 描述 + 更多文章 CTA + 文章列表聚合卡。', 'aksrt'), 'classname' => 'widget_aksrt_term_aggregation', ] ); @@ -37,15 +37,68 @@ class WidgetTermAggregation extends \WP_Widget { return array_values(array_unique($ids)); } + /** + * @param \WP_Term $term + */ + private static function term_cover_url(\WP_Term $term): string { + if (class_exists(WidgetTermCards::class) && method_exists(WidgetTermCards::class, 'term_cover_url')) { + return WidgetTermCards::term_cover_url($term); + } + $img_id = (int) get_term_meta($term->term_id, 'thumbnail_id', true); + if (!$img_id) { + $img_id = (int) get_term_meta($term->term_id, '_aksrt_cover', true); + } + if ($img_id) { + $url = wp_get_attachment_image_url($img_id, 'medium'); + if ($url) { + return (string) $url; + } + } + $url = (string) get_term_meta($term->term_id, '_aksrt_cover_url', true); + return $url !== '' ? $url : get_template_directory_uri() . '/assets/img/placeholder.svg'; + } + + /** + * Sum of post views under a term (best-effort). + */ + private static function term_views_sum(int $term_id, string $taxonomy): int { + $q = new \WP_Query([ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => 50, + 'fields' => 'ids', + 'no_found_rows' => true, + 'update_post_meta_cache' => true, + 'update_post_term_cache' => false, + 'tax_query' => [[ + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $term_id, + ]], + ]); + $sum = 0; + foreach ($q->posts as $pid) { + $sum += function_exists('aksrt_get_post_views') + ? (int) aksrt_get_post_views((int) $pid) + : (int) get_post_meta((int) $pid, 'aksrt_post_views', true); + } + return $sum; + } + + /** + * @param array $instance + */ public static function render_html(array $instance): string { $taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'category')); if (!taxonomy_exists($taxonomy)) { $taxonomy = 'category'; } $term_ids = self::parse_ids_static((string) ($instance['term_ids'] ?? '')); - $count = max(1, min(12, (int) ($instance['count'] ?? 4))); - $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); - $columns = max(1, min(3, (int) ($instance['columns'] ?? 1))); + $count = max(1, min(12, (int) ($instance['count'] ?? 4))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $columns = max(1, min(3, (int) ($instance['columns'] ?? 1))); + $target_blank = !empty($instance['target_blank']); + $tgt = $target_blank ? ' target="_blank" rel="noopener noreferrer"' : ''; if (!$term_ids) { $terms = get_terms([ @@ -74,16 +127,38 @@ class WidgetTermAggregation extends \WP_Widget { if (is_wp_error($link)) { continue; } - $cover = ''; - $img_id = (int) get_term_meta($term_id, 'thumbnail_id', true); - if (!$img_id) { - $img_id = (int) get_term_meta($term_id, '_aksrt_cover', true); + + $cover = self::term_cover_url($term); + $views = self::term_views_sum((int) $term->term_id, $taxonomy); + $more_badge = ' ' . esc_html((string) $views) . ''; + + $img_html = ''; + if (function_exists('aksrt_graphic_card')) { + $img_html = aksrt_graphic_card([ + 'type' => '', + 'class' => '', + 'img' => $cover, + 'alt' => $term->name, + 'link' => [ + 'url' => $link, + 'target' => $target_blank ? '_blank' : '', + ], + 'lazy' => true, + 'height_scale' => 70, + 'mask_opacity' => 0, + 'more' => $more_badge, + ]); } - if ($img_id) { - $cover = wp_get_attachment_image_url($img_id, 'medium') ?: ''; - } - if ($cover === '') { - $cover = (string) get_term_meta($term_id, '_aksrt_cover_url', true); + + $description = trim((string) $term->description); + if ($description === '' && current_user_can('manage_categories')) { + $edit = get_edit_term_link((int) $term->term_id, $taxonomy); + $description = esc_html__('请在后台为该分类添加描述。', 'aksrt'); + if ($edit) { + $description .= ' ' . esc_html__('立即编辑', 'aksrt') . ''; + } + } else { + $description = esc_html($description); } $posts_q = [ @@ -91,39 +166,75 @@ class WidgetTermAggregation extends \WP_Widget { 'post_status' => 'publish', 'posts_per_page' => $count, 'ignore_sticky_posts' => true, - 'no_found_rows' => true, + 'no_found_rows' => false, 'tax_query' => [[ 'taxonomy' => $taxonomy, 'field' => 'term_id', - 'terms' => $term_id, + 'terms' => (int) $term->term_id, ]], ]; if ($orderby === 'views') { $posts_q['meta_key'] = 'aksrt_post_views'; - $posts_q['orderby'] = 'meta_value_num'; - $posts_q['order'] = 'DESC'; + $posts_q['orderby'] = 'meta_value_num'; + $posts_q['order'] = 'DESC'; + } elseif ($orderby === 'like') { + $posts_q['meta_key'] = 'aksrt_post_likes'; + $posts_q['orderby'] = 'meta_value_num'; + $posts_q['order'] = 'DESC'; + } elseif ($orderby === 'comment_count') { + $posts_q['orderby'] = 'comment_count'; + $posts_q['order'] = 'DESC'; } elseif ($orderby === 'modified') { $posts_q['orderby'] = 'modified'; - $posts_q['order'] = 'DESC'; + $posts_q['order'] = 'DESC'; } else { $posts_q['orderby'] = 'date'; - $posts_q['order'] = 'DESC'; + $posts_q['order'] = 'DESC'; } $q = new \WP_Query($posts_q); + $found = (int) $q->found_posts; + + echo '

'; + echo '
'; + echo '
' . $img_html . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + echo '' . esc_html($term->name) . ''; + echo '
' . $description . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + echo '' + . esc_html(sprintf( + /* translators: %s: article count */ + __('%s篇文章', 'aksrt'), + (string) $found + )) + . ''; + echo '' . esc_html__('更多文章', 'aksrt') . ''; + echo '
'; - echo '
'; - echo ''; - echo '
'; - echo '
' . esc_html($term->name) . '
'; - echo '
' . esc_html(sprintf(_n('%d 篇', '%d 篇', (int) $term->count, 'aksrt'), (int) $term->count)) . '
'; - echo '
'; if ($q->have_posts()) { - echo '
    '; while ($q->have_posts()) { $q->the_post(); - echo '
  • ' . esc_html(get_the_title()) . '
  • '; + $pid = get_the_ID(); + $title = get_the_title($pid); + $meta = ''; + if ($orderby === 'views') { + $v = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($pid) : (int) get_post_meta($pid, 'aksrt_post_views', true); + $meta = '' . esc_html((string) $v); + } elseif ($orderby === 'like') { + $v = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($pid) : (int) get_post_meta($pid, 'aksrt_post_likes', true); + $meta = '' . esc_html((string) $v); + } elseif ($orderby === 'comment_count') { + $meta = '' . esc_html((string) get_comments_number($pid)); + } else { + $ts = $orderby === 'modified' ? get_the_modified_time('U', $pid) : get_the_time('U', $pid); + $ago = human_time_diff((int) $ts, current_time('timestamp')); + $meta = '' . esc_html($ago); + } + echo '
    '; + echo ''; + echo '
    ' . $meta . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
    '; } - echo '
'; wp_reset_postdata(); } echo '
'; @@ -132,6 +243,10 @@ class WidgetTermAggregation extends \WP_Widget { return (string) ob_get_clean(); } + /** + * @param array $args + * @param array $instance + */ public function widget($args, $instance): void { $html = self::render_html($instance); if ($html === '') { @@ -146,11 +261,9 @@ class WidgetTermAggregation extends \WP_Widget { echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - /** @return array */ - private function parse_ids(string $raw): array { - return self::parse_ids_static($raw); - } - + /** + * @param array $instance + */ public function form($instance): void { $taxs = get_taxonomies(['public' => true], 'objects'); ?> @@ -187,25 +300,36 @@ class WidgetTermAggregation extends \WP_Widget { + +

+

+ +

$new_instance + * @param array $old_instance + * @return array + */ public function update($new_instance, $old_instance): array { $orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'date')); - if (!in_array($orderby, ['date', 'modified', 'views'], true)) { + if (!in_array($orderby, ['date', 'modified', 'views', 'like', 'comment_count'], true)) { $orderby = 'date'; } return [ - 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), - 'taxonomy' => sanitize_key((string) ($new_instance['taxonomy'] ?? 'category')), - 'term_ids' => sanitize_text_field((string) ($new_instance['term_ids'] ?? '')), - 'count' => max(1, min(12, (int) ($new_instance['count'] ?? 4))), - 'columns' => max(1, min(3, (int) ($new_instance['columns'] ?? 1))), - 'orderby' => $orderby, - 'auto_terms' => max(1, min(4, (int) ($new_instance['auto_terms'] ?? 2))), + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'taxonomy' => sanitize_key((string) ($new_instance['taxonomy'] ?? 'category')), + 'term_ids' => sanitize_text_field((string) ($new_instance['term_ids'] ?? '')), + 'count' => max(1, min(12, (int) ($new_instance['count'] ?? 4))), + 'columns' => max(1, min(3, (int) ($new_instance['columns'] ?? 1))), + 'orderby' => $orderby, + 'auto_terms' => max(1, min(4, (int) ($new_instance['auto_terms'] ?? 2))), + 'target_blank' => !empty($new_instance['target_blank']) ? 1 : 0, ]; } } diff --git a/src/AKSRT/Widgets/WidgetTermCards.php b/src/AKSRT/Widgets/WidgetTermCards.php index dd881738..02311366 100644 --- a/src/AKSRT/Widgets/WidgetTermCards.php +++ b/src/AKSRT/Widgets/WidgetTermCards.php @@ -1,6 +1,6 @@ esc_html__('以卡片展示分类或标签。', 'aksrt'), + 'description' => esc_html__('以图文封面卡片展示分类或标签(style-1..4)。', 'aksrt'), 'classname' => 'widget_aksrt_term_cards', ] ); @@ -37,15 +37,49 @@ class WidgetTermCards extends \WP_Widget { return array_values(array_unique($ids)); } + /** + * Resolve term cover URL. + */ + public static function term_cover_url(\WP_Term $term): string { + $thumb = ''; + $img_id = (int) get_term_meta($term->term_id, 'thumbnail_id', true); + if (!$img_id) { + $img_id = (int) get_term_meta($term->term_id, '_aksrt_cover', true); + } + if ($img_id) { + $thumb = (string) (wp_get_attachment_image_url($img_id, 'medium_large') ?: wp_get_attachment_image_url($img_id, 'medium') ?: ''); + } + if ($thumb === '') { + $thumb = (string) get_term_meta($term->term_id, '_aksrt_cover_url', true); + } + if ($thumb === '') { + // Lightweight default gradient placeholder as data URI is heavy — use theme placeholder. + $thumb = get_template_directory_uri() . '/assets/img/placeholder.svg'; + } + return $thumb; + } + + /** + * @param array $instance + */ public static function render_html(array $instance): string { $taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'category')); if (!taxonomy_exists($taxonomy)) { $taxonomy = 'category'; } - $number = max(1, min(30, (int) ($instance['number'] ?? 8))); - $orderby = sanitize_key((string) ($instance['orderby'] ?? 'count')); - $include = self::parse_ids_static((string) ($instance['include'] ?? '')); + $number = max(1, min(30, (int) ($instance['number'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'count')); + $include = self::parse_ids_static((string) ($instance['include'] ?? '')); $hide_empty = !isset($instance['hide_empty']) || !empty($instance['hide_empty']); + $type = sanitize_key((string) ($instance['type'] ?? 'style-1')); + if (!in_array($type, ['style-1', 'style-2', 'style-3', 'style-4'], true)) { + $type = 'style-1'; + } + $height_scale = max(30, min(120, (int) ($instance['height_scale'] ?? 60))); + $mask_opacity = max(0, min(80, (int) ($instance['mask_opacity'] ?? 10))); + $pc_row = max(1, min(6, (int) ($instance['pc_row'] ?? $instance['columns'] ?? 2))); + $m_row = max(1, min(3, (int) ($instance['m_row'] ?? 1))); + $target_blank = !empty($instance['target_blank']); $term_args = [ 'taxonomy' => $taxonomy, @@ -67,42 +101,94 @@ class WidgetTermCards extends \WP_Widget { : '

' . esc_html__('暂无分类', 'aksrt') . '

'; } - $cols = max(1, min(4, (int) ($instance['columns'] ?? 2))); + $icon = $taxonomy === 'category' + ? '' + : ''; + + $is_row = count($terms) > 1; + // Bootstrap-like col classes already used elsewhere; keep numeric CSS vars as fallback. + $pc_span = max(1, (int) floor(12 / $pc_row)); + $m_span = max(1, (int) floor(12 / $m_row)); + $row_class = 'col-sm-' . $pc_span . ($m_row > 1 ? ' col-xs-' . $m_span : ''); + ob_start(); - echo '
'; + echo '
'; + if ($is_row) { + echo '
'; + } foreach ($terms as $term) { + if (!($term instanceof \WP_Term)) { + continue; + } $link = get_term_link($term); if (is_wp_error($link)) { continue; } - $thumb = ''; - if (function_exists('get_term_meta')) { - $img_id = (int) get_term_meta($term->term_id, 'thumbnail_id', true); - if (!$img_id) { - $img_id = (int) get_term_meta($term->term_id, '_aksrt_cover', true); - } - if ($img_id) { - $thumb = wp_get_attachment_image_url($img_id, 'medium') ?: ''; - } - if ($thumb === '') { - $thumb = (string) get_term_meta($term->term_id, '_aksrt_cover_url', true); + $name = mb_strlen($term->name) > 16 ? mb_substr($term->name, 0, 16) . '…' : $term->name; + $desc = trim((string) $term->description); + if (mb_strlen($desc) > 24) { + $desc = mb_substr($desc, 0, 24) . '…'; + } + $count = (int) $term->count; + $count_label = sprintf( + /* translators: %d: post count */ + _n('%d篇文章', '%d篇文章', $count, 'aksrt'), + $count + ); + + $text1 = $name; + $text2 = $desc; + $text3 = $icon . esc_html($count_label); + if ($type === 'style-2' || $type === 'style-4') { + $text3 = '' . $icon . $count . ''; + } + if ($type === 'style-3' || $type === 'style-4') { + $text1 = $icon . esc_html($name); + if ($type === 'style-3') { + $text3 = '' . esc_html($count_label); } } - echo ''; - if ($thumb !== '') { - echo '
'; + + $card_args = [ + 'type' => $type, + 'class' => 'mb10', + 'img' => self::term_cover_url($term), + 'alt' => $name . ($desc !== '' ? '-' . $desc : ''), + 'link' => [ + 'url' => $link, + 'target' => $target_blank ? '_blank' : '', + ], + 'text1' => $text1, + 'text2' => $text2, + 'text3' => $text3, + 'lazy' => true, + 'height_scale' => $height_scale, + 'mask_opacity' => $mask_opacity, + ]; + + if ($is_row) { + echo '
'; + } + if (function_exists('aksrt_graphic_card')) { + echo aksrt_graphic_card($card_args); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { - echo '
'; + echo '
' . esc_html($name) . '
'; } - echo '
'; - echo '
' . esc_html($term->name) . '
'; - echo '
' . esc_html(sprintf(_n('%d 篇', '%d 篇', (int) $term->count, 'aksrt'), (int) $term->count)) . '
'; - echo '
'; + if ($is_row) { + echo '
'; + } + } + if ($is_row) { + echo '
'; } echo '
'; return (string) ob_get_clean(); } + /** + * @param array $args + * @param array $instance + */ public function widget($args, $instance): void { $html = self::render_html($instance); if ($html === '') { @@ -117,11 +203,9 @@ class WidgetTermCards extends \WP_Widget { echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - /** @return array */ - private function parse_ids(string $raw): array { - return self::parse_ids_static($raw); - } - + /** + * @param array $instance + */ public function form($instance): void { $taxs = get_taxonomies(['public' => true], 'objects'); ?> @@ -143,8 +227,29 @@ class WidgetTermCards extends \WP_Widget { -