From 47d202bb96de99606c09a517c44f9a45c5d74ed1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:25:43 +0800 Subject: [PATCH] feat(widgets): reviews + highlights carousel cards; plate-aware board list Add zibll reviews/highlights marketing widgets with CSS, and make the legacy forum board-list widget prefer aksrt_forum_plate CPT. --- assets/css/frontend/widget-batch3c.css | 110 +++++++++++++ assets/css/frontend/widgets.css | 1 + inc/modules/widgets-register.php | 2 + src/AKSRT/Widgets/WidgetForumBoardList.php | 75 ++++++--- src/AKSRT/Widgets/WidgetHighlights.php | 177 +++++++++++++++++++++ src/AKSRT/Widgets/WidgetReviews.php | 149 +++++++++++++++++ 6 files changed, 491 insertions(+), 23 deletions(-) create mode 100644 assets/css/frontend/widget-batch3c.css create mode 100644 src/AKSRT/Widgets/WidgetHighlights.php create mode 100644 src/AKSRT/Widgets/WidgetReviews.php diff --git a/assets/css/frontend/widget-batch3c.css b/assets/css/frontend/widget-batch3c.css new file mode 100644 index 00000000..4614fd89 --- /dev/null +++ b/assets/css/frontend/widget-batch3c.css @@ -0,0 +1,110 @@ +/** + * Batch3c: reviews + highlights. + */ + +.aksrt-reviews { + margin-bottom: 16px; + --aksrt-rev-pc: 3; + --aksrt-rev-m: 1.2; +} +.aksrt-reviews__track { + display: grid; + grid-auto-flow: column; + grid-auto-columns: calc((100% - (var(--aksrt-rev-pc) - 1) * 12px) / var(--aksrt-rev-pc)); + gap: 12px; + overflow-x: auto; + scroll-snap-type: x mandatory; + padding-bottom: 6px; + -webkit-overflow-scrolling: touch; +} +@media (max-width: 767.98px) { + .aksrt-reviews__track { + grid-auto-columns: calc((100% - (var(--aksrt-rev-m) - 1) * 12px) / var(--aksrt-rev-m)); + } +} +.aksrt-reviews__slide { scroll-snap-align: start; min-width: 0; } +.aksrt-reviews__card { + position: relative; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 12px; + padding: 16px; + background: var(--surface-1, #fff); + min-height: 100%; +} +.aksrt-reviews__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } +.aksrt-reviews__avatar { + width: 44px; + height: 44px; + border-radius: 50%; + object-fit: cover; + background: var(--surface-2, #eee); + display: inline-flex; + align-items: center; + justify-content: center; + color: #999; + flex-shrink: 0; +} +.aksrt-reviews__name { font-weight: 700; font-size: 14px; } +.aksrt-reviews__name a { color: inherit; text-decoration: none; } +.aksrt-reviews__role { font-size: 12px; color: var(--muted-2-color, #888); } +.aksrt-reviews__stars { margin-bottom: 8px; font-size: 12px; } +.aksrt-reviews__stars .is-on { color: #f5a623; } +.aksrt-reviews__stars .is-off { color: #ddd; } +.aksrt-reviews__content { + font-size: 13px; + line-height: 1.55; + color: var(--color-ink-2, #444); + margin: 0; +} +.aksrt-reviews__quote { + position: absolute; + right: 14px; + bottom: 10px; + opacity: .12; + font-size: 28px; +} + +.aksrt-highlights { margin-bottom: 16px; display: flex; flex-direction: column; gap: 12px; } +.aksrt-highlights__row { + display: grid; + gap: 12px; +} +.aksrt-highlights__row.is-large { grid-template-columns: 1fr; } +.aksrt-highlights__row.is-medium { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.aksrt-highlights__row.is-small { grid-template-columns: repeat(3, minmax(0, 1fr)); } +@media (max-width: 767.98px) { + .aksrt-highlights__row.is-medium, + .aksrt-highlights__row.is-small { grid-template-columns: 1fr; } +} +.aksrt-hl-card { + display: flex; + gap: 12px; + border-radius: 12px; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + padding: 14px; + text-decoration: none; + color: inherit; + overflow: hidden; + min-height: 100%; +} +.aksrt-hl-card.color-blue { --hl: #3b82f6; } +.aksrt-hl-card.color-pink { --hl: #ec4899; } +.aksrt-hl-card.color-purple { --hl: #a855f7; } +.aksrt-hl-card.color-green { --hl: #22c55e; } +.aksrt-hl-card.color-orange { --hl: #f97316; } +.aksrt-hl-card.color-cyan { --hl: #06b6d4; } +.aksrt-hl-card.size-large { min-height: 140px; } +.aksrt-hl-card__media { + width: 96px; + flex-shrink: 0; + border-radius: 10px; + overflow: hidden; + background: var(--surface-2, #f3f4f6); +} +.aksrt-hl-card.size-large .aksrt-hl-card__media { width: 160px; } +.aksrt-hl-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; min-height: 80px; } +.aksrt-hl-card__body { min-width: 0; flex: 1; display: flex; flex-direction: column; } +.aksrt-hl-card__icon { color: var(--hl, #3b82f6); font-size: 22px; margin-bottom: 8px; } +.aksrt-hl-card__title { font-weight: 700; font-size: 15px; margin-bottom: 4px; } +.aksrt-hl-card__desc { font-size: 12px; color: var(--muted-2-color, #888); line-height: 1.5; } diff --git a/assets/css/frontend/widgets.css b/assets/css/frontend/widgets.css index 97ccca5b..6661bdc1 100644 --- a/assets/css/frontend/widgets.css +++ b/assets/css/frontend/widgets.css @@ -4,3 +4,4 @@ @import url("widgets/part-4.css"); @import url("widget-slider.css"); @import url("widget-batch3.css"); +@import url("widget-batch3c.css"); diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index 885cdc44..2fa14103 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -66,6 +66,8 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetPricingPlans'); register_widget('\AKSRT\Widgets\WidgetFaq'); register_widget('\AKSRT\Widgets\WidgetStats'); + register_widget('\AKSRT\Widgets\WidgetReviews'); + register_widget('\AKSRT\Widgets\WidgetHighlights'); // Forum widgets diff --git a/src/AKSRT/Widgets/WidgetForumBoardList.php b/src/AKSRT/Widgets/WidgetForumBoardList.php index 5ee4dc9d..5cd9d5d0 100644 --- a/src/AKSRT/Widgets/WidgetForumBoardList.php +++ b/src/AKSRT/Widgets/WidgetForumBoardList.php @@ -26,31 +26,60 @@ class WidgetForumBoardList extends \WP_Widget { echo $args['before_widget']; echo $args['before_title'] . esc_html($title) . $args['after_title']; - $boards = get_terms([ - 'taxonomy' => 'aksrt_forum_board', - 'hide_empty' => true, - 'orderby' => 'meta_value_num', - 'meta_key' => '_aksrt_sort_order', - 'order' => 'ASC', - ]); - - if (!is_wp_error($boards) && !empty($boards)) { - echo ''; - } else { - echo '

' . esc_html__('暂无版块', 'aksrt') . '

'; } echo $args['after_widget']; diff --git a/src/AKSRT/Widgets/WidgetHighlights.php b/src/AKSRT/Widgets/WidgetHighlights.php new file mode 100644 index 00000000..f9f1d12f --- /dev/null +++ b/src/AKSRT/Widgets/WidgetHighlights.php @@ -0,0 +1,177 @@ + esc_html__('大中小尺寸亮点/卖点卡片栅格。', 'aksrt'), + 'classname' => 'widget_aksrt_highlights', + ] + ); + } + + public function widget($args, $instance): void { + $cards = $this->normalize($instance['cards'] ?? []); + if (!$cards) { + return; + } + $go_link = !empty($instance['go_link']); + + 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 + } + + $large = array_values(array_filter($cards, static fn($c) => ($c['size'] ?? 'medium') === 'large')); + $medium = array_values(array_filter($cards, static fn($c) => ($c['size'] ?? 'medium') === 'medium')); + $small = array_values(array_filter($cards, static fn($c) => ($c['size'] ?? 'medium') === 'small')); + // Fallback: if no size tags, treat all as medium. + if (!$large && !$medium && !$small) { + $medium = $cards; + } + + echo '
'; + if ($large) { + echo '
'; + foreach ($large as $c) { + echo $this->card($c, 'large', $go_link); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } + if ($medium) { + echo '
'; + foreach ($medium as $c) { + echo $this->card($c, 'medium', $go_link); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } + if ($small) { + echo '
'; + foreach ($small as $c) { + echo $this->card($c, 'small', $go_link); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param array $c + */ + private function card(array $c, string $size, bool $go_link): string { + $color = sanitize_key((string) ($c['color'] ?? 'blue')); + if (!in_array($color, ['blue', 'pink', 'purple', 'green', 'orange', 'cyan'], true)) { + $color = 'blue'; + } + $url = (string) ($c['url'] ?? ''); + if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $tag = $url !== '' ? 'a' : 'div'; + $href = $url !== '' ? ' href="' . esc_url($url) . '"' : ''; + $img = (string) ($c['image'] ?? ''); + $html = '<' . $tag . ' class="aksrt-hl-card size-' . esc_attr($size) . ' color-' . esc_attr($color) . '"' . $href . '>'; + if ($img !== '') { + $html .= '
'; + } + $html .= '
'; + if (!empty($c['icon'])) { + $html .= '
'; + } + if ($c['title'] !== '') { + $html .= '
' . esc_html($c['title']) . '
'; + } + if ($c['desc'] !== '') { + $html .= '
' . esc_html($c['desc']) . '
'; + } + $html .= '
'; + return $html; + } + + /** + * @param mixed $raw + * @return array> + */ + private function normalize($raw): array { + if (is_string($raw)) { + $decoded = json_decode($raw, true); + $raw = is_array($decoded) ? $decoded : []; + } + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (!is_array($row)) { + continue; + } + $title = sanitize_text_field((string) ($row['title'] ?? '')); + $desc = sanitize_text_field((string) ($row['desc'] ?? '')); + if ($title === '' && $desc === '') { + continue; + } + $size = sanitize_key((string) ($row['size'] ?? 'medium')); + if (!in_array($size, ['large', 'medium', 'small'], true)) { + $size = 'medium'; + } + $out[] = [ + 'title' => $title, + 'desc' => $desc, + 'icon' => sanitize_text_field((string) ($row['icon'] ?? '')), + 'image' => esc_url_raw(trim((string) ($row['image'] ?? ''))), + 'url' => esc_url_raw(trim((string) ($row['url'] ?? ''))), + 'color' => sanitize_key((string) ($row['color'] ?? 'blue')), + 'size' => $size, + ]; + if (count($out) >= 12) { + break; + } + } + return $out; + } + + public function form($instance): void { + $json = is_string($instance['cards'] ?? null) + ? (string) $instance['cards'] + : wp_json_encode($this->normalize($instance['cards'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ + +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'cards' => $this->normalize($new_instance['cards'] ?? []), + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetReviews.php b/src/AKSRT/Widgets/WidgetReviews.php new file mode 100644 index 00000000..370babb5 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetReviews.php @@ -0,0 +1,149 @@ + esc_html__('虚拟用户评价卡片横向滚动。', 'aksrt'), + 'classname' => 'widget_aksrt_reviews', + ] + ); + } + + public function widget($args, $instance): void { + $reviews = $this->normalize($instance['reviews'] ?? []); + if (!$reviews) { + return; + } + $pc = max(1, min(6, (float) ($instance['desktop_per_view'] ?? 3))); + $m = max(1, min(3, (float) ($instance['mobile_per_view'] ?? 1.2))); + + 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 + } + + $style = sprintf('--aksrt-rev-pc:%s;--aksrt-rev-m:%s;', rtrim(rtrim(number_format($pc, 2, '.', ''), '0'), '.'), rtrim(rtrim(number_format($m, 2, '.', ''), '0'), '.')); + echo '
'; + echo '
'; + foreach ($reviews as $r) { + echo '
'; + echo '
'; + if ($r['avatar'] !== '') { + echo ''; + } else { + echo ''; + } + echo '
'; + if ($r['name'] !== '') { + $name_html = esc_html($r['name']); + if ($r['url'] !== '') { + $name_html = '' . $name_html . ''; + } + echo '
' . $name_html . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + if ($r['role'] !== '') { + echo '
' . esc_html($r['role']) . '
'; + } + echo '
'; + echo '
'; + for ($i = 1; $i <= 5; $i++) { + echo ''; + } + echo '
'; + if ($r['content'] !== '') { + echo '

' . nl2br(esc_html($r['content'])) . '

'; + } + echo ''; + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param mixed $raw + * @return array + */ + private function normalize($raw): array { + if (is_string($raw)) { + $decoded = json_decode($raw, true); + $raw = is_array($decoded) ? $decoded : []; + } + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (!is_array($row)) { + continue; + } + $name = sanitize_text_field((string) ($row['name'] ?? '')); + $content = sanitize_textarea_field((string) ($row['content'] ?? '')); + if ($name === '' && $content === '') { + continue; + } + $out[] = [ + 'name' => $name, + 'role' => sanitize_text_field((string) ($row['role'] ?? '')), + 'avatar' => esc_url_raw(trim((string) ($row['avatar'] ?? ''))), + 'content' => $content, + 'rating' => max(0, min(5, (int) ($row['rating'] ?? 5))), + 'url' => esc_url_raw(trim((string) ($row['url'] ?? ''))), + ]; + if (count($out) >= 20) { + break; + } + } + return $out; + } + + public function form($instance): void { + $json = is_string($instance['reviews'] ?? null) + ? (string) $instance['reviews'] + : wp_json_encode($this->normalize($instance['reviews'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ + +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'reviews' => $this->normalize($new_instance['reviews'] ?? []), + 'desktop_per_view' => max(1, min(6, (float) ($new_instance['desktop_per_view'] ?? 3))), + 'mobile_per_view' => max(1, min(3, (float) ($new_instance['mobile_per_view'] ?? 1.2))), + ]; + } +}