From 9356faf13d91fffba882ec1f704fc0b7a9c6db5c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:29:37 +0800 Subject: [PATCH] feat(widgets): feature intro, term cards, paid posts lists Close more zibll produck/posts widget gaps: media-tab feature intro, taxonomy term cards, and paid/credits content list with CSS. --- assets/css/frontend/widget-batch3d.css | 163 ++++++++++++++++ assets/css/frontend/widgets.css | 1 + inc/modules/widgets-register.php | 3 + src/AKSRT/Widgets/WidgetFeatureIntro.php | 227 +++++++++++++++++++++++ src/AKSRT/Widgets/WidgetPaidPosts.php | 140 ++++++++++++++ src/AKSRT/Widgets/WidgetTermCards.php | 166 +++++++++++++++++ 6 files changed, 700 insertions(+) create mode 100644 assets/css/frontend/widget-batch3d.css create mode 100644 src/AKSRT/Widgets/WidgetFeatureIntro.php create mode 100644 src/AKSRT/Widgets/WidgetPaidPosts.php create mode 100644 src/AKSRT/Widgets/WidgetTermCards.php diff --git a/assets/css/frontend/widget-batch3d.css b/assets/css/frontend/widget-batch3d.css new file mode 100644 index 00000000..d9fe3fcd --- /dev/null +++ b/assets/css/frontend/widget-batch3d.css @@ -0,0 +1,163 @@ +/** + * Batch3d: feature intro, term cards, paid posts. + */ + +.aksrt-fi { + display: grid; + grid-template-columns: 1fr 1.1fr; + gap: 28px; + align-items: center; + margin-bottom: 20px; +} +.aksrt-fi.media-left { grid-template-columns: 1.1fr 1fr; } +.aksrt-fi.media-left .aksrt-fi__text { order: 2; } +.aksrt-fi.media-left .aksrt-fi__media { order: 1; } +@media (max-width: 991.98px) { + .aksrt-fi, + .aksrt-fi.media-left { + grid-template-columns: 1fr; + } + .aksrt-fi.media-left .aksrt-fi__text, + .aksrt-fi.media-left .aksrt-fi__media { order: initial; } +} +.aksrt-fi__title { + font-size: 28px; + line-height: 1.25; + margin: 0 0 12px; +} +.aksrt-fi__desc { + font-size: 14px; + color: var(--muted-2-color, #666); + line-height: 1.7; + margin-bottom: 14px; +} +.aksrt-fi__stat { + display: flex; + align-items: baseline; + gap: 6px; + margin-bottom: 14px; +} +.aksrt-fi__stat-num { + font-size: 36px; + font-weight: 800; + color: var(--color-primary, #3b82f6); + line-height: 1; +} +.aksrt-fi__stat-plus { font-size: 22px; font-weight: 700; color: var(--color-primary, #3b82f6); } +.aksrt-fi__stat-label { font-size: 13px; color: var(--muted-2-color, #888); margin-left: 4px; } +.aksrt-fi__tabs { display: flex; flex-wrap: wrap; gap: 8px; } +.aksrt-fi__tab { + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + border-radius: 999px; + padding: 6px 14px; + font-size: 13px; + cursor: pointer; +} +.aksrt-fi__tab.is-active { + background: var(--color-primary, #3b82f6); + border-color: transparent; + color: #fff; +} +.aksrt-fi__panel { display: none; } +.aksrt-fi__panel.is-active { display: block; } +.aksrt-fi__browser { + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-2, #f3f4f6); + box-shadow: 0 12px 32px rgba(0,0,0,.08); +} +.aksrt-fi__browser.is-plain { box-shadow: none; } +.aksrt-fi__browser-bar { + height: 28px; + background: #eef1f5; + display: flex; + align-items: center; + gap: 6px; + padding: 0 12px; +} +.aksrt-fi__browser-bar span { + width: 8px; + height: 8px; + border-radius: 50%; + background: #d0d5dd; +} +.aksrt-fi__browser-bar span:nth-child(1) { background: #ff5f57; } +.aksrt-fi__browser-bar span:nth-child(2) { background: #febc2e; } +.aksrt-fi__browser-bar span:nth-child(3) { background: #28c840; } +.aksrt-fi__browser-body { + position: relative; + width: 100%; + height: 0; + background: #111; +} +.aksrt-fi__img, +.aksrt-fi__video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.aksrt-term-cards { + display: grid; + grid-template-columns: repeat(var(--aksrt-tc-cols, 2), minmax(0, 1fr)); + gap: 12px; + margin-bottom: 12px; +} +.aksrt-term-card { + display: block; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + text-decoration: none; + color: inherit; + background: var(--surface-1, #fff); +} +.aksrt-term-card__cover { + height: 88px; + background: var(--surface-2, #f3f4f6) center/cover no-repeat; +} +.aksrt-term-card__cover.is-empty { + background: linear-gradient(135deg, #dbeafe, #eff6ff); +} +.aksrt-term-card__body { padding: 10px 12px 12px; } +.aksrt-term-card__name { font-weight: 700; font-size: 14px; } +.aksrt-term-card__count { font-size: 12px; color: var(--muted-2-color, #888); margin-top: 2px; } + +.aksrt-paid-posts { list-style: none; margin: 0; padding: 0; } +.aksrt-paid-posts__item { + display: flex; + gap: 10px; + padding: 8px 0; + border-bottom: 1px dashed var(--color-line, #e5e7eb); +} +.aksrt-paid-posts__item:last-child { border-bottom: 0; } +.aksrt-paid-posts__thumb { + width: 56px; + height: 56px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + background: var(--surface-2, #eee); +} +.aksrt-paid-posts__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-paid-posts__body { min-width: 0; flex: 1; } +.aksrt-paid-posts__title { + display: block; + color: inherit; + text-decoration: none; + font-size: 13px; + font-weight: 600; + line-height: 1.4; +} +.aksrt-paid-posts__price { + display: inline-block; + margin-top: 4px; + font-size: 12px; + color: #e11d48; + font-weight: 700; +} diff --git a/assets/css/frontend/widgets.css b/assets/css/frontend/widgets.css index 6661bdc1..88c6fa77 100644 --- a/assets/css/frontend/widgets.css +++ b/assets/css/frontend/widgets.css @@ -5,3 +5,4 @@ @import url("widget-slider.css"); @import url("widget-batch3.css"); @import url("widget-batch3c.css"); +@import url("widget-batch3d.css"); diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index 2fa14103..7d7690df 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -68,6 +68,9 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetStats'); register_widget('\AKSRT\Widgets\WidgetReviews'); register_widget('\AKSRT\Widgets\WidgetHighlights'); + register_widget('\AKSRT\Widgets\WidgetFeatureIntro'); + register_widget('\AKSRT\Widgets\WidgetTermCards'); + register_widget('\AKSRT\Widgets\WidgetPaidPosts'); // Forum widgets diff --git a/src/AKSRT/Widgets/WidgetFeatureIntro.php b/src/AKSRT/Widgets/WidgetFeatureIntro.php new file mode 100644 index 00000000..3435deb1 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetFeatureIntro.php @@ -0,0 +1,227 @@ + esc_html__('左文右图/视频的 TAB 功能介绍模块。', 'aksrt'), + 'classname' => 'widget_aksrt_feature_intro', + ] + ); + } + + public function widget($args, $instance): void { + $slides = $this->normalize_slides($instance['slides'] ?? []); + if (!$slides) { + return; + } + $heading = trim((string) ($instance['heading'] ?? '')); + $desc = trim((string) ($instance['description'] ?? '')); + $media_pos = (($instance['media_position'] ?? 'right') === 'left') ? 'left' : 'right'; + $chrome = !empty($instance['browser_chrome']) || !isset($instance['browser_chrome']); + $ratio = max(30, min(100, (int) ($instance['media_ratio'] ?? 66))); + $show_stat = !empty($instance['show_stat']); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $uid = 'aksrt-fi-' . esc_attr($this->id); + + echo '
'; + echo '
'; + if ($heading !== '') { + echo '

' . esc_html($heading) . '

'; + } + if ($desc !== '') { + echo '
' . wp_kses_post(wpautop($desc)) . '
'; + } + if ($show_stat) { + $sn = sanitize_text_field((string) ($instance['stat_number'] ?? '')); + $ss = sanitize_text_field((string) ($instance['stat_suffix'] ?? '+')); + $sl = sanitize_text_field((string) ($instance['stat_label'] ?? '')); + if ($sn !== '') { + echo '
' . esc_html($sn) . ''; + if ($ss !== '') { + echo '' . esc_html($ss) . ''; + } + if ($sl !== '') { + echo '' . esc_html($sl) . ''; + } + echo '
'; + } + } + if (count($slides) > 1) { + echo '
'; + foreach ($slides as $i => $s) { + $cls = $i === 0 ? ' is-active' : ''; + $label = $s['tab_label'] !== '' ? $s['tab_label'] : sprintf(__('面板 %d', 'aksrt'), $i + 1); + echo ''; + } + echo '
'; + } + echo '
'; + + echo '
'; + foreach ($slides as $i => $s) { + $cls = $i === 0 ? ' is-active' : ''; + echo '
'; + $box_cls = $chrome ? 'aksrt-fi__browser' : 'aksrt-fi__browser is-plain'; + echo '
'; + if ($chrome) { + echo ''; + } + echo '
'; + if ($s['media_type'] === 'video' && $s['video_url'] !== '') { + $poster = $s['video_poster'] !== '' ? ' poster="' . esc_url($s['video_poster']) . '"' : ''; + echo ''; + } elseif ($s['image'] !== '') { + echo '' . esc_attr($s['tab_label']) . ''; + } + echo '
'; + } + echo '
'; + + if (count($slides) > 1) { + ?> + + > + */ + private function normalize_slides($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; + } + $type = sanitize_key((string) ($row['media_type'] ?? 'image')); + if ($type !== 'video') { + $type = 'image'; + } + $image = esc_url_raw(trim((string) ($row['image'] ?? ''))); + $video = esc_url_raw(trim((string) ($row['video_url'] ?? ''))); + if ($type === 'video' && $video === '') { + continue; + } + if ($type === 'image' && $image === '') { + continue; + } + $out[] = [ + 'tab_label' => sanitize_text_field((string) ($row['tab_label'] ?? '')), + 'media_type' => $type, + 'image' => $image, + 'video_url' => $video, + 'video_poster' => esc_url_raw(trim((string) ($row['video_poster'] ?? ''))), + ]; + if (count($out) >= 8) { + break; + } + } + return $out; + } + + public function form($instance): void { + $json = is_string($instance['slides'] ?? null) + ? (string) $instance['slides'] + : wp_json_encode($this->normalize_slides($instance['slides'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+
+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['heading'] ?? '')), + 'description' => wp_kses_post((string) ($new_instance['description'] ?? '')), + 'slides' => $this->normalize_slides($new_instance['slides'] ?? []), + 'media_position' => ($new_instance['media_position'] ?? '') === 'left' ? 'left' : 'right', + 'media_ratio' => max(30, min(100, (int) ($new_instance['media_ratio'] ?? 66))), + 'browser_chrome' => !empty($new_instance['browser_chrome']) ? 1 : 0, + 'show_stat' => !empty($new_instance['show_stat']) ? 1 : 0, + 'stat_number' => sanitize_text_field((string) ($new_instance['stat_number'] ?? '')), + 'stat_suffix' => sanitize_text_field((string) ($new_instance['stat_suffix'] ?? '')), + 'stat_label' => sanitize_text_field((string) ($new_instance['stat_label'] ?? '')), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetPaidPosts.php b/src/AKSRT/Widgets/WidgetPaidPosts.php new file mode 100644 index 00000000..21472e78 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetPaidPosts.php @@ -0,0 +1,140 @@ + esc_html__('展示开启付费/积分可见的文章。', 'aksrt'), + 'classname' => 'widget_aksrt_paid_posts', + ] + ); + } + + public function widget($args, $instance): void { + $count = max(1, min(20, (int) ($instance['count'] ?? 6))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + + $query = [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => $count, + 'order' => 'DESC', + 'meta_query' => [ + 'relation' => 'OR', + [ + 'key' => '_aksrt_pay_price', + 'value' => 0, + 'compare' => '>', + 'type' => 'NUMERIC', + ], + [ + 'key' => '_aksrt_credits_cost', + 'value' => 0, + 'compare' => '>', + 'type' => 'NUMERIC', + ], + [ + 'key' => '_aksrt_visibility', + 'value' => ['pay', 'credits'], + 'compare' => 'IN', + ], + ], + ]; + if ($orderby === 'views') { + $query['meta_key'] = '_aksrt_views'; + $query['orderby'] = 'meta_value_num'; + } elseif ($orderby === 'price') { + $query['meta_key'] = '_aksrt_pay_price'; + $query['orderby'] = 'meta_value_num'; + } else { + $query['orderby'] = 'date'; + } + + $q = new \WP_Query($query); + if (!$q->have_posts()) { + wp_reset_postdata(); + 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 + } + + echo ''; + wp_reset_postdata(); + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(20, (int) ($new_instance['count'] ?? 6))), + 'orderby' => $orderby, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetTermCards.php b/src/AKSRT/Widgets/WidgetTermCards.php new file mode 100644 index 00000000..3fd1cf40 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetTermCards.php @@ -0,0 +1,166 @@ + esc_html__('以卡片展示分类或标签。', 'aksrt'), + 'classname' => 'widget_aksrt_term_cards', + ] + ); + } + + public function widget($args, $instance): void { + $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 = $this->parse_ids((string) ($instance['include'] ?? '')); + $hide_empty = !isset($instance['hide_empty']) || !empty($instance['hide_empty']); + + $term_args = [ + 'taxonomy' => $taxonomy, + 'number' => $number, + 'hide_empty' => $hide_empty, + ]; + if ($include) { + $term_args['include'] = $include; + $term_args['orderby'] = 'include'; + } else { + $term_args['orderby'] = in_array($orderby, ['name', 'count', 'id'], true) ? $orderby : 'count'; + $term_args['order'] = $orderby === 'name' ? 'ASC' : 'DESC'; + } + + $terms = get_terms($term_args); + if (is_wp_error($terms) || !$terms) { + 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 + } + + $cols = max(1, min(4, (int) ($instance['columns'] ?? 2))); + echo '
'; + foreach ($terms as $term) { + $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); + } + } + echo ''; + if ($thumb !== '') { + echo '
'; + } else { + echo '
'; + } + echo '
'; + echo '
' . esc_html($term->name) . '
'; + echo '
' . esc_html(sprintf(_n('%d 篇', '%d 篇', (int) $term->count, 'aksrt'), (int) $term->count)) . '
'; + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** @return array */ + private function parse_ids(string $raw): array { + if ($raw === '') { + return []; + } + $ids = []; + foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) { + $id = (int) $p; + if ($id > 0) { + $ids[] = $id; + } + } + return array_values(array_unique($ids)); + } + + public function form($instance): void { + $taxs = get_taxonomies(['public' => true], 'objects'); + ?> +

+ +

+

+ +

+

+ + +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'taxonomy' => sanitize_key((string) ($new_instance['taxonomy'] ?? 'category')), + 'number' => max(1, min(30, (int) ($new_instance['number'] ?? 8))), + 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))), + 'orderby' => in_array(($new_instance['orderby'] ?? ''), ['count', 'name', 'id'], true) ? $new_instance['orderby'] : 'count', + 'include' => sanitize_text_field((string) ($new_instance['include'] ?? '')), + 'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0, + ]; + } +}