From 2f81e2a0b5fd33c16ef932a6af1675e8dfbee2ab Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:35:00 +0800 Subject: [PATCH] feat(widgets): oneline posts scroller + tilt showcase cases Add remaining high-traffic zibll marketing widgets: horizontal posts rail and left-copy/right-image tilt cases, with CSS registration. --- assets/css/frontend/widget-batch3f.css | 157 ++++++++++++++ assets/css/frontend/widgets.css | 1 + inc/modules/widgets-register.php | 2 + src/AKSRT/Widgets/WidgetOnelinePosts.php | 163 ++++++++++++++ src/AKSRT/Widgets/WidgetTiltCases.php | 258 +++++++++++++++++++++++ 5 files changed, 581 insertions(+) create mode 100644 assets/css/frontend/widget-batch3f.css create mode 100644 src/AKSRT/Widgets/WidgetOnelinePosts.php create mode 100644 src/AKSRT/Widgets/WidgetTiltCases.php diff --git a/assets/css/frontend/widget-batch3f.css b/assets/css/frontend/widget-batch3f.css new file mode 100644 index 00000000..5ad9a74d --- /dev/null +++ b/assets/css/frontend/widget-batch3f.css @@ -0,0 +1,157 @@ +/** + * Batch3f: oneline posts + tilt cases. + */ + +.aksrt-oneline { margin-bottom: 12px; } +.aksrt-oneline__track { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(180px, 220px); + gap: 12px; + overflow-x: auto; + scroll-snap-type: x mandatory; + padding-bottom: 6px; + -webkit-overflow-scrolling: touch; +} +.aksrt-oneline.style-mini .aksrt-oneline__track { + grid-auto-columns: minmax(140px, 160px); +} +.aksrt-oneline__item { + display: flex; + flex-direction: column; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + text-decoration: none; + color: inherit; + scroll-snap-align: start; + min-width: 0; +} +.aksrt-oneline.style-list .aksrt-oneline__item { + flex-direction: row; + align-items: center; + gap: 10px; + padding: 8px; +} +.aksrt-oneline__thumb { + height: 110px; + background: var(--surface-2, #f3f4f6) center/cover no-repeat; + overflow: hidden; +} +.aksrt-oneline.style-mini .aksrt-oneline__thumb { height: 90px; } +.aksrt-oneline.style-list .aksrt-oneline__thumb { + width: 64px; + height: 64px; + border-radius: 8px; + flex-shrink: 0; +} +.aksrt-oneline__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-oneline__thumb.is-empty { background: linear-gradient(135deg, #e2e8f0, #f8fafc); } +.aksrt-oneline__body { padding: 10px 12px 12px; min-width: 0; } +.aksrt-oneline.style-list .aksrt-oneline__body { padding: 0; } +.aksrt-oneline__title { + font-size: 13px; + font-weight: 600; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.aksrt-oneline__meta { margin-top: 4px; font-size: 11px; color: var(--muted-2-color, #999); } + +.aksrt-tilt { + display: grid; + grid-template-columns: 1fr 1.1fr; + gap: 28px; + align-items: center; + margin-bottom: 18px; +} +@media (max-width: 991.98px) { + .aksrt-tilt { grid-template-columns: 1fr; } +} +.aksrt-tilt__badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + padding: 4px 10px; + border-radius: 999px; + background: #eff6ff; + color: #2563eb; + margin-bottom: 12px; +} +.aksrt-tilt__title { + font-size: 30px; + line-height: 1.25; + margin: 0 0 12px; +} +.aksrt-tilt__hl { + font-style: normal; + background: linear-gradient(90deg, #3b82f6, #a855f7); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.aksrt-tilt__subtitle { + font-size: 14px; + color: var(--muted-2-color, #666); + line-height: 1.7; + margin: 0 0 14px; +} +.aksrt-tilt__features { + list-style: none; + margin: 0 0 16px; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + font-size: 13px; +} +.aksrt-tilt__features i { color: var(--color-primary, #3b82f6); margin-right: 4px; } +.aksrt-tilt__actions { display: flex; flex-wrap: wrap; gap: 10px; } +.aksrt-tilt__btn { + display: inline-flex; + align-items: center; + padding: 8px 16px; + border-radius: 999px; + background: var(--color-primary, #3b82f6); + color: #fff !important; + text-decoration: none; + font-size: 13px; + font-weight: 600; +} +.aksrt-tilt__showcase { + position: relative; + min-height: 260px; +} +.aksrt-tilt__img { + position: absolute; + border-radius: 14px; + overflow: hidden; + box-shadow: 0 16px 40px rgba(0,0,0,.12); + background: var(--surface-2, #f3f4f6); +} +.aksrt-tilt__img.is-main { + position: relative; + width: 100%; + aspect-ratio: 4/3; +} +.aksrt-tilt__img:not(.is-main) { + width: 42%; + aspect-ratio: 4/3; + right: -4%; + bottom: -8%; + z-index: 2; +} +.aksrt-tilt__img:nth-child(3) { + left: -4%; + top: -6%; + right: auto; + bottom: auto; + width: 38%; + z-index: 1; +} +.aksrt-tilt__img img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-tilt__img.is-empty { background: linear-gradient(135deg, #dbeafe, #ede9fe); } diff --git a/assets/css/frontend/widgets.css b/assets/css/frontend/widgets.css index 08a76fb6..69e0c933 100644 --- a/assets/css/frontend/widgets.css +++ b/assets/css/frontend/widgets.css @@ -7,3 +7,4 @@ @import url("widget-batch3c.css"); @import url("widget-batch3d.css"); @import url("widget-batch3e.css"); +@import url("widget-batch3f.css"); diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index d0c15335..8ba39126 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -72,6 +72,8 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetTermCards'); register_widget('\AKSRT\Widgets\WidgetPaidPosts'); register_widget('\AKSRT\Widgets\WidgetAuthorCard'); + register_widget('\AKSRT\Widgets\WidgetOnelinePosts'); + register_widget('\AKSRT\Widgets\WidgetTiltCases'); // Forum widgets diff --git a/src/AKSRT/Widgets/WidgetOnelinePosts.php b/src/AKSRT/Widgets/WidgetOnelinePosts.php new file mode 100644 index 00000000..98a445df --- /dev/null +++ b/src/AKSRT/Widgets/WidgetOnelinePosts.php @@ -0,0 +1,163 @@ + esc_html__('横向滚动的文章卡片/迷你列表。', 'aksrt'), + 'classname' => 'widget_aksrt_oneline_posts', + ] + ); + } + + public function widget($args, $instance): void { + $count = max(1, min(20, (int) ($instance['count'] ?? 8))); + $style = sanitize_key((string) ($instance['style'] ?? 'card')); + if (!in_array($style, ['card', 'mini', 'list'], true)) { + $style = 'card'; + } + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $cat = (int) ($instance['cat'] ?? 0); + $limit_day = max(0, (int) ($instance['limit_day'] ?? 0)); + + $query = [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => $count, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ]; + if ($cat > 0) { + $query['cat'] = $cat; + } + if ($limit_day > 0) { + $query['date_query'] = [['after' => $limit_day . ' days ago']]; + } + switch ($orderby) { + case 'views': + $query['meta_key'] = 'aksrt_post_views'; + $query['orderby'] = 'meta_value_num'; + $query['order'] = 'DESC'; + break; + case 'comments': + $query['orderby'] = 'comment_count'; + $query['order'] = 'DESC'; + break; + case 'rand': + $query['orderby'] = 'rand'; + break; + default: + $query['orderby'] = 'date'; + $query['order'] = 'DESC'; + } + + $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 '
'; + 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 '
'; + } + echo '
'; + echo '
' . esc_html(get_the_title()) . '
'; + if ($style !== 'mini') { + echo '
' . esc_html(get_the_date()) . '
'; + } + echo '
'; + } + 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'] ?? 8))), + 'style' => $style, + 'orderby' => $orderby, + 'cat' => max(0, (int) ($new_instance['cat'] ?? 0)), + 'limit_day' => max(0, (int) ($new_instance['limit_day'] ?? 0)), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetTiltCases.php b/src/AKSRT/Widgets/WidgetTiltCases.php new file mode 100644 index 00000000..479fe2cb --- /dev/null +++ b/src/AKSRT/Widgets/WidgetTiltCases.php @@ -0,0 +1,258 @@ + esc_html__('左文案 + 右侧叠层图片案例展示。', 'aksrt'), + 'classname' => 'widget_aksrt_tilt_cases', + ] + ); + } + + public function widget($args, $instance): void { + $title = trim((string) ($instance['intro_title'] ?? '')); + $subtitle = trim((string) ($instance['intro_subtitle'] ?? '')); + $badge = trim((string) ($instance['intro_badge_text'] ?? '')); + $images = $this->normalize_images($instance['intro_showcase_images'] ?? []); + $features = $this->normalize_features($instance['intro_features'] ?? []); + $buttons = $this->normalize_buttons($instance['intro_buttons'] ?? []); + + if ($title === '' && $subtitle === '' && !$images) { + return; + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + echo '
'; + echo '
'; + if ($badge !== '') { + $icon = sanitize_text_field((string) ($instance['intro_badge_icon'] ?? '')); + echo '
'; + if ($icon !== '') { + echo ' '; + } + echo esc_html($badge) . '
'; + } + if ($title !== '') { + $highlight = trim((string) ($instance['intro_title_highlight'] ?? '')); + $title_html = esc_html($title); + if ($highlight !== '') { + $h = esc_html($highlight); + $title_html = str_replace($h, '' . $h . '', $title_html); + } + echo '

' . $title_html . '

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

' . esc_html($subtitle) . '

'; + } + if ($features) { + echo '
    '; + foreach ($features as $f) { + echo '
  • '; + if ($f['icon'] !== '') { + echo ' '; + } + echo esc_html($f['text']) . '
  • '; + } + echo '
'; + } + if ($buttons) { + echo '
'; + foreach ($buttons as $b) { + $url = $b['url']; + if ($url !== '' && !empty($instance['go_link']) && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + echo '' . esc_html($b['text']) . ''; + } + echo '
'; + } + echo '
'; + + echo '
'; + if ($images) { + foreach ($images as $i => $img) { + $cls = $i === 0 ? ' is-main' : ''; + echo '
'; + } + } else { + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** @param mixed $raw @return array */ + private function normalize_images($raw): array { + if (is_string($raw)) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $raw = $decoded; + } else { + $parts = preg_split('/\r\n|\r|\n|,/', $raw) ?: []; + $raw = $parts; + } + } + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (is_array($row)) { + $url = esc_url_raw(trim((string) ($row['image'] ?? $row['url'] ?? ''))); + } else { + $url = esc_url_raw(trim((string) $row)); + } + if ($url !== '') { + $out[] = $url; + } + if (count($out) >= 5) { + break; + } + } + return $out; + } + + /** @param mixed $raw @return array */ + private function normalize_features($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_string($row)) { + $text = sanitize_text_field($row); + $icon = ''; + } elseif (is_array($row)) { + $text = sanitize_text_field((string) ($row['text'] ?? '')); + $icon = sanitize_text_field((string) ($row['icon'] ?? '')); + } else { + continue; + } + if ($text === '') { + continue; + } + $out[] = ['text' => $text, 'icon' => $icon]; + if (count($out) >= 8) { + break; + } + } + return $out; + } + + /** @param mixed $raw @return array */ + private function normalize_buttons($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; + } + $text = sanitize_text_field((string) ($row['text'] ?? $row['btn']['text'] ?? '')); + $url = esc_url_raw(trim((string) ($row['url'] ?? $row['btn']['url'] ?? ''))); + if ($text === '') { + continue; + } + $out[] = ['text' => $text, 'url' => $url]; + if (count($out) >= 4) { + break; + } + } + return $out; + } + + public function form($instance): void { + $images = is_string($instance['intro_showcase_images'] ?? null) + ? (string) $instance['intro_showcase_images'] + : wp_json_encode($this->normalize_images($instance['intro_showcase_images'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $features = is_string($instance['intro_features'] ?? null) + ? (string) $instance['intro_features'] + : wp_json_encode($this->normalize_features($instance['intro_features'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $buttons = is_string($instance['intro_buttons'] ?? null) + ? (string) $instance['intro_buttons'] + : wp_json_encode($this->normalize_buttons($instance['intro_buttons'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['intro_badge_text'] ?? '')), + 'intro_badge_icon' => sanitize_text_field((string) ($new_instance['intro_badge_icon'] ?? '')), + 'intro_title' => sanitize_textarea_field((string) ($new_instance['intro_title'] ?? '')), + 'intro_title_highlight' => sanitize_text_field((string) ($new_instance['intro_title_highlight'] ?? '')), + 'intro_subtitle' => sanitize_textarea_field((string) ($new_instance['intro_subtitle'] ?? '')), + 'intro_features' => $this->normalize_features($new_instance['intro_features'] ?? []), + 'intro_buttons' => $this->normalize_buttons($new_instance['intro_buttons'] ?? []), + 'intro_showcase_images' => $this->normalize_images($new_instance['intro_showcase_images'] ?? []), + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + ]; + } +}