From 81bf3193a4f90030067819f467579cbfd3207215 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:15:32 +0800 Subject: [PATCH 1/4] feat(widgets): partners/iframe/combo/text/user + forum plate widgets Close major zibll widget-more and bbs plate gaps: partners marquee, super embed, combo info cards, text title, user lists, and plate list/info/moderator sidebars with batch3 CSS registration. --- assets/css/frontend/widget-batch3.css | 532 ++++++++++++++++++ assets/css/frontend/widgets.css | 1 + inc/modules/widgets-register.php | 16 + src/AKSRT/Widgets/WidgetComboInfo.php | 238 ++++++++ src/AKSRT/Widgets/WidgetForumPlateInfo.php | 94 ++++ src/AKSRT/Widgets/WidgetForumPlateLists.php | 216 +++++++ .../Widgets/WidgetForumPlateModerator.php | 131 +++++ src/AKSRT/Widgets/WidgetIframe.php | 113 ++++ src/AKSRT/Widgets/WidgetPartnersScroll.php | 348 ++++++++++++ src/AKSRT/Widgets/WidgetTextTitle.php | 138 +++++ src/AKSRT/Widgets/WidgetUserLists.php | 199 +++++++ 11 files changed, 2026 insertions(+) create mode 100644 assets/css/frontend/widget-batch3.css create mode 100644 src/AKSRT/Widgets/WidgetComboInfo.php create mode 100644 src/AKSRT/Widgets/WidgetForumPlateInfo.php create mode 100644 src/AKSRT/Widgets/WidgetForumPlateLists.php create mode 100644 src/AKSRT/Widgets/WidgetForumPlateModerator.php create mode 100644 src/AKSRT/Widgets/WidgetIframe.php create mode 100644 src/AKSRT/Widgets/WidgetPartnersScroll.php create mode 100644 src/AKSRT/Widgets/WidgetTextTitle.php create mode 100644 src/AKSRT/Widgets/WidgetUserLists.php diff --git a/assets/css/frontend/widget-batch3.css b/assets/css/frontend/widget-batch3.css new file mode 100644 index 00000000..6853d0f9 --- /dev/null +++ b/assets/css/frontend/widget-batch3.css @@ -0,0 +1,532 @@ +/** + * Batch3 widgets: partners scroll, iframe, combo info, text title, user lists, plate widgets. + * @package AKSRT + */ + +/* ---- Partners scroll ---- */ +.aksrt-partners-scroll-wrap { + margin-bottom: 20px; +} +.aksrt-partners-scroll-wrap.is-bare { + background: transparent; + box-shadow: none; + padding: 0; +} +.aksrt-partners-scroll { + overflow: hidden; + position: relative; + --aksrt-ps-w: 220px; + --aksrt-ps-h: 90px; + --aksrt-ps-radius: 10px; + --aksrt-ps-speed: 3; +} +.aksrt-ps-end-fade::before, +.aksrt-ps-end-fade::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 48px; + z-index: 2; + pointer-events: none; +} +.aksrt-ps-end-fade::before { + left: 0; + background: linear-gradient(90deg, var(--surface-1, #fff), transparent); +} +.aksrt-ps-end-fade::after { + right: 0; + background: linear-gradient(270deg, var(--surface-1, #fff), transparent); +} +.aksrt-ps-row { + overflow: hidden; + margin-bottom: 10px; +} +.aksrt-ps-row:last-child { + margin-bottom: 0; +} +.aksrt-ps-track { + display: flex; + width: max-content; + animation: aksrt-ps-marquee calc(40s / var(--aksrt-ps-speed)) linear infinite; +} +.aksrt-ps-dir-right .aksrt-ps-track { + animation-name: aksrt-ps-marquee-rev; +} +.aksrt-ps-pause-hover:hover .aksrt-ps-track { + animation-play-state: paused; +} +.aksrt-ps-group { + display: flex; + flex-shrink: 0; + align-items: center; +} +.aksrt-ps-item { + display: flex; + align-items: center; + flex-shrink: 0; + width: var(--aksrt-ps-w); + height: var(--aksrt-ps-h); + margin-right: 12px; + border-radius: var(--aksrt-ps-radius); + overflow: hidden; + text-decoration: none; + color: inherit; + box-sizing: border-box; + padding: 8px 12px; +} +.aksrt-ps-card-bg .aksrt-ps-item { + background: var(--surface-2, #f3f4f6); +} +.aksrt-ps-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + display: block; +} +.aksrt-ps-style-logo .aksrt-ps-item { + justify-content: center; + padding: 10px; +} +.aksrt-ps-card-icon { + width: 42px; + height: 42px; + flex-shrink: 0; + margin-right: 10px; + display: flex; + align-items: center; + justify-content: center; +} +.aksrt-ps-card-icon .aksrt-ps-img { + max-height: 42px; +} +.aksrt-ps-card-text { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; +} +.aksrt-ps-card-title { + font-weight: 700; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-ps-card-desc { + font-size: 12px; + color: var(--muted-2-color, #999); + margin-top: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-ps-style-chip .aksrt-ps-item { + width: auto; + height: auto; + min-height: 36px; + border-radius: 999px; + padding: 6px 14px; + gap: 6px; +} +.aksrt-ps-chip-icon { + width: 22px; + height: 22px; + display: flex; + align-items: center; + margin-right: 6px; +} +.aksrt-ps-chip-title { + font-size: 13px; + white-space: nowrap; +} +@keyframes aksrt-ps-marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@keyframes aksrt-ps-marquee-rev { + from { transform: translateX(-50%); } + to { transform: translateX(0); } +} + +/* ---- Iframe embed ---- */ +.aksrt-iframe-embed { + margin-bottom: 16px; +} +.aksrt-iframe-ratio { + position: relative; + width: 100%; + height: 0; + border-radius: 8px; + overflow: hidden; + background: var(--surface-2, #f3f4f6); +} +.aksrt-iframe-ratio iframe, +.aksrt-iframe-el { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: 0; +} + +/* ---- Combo info cards ---- */ +.aksrt-combo-info { + display: grid; + grid-template-columns: repeat(var(--aksrt-zci-pc, 4), minmax(0, 1fr)); + gap: 14px; + margin-bottom: 16px; + --aksrt-zci-radius: 12px; + --aksrt-zci-band-h: 70px; +} +@media (max-width: 991.98px) { + .aksrt-combo-info { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 575.98px) { + .aksrt-combo-info { + grid-template-columns: repeat(var(--aksrt-zci-m, 1), minmax(0, 1fr)); + } +} +.aksrt-zci-card { + position: relative; + display: flex; + flex-direction: column; + border-radius: var(--aksrt-zci-radius); + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + padding: 18px 16px 16px; + overflow: hidden; + min-height: 100%; +} +.aksrt-combo-info.has-band .aksrt-zci-card::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + height: 4px; + background: var(--color-primary, #3b82f6); +} +.aksrt-zci-band-blue::before { background: #3b82f6; } +.aksrt-zci-band-cyan::before { background: #06b6d4; } +.aksrt-zci-band-green::before { background: #22c55e; } +.aksrt-zci-band-yellow::before { background: #eab308; } +.aksrt-zci-band-pink::before { background: #ec4899; } +.aksrt-zci-band-purple::before { background: #a855f7; } +.aksrt-zci-icon { + width: 48px; + height: 48px; + margin-bottom: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + color: var(--color-primary, #3b82f6); +} +.aksrt-zci-icon img { + max-width: 48px; + max-height: 48px; + object-fit: contain; +} +.aksrt-zci-title { + font-weight: 700; + font-size: 16px; + margin-bottom: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-zci-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} +.aksrt-zci-tag { + display: inline-block; + font-size: 12px; + padding: 2px 10px; + border-radius: 999px; + background: var(--surface-2, #f3f4f6); + color: var(--color-ink-3, #666); +} +.aksrt-zci-desc { + font-size: 13px; + color: var(--muted-2-color, #888); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + flex: 1; +} +.aksrt-zci-cta { + margin-top: 14px; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--color-primary, #3b82f6); + text-decoration: none; +} +.aksrt-zci-cta:hover i { + transform: translateX(3px); +} +.aksrt-zci-cta i { + transition: transform .2s ease; + font-size: 12px; +} + +/* ---- Text title ---- */ +.aksrt-text-title { + margin: 8px 0 18px; + --aksrt-tt-title-size: 22px; + --aksrt-tt-desc-size: 14px; +} +.aksrt-text-title.align-center { text-align: center; } +.aksrt-text-title.align-left { text-align: left; } +.aksrt-text-title.align-right { text-align: right; } +.aksrt-text-title__main { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: var(--aksrt-tt-title-size); + font-weight: 700; + color: var(--aksrt-tt-title-color, inherit); + line-height: 1.3; +} +.aksrt-text-title.align-center .aksrt-text-title__main { + justify-content: center; +} +.aksrt-text-title__desc { + margin-top: 8px; + font-size: var(--aksrt-tt-desc-size); + color: var(--aksrt-tt-desc-color, var(--muted-2-color, #888)); + line-height: 1.5; +} +.aksrt-text-title__icon { + display: inline-flex; + opacity: .85; +} + +/* ---- User lists ---- */ +.aksrt-user-lists { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px 8px; + text-align: center; + padding: 4px 0 8px; +} +.aksrt-user-lists.is-bare { + padding: 0; +} +@media (max-width: 575.98px) { + .aksrt-user-lists { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} +.aksrt-user-lists__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + text-decoration: none; + color: inherit; + min-width: 0; +} +.aksrt-user-lists__avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + background: var(--surface-2, #eee); +} +.aksrt-user-lists__name { + font-size: 12px; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ---- Forum plate lists ---- */ +.aksrt-plate-lists { + display: flex; + flex-direction: column; + gap: 10px; +} +.aksrt-plate-item { + display: flex; + gap: 12px; + align-items: flex-start; + text-decoration: none; + color: inherit; + padding: 6px 0; + border-bottom: 1px dashed var(--color-line, #e5e7eb); +} +.aksrt-plate-item:last-child { + border-bottom: 0; +} +.aksrt-plate-item__thumb { + width: 48px; + height: 48px; + border-radius: 10px; + overflow: hidden; + flex-shrink: 0; + background: var(--surface-2, #f3f4f6); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-primary, #3b82f6); + font-size: 18px; +} +.aksrt-plate-item__thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} +.aksrt-plate-item__body { + min-width: 0; + flex: 1; +} +.aksrt-plate-item__title { + font-weight: 700; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-plate-item__desc { + font-size: 12px; + color: var(--muted-2-color, #888); + margin-top: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-plate-item__meta { + font-size: 11px; + color: var(--color-ink-4, #999); + margin-top: 4px; +} +.aksrt-plate-cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +@media (max-width: 575.98px) { + .aksrt-plate-cards { + grid-template-columns: 1fr; + } +} +.aksrt-plate-card { + display: block; + text-decoration: none; + color: inherit; + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); +} +.aksrt-plate-card__cover { + height: 88px; + background: var(--surface-2, #f3f4f6) center/cover no-repeat; +} +.aksrt-plate-card__title { + font-weight: 700; + font-size: 13px; + padding: 10px 10px 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-plate-card__meta { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--muted-2-color, #888); + padding: 0 10px 10px; +} + +/* ---- Plate info ---- */ +.aksrt-plate-info { + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + margin-bottom: 12px; +} +.aksrt-plate-info__cover { + height: 90px; + background: var(--surface-2, #f3f4f6) center/cover no-repeat; +} +.aksrt-plate-info__body { + padding: 12px 14px 14px; +} +.aksrt-plate-info__title { + display: block; + font-weight: 700; + font-size: 16px; + color: inherit; + text-decoration: none; + margin-bottom: 6px; +} +.aksrt-plate-info__desc { + font-size: 12px; + color: var(--muted-2-color, #888); + line-height: 1.5; + margin-bottom: 10px; +} +.aksrt-plate-info__stats { + display: flex; + gap: 14px; + font-size: 12px; + color: var(--color-ink-3, #666); +} +.aksrt-plate-info__stats i { + margin-right: 4px; + opacity: .75; +} + +/* ---- Plate moderators ---- */ +.aksrt-plate-mods { + margin-bottom: 12px; +} +.aksrt-plate-mods__head { + margin-bottom: 10px; +} +.aksrt-plate-mods__list { + display: flex; + flex-direction: column; + gap: 8px; +} +.aksrt-plate-mods__item { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: inherit; +} +.aksrt-plate-mods__avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} +.aksrt-plate-mods__name { + flex: 1; + min-width: 0; + font-size: 13px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.aksrt-plate-mods__role { + font-size: 11px; + color: var(--color-primary, #3b82f6); + flex-shrink: 0; +} diff --git a/assets/css/frontend/widgets.css b/assets/css/frontend/widgets.css index ed3d1caf..97ccca5b 100644 --- a/assets/css/frontend/widgets.css +++ b/assets/css/frontend/widgets.css @@ -3,3 +3,4 @@ @import url("widgets/part-3.css"); @import url("widgets/part-4.css"); @import url("widget-slider.css"); +@import url("widget-batch3.css"); diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index 65fbb549..124e3d15 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -56,6 +56,13 @@ function aksrt_register_widgets() { // 幻灯片 register_widget('\AKSRT\Widgets\WidgetSlider'); + // zibll more widgets batch + register_widget('\AKSRT\Widgets\WidgetPartnersScroll'); + register_widget('\AKSRT\Widgets\WidgetIframe'); + register_widget('\AKSRT\Widgets\WidgetComboInfo'); + register_widget('\AKSRT\Widgets\WidgetTextTitle'); + register_widget('\AKSRT\Widgets\WidgetUserLists'); + // Forum widgets if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { register_widget('\AKSRT\Widgets\WidgetForumBoardList'); @@ -66,6 +73,15 @@ function aksrt_register_widgets() { if (class_exists('\AKSRT\Widgets\WidgetForumRecentTopics')) { register_widget('\AKSRT\Widgets\WidgetForumRecentTopics'); } + if (class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) { + register_widget('\AKSRT\Widgets\WidgetForumPlateLists'); + } + if (class_exists('\AKSRT\Widgets\WidgetForumPlateInfo')) { + register_widget('\AKSRT\Widgets\WidgetForumPlateInfo'); + } + if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) { + register_widget('\AKSRT\Widgets\WidgetForumPlateModerator'); + } // Shop product widgets (§6.9) register_widget('\AKSRT\Widgets\WidgetProductList'); diff --git a/src/AKSRT/Widgets/WidgetComboInfo.php b/src/AKSRT/Widgets/WidgetComboInfo.php new file mode 100644 index 00000000..61e6514e --- /dev/null +++ b/src/AKSRT/Widgets/WidgetComboInfo.php @@ -0,0 +1,238 @@ + esc_html__('图标 + 标题 + 标签 + 描述 + CTA 的多列信息卡。', 'aksrt'), + 'classname' => 'widget_aksrt_combo_info', + ] + ); + } + + public function widget($args, $instance): void { + $cards = $this->normalize_cards($instance['cards'] ?? []); + if (!$cards) { + return; + } + + $pc_row = max(1, min(6, (int) ($instance['pc_row'] ?? 4))); + $m_row = max(1, min(3, (int) ($instance['m_row'] ?? 1))); + $radius = max(0, min(40, (int) ($instance['card_radius'] ?? 12))); + $top_band = !empty($instance['top_band']) || !isset($instance['top_band']); + $band_h = max(20, min(160, (int) ($instance['top_band_height'] ?? 70))); + $cta_default = (string) ($instance['cta_text'] ?? __('立即体验', 'aksrt')); + $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 + } + + $style = sprintf('--aksrt-zci-radius:%dpx;--aksrt-zci-band-h:%dpx;--aksrt-zci-pc:%d;--aksrt-zci-m:%d;', $radius, $band_h, $pc_row, $m_row); + $wrap = 'aksrt-combo-info' . ($top_band ? ' has-band' : ''); + + echo '
'; + $auto_idx = 0; + foreach ($cards as $card) { + $html = $this->render_card($card, $cta_default, $auto_idx, $go_link, $top_band); + if ($html !== '') { + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param array $card + */ + private function render_card(array $card, string $default_cta, int &$auto_idx, bool $go_link, bool $top_band): string { + $title = trim((string) ($card['title'] ?? '')); + $desc = trim((string) ($card['desc'] ?? '')); + if ($title === '' && $desc === '') { + return ''; + } + + $band = sanitize_key((string) ($card['band_color'] ?? 'auto')); + if ($band === '' || $band === 'auto' || !in_array($band, self::BANDS, true)) { + $band = self::BANDS[$auto_idx % count(self::BANDS)]; + $auto_idx++; + } + + $icon_html = ''; + $icon_img = (string) ($card['icon_image'] ?? ''); + $icon_fa = (string) ($card['icon'] ?? ''); + if ($icon_img !== '') { + $icon_html = '
'; + } elseif ($icon_fa !== '') { + $icon_html = '
'; + } + + $tags_html = ''; + $tags = $card['tags'] ?? []; + if (is_string($tags)) { + $tags = array_filter(array_map('trim', explode(',', $tags))); + $tags = array_map(static fn($t) => ['text' => $t, 'color' => ''], $tags); + } + if (is_array($tags) && $tags) { + $tags_html .= '
'; + foreach ($tags as $tag) { + if (is_string($tag)) { + $tag = ['text' => $tag, 'color' => '']; + } + if (!is_array($tag)) { + continue; + } + $tt = trim((string) ($tag['text'] ?? '')); + if ($tt === '') { + continue; + } + $color = sanitize_html_class((string) ($tag['color'] ?? '')); + $tags_html .= '' . esc_html($tt) . ''; + } + $tags_html .= '
'; + } + + $cta_text = trim((string) ($card['cta_text'] ?? '')); + if ($cta_text === '') { + $cta_text = $default_cta; + } + $url = (string) ($card['url'] ?? ''); + if ($url === '' && is_array($card['link'] ?? null)) { + $url = (string) ($card['link']['url'] ?? ''); + } + $target = (string) ($card['target'] ?? ''); + if ($target === '' && is_array($card['link'] ?? null)) { + $target = (string) ($card['link']['target'] ?? ''); + } + $cta_html = ''; + if ($cta_text !== '' && $url !== '') { + if ($go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $tgt = $target !== '' ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' : ''; + $cta_html = '' . esc_html($cta_text) . ''; + } + + $band_class = $top_band ? ' aksrt-zci-band-' . esc_attr($band) : ''; + $html = '
'; + $html .= $icon_html; + if ($title !== '') { + $html .= '
' . esc_html($title) . '
'; + } + $html .= $tags_html; + if ($desc !== '') { + $html .= '
' . esc_html($desc) . '
'; + } + $html .= $cta_html; + $html .= '
'; + return $html; + } + + /** + * @param mixed $raw + * @return array> + */ + private function normalize_cards($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; + } + $out[] = [ + 'title' => $title, + 'desc' => $desc, + 'icon' => sanitize_text_field((string) ($row['icon'] ?? '')), + 'icon_image' => esc_url_raw(trim((string) ($row['icon_image'] ?? ''))), + 'url' => esc_url_raw(trim((string) ($row['url'] ?? (is_array($row['link'] ?? null) ? ($row['link']['url'] ?? '') : '')))), + 'target' => sanitize_text_field((string) ($row['target'] ?? '_blank')), + 'cta_text' => sanitize_text_field((string) ($row['cta_text'] ?? '')), + 'band_color' => sanitize_key((string) ($row['band_color'] ?? 'auto')), + 'tags' => $row['tags'] ?? [], + ]; + if (count($out) >= 12) { + break; + } + } + return $out; + } + + public function form($instance): void { + $title = (string) ($instance['title'] ?? ''); + $cards = is_string($instance['cards'] ?? null) + ? (string) $instance['cards'] + : wp_json_encode($this->normalize_cards($instance['cards'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ + +

+

+ +

+

+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'cards' => $this->normalize_cards($new_instance['cards'] ?? []), + 'pc_row' => max(1, min(6, (int) ($new_instance['pc_row'] ?? 4))), + 'm_row' => max(1, min(3, (int) ($new_instance['m_row'] ?? 1))), + 'card_radius' => max(0, min(40, (int) ($new_instance['card_radius'] ?? 12))), + 'top_band' => !empty($new_instance['top_band']) ? 1 : 0, + 'top_band_height' => max(20, min(160, (int) ($new_instance['top_band_height'] ?? 70))), + 'cta_text' => sanitize_text_field((string) ($new_instance['cta_text'] ?? '')), + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetForumPlateInfo.php b/src/AKSRT/Widgets/WidgetForumPlateInfo.php new file mode 100644 index 00000000..b0bf8b8b --- /dev/null +++ b/src/AKSRT/Widgets/WidgetForumPlateInfo.php @@ -0,0 +1,94 @@ + esc_html__('在版块/帖子页侧栏显示当前 plate 信息。', 'aksrt'), + 'classname' => 'widget_aksrt_forum_plate_info', + ] + ); + } + + public function widget($args, $instance): void { + $plate = $this->resolve_current_plate(); + if (!$plate) { + return; + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + $id = (int) $plate->ID; + $url = get_permalink($id); + $thumb = get_the_post_thumbnail_url($id, 'medium'); + $posts = (int) get_post_meta($id, '_aksrt_posts_count', true); + $reply = (int) get_post_meta($id, '_aksrt_reply_count', true); + $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); + $desc = $plate->post_excerpt !== '' ? $plate->post_excerpt : wp_trim_words(wp_strip_all_tags($plate->post_content), 30); + + echo '
'; + if ($thumb) { + echo '
'; + } + echo '
'; + echo '' . esc_html(get_the_title($plate)) . ''; + if ($desc !== '') { + echo '
' . esc_html($desc) . '
'; + } + echo '
'; + echo ' ' . esc_html((string) $posts) . ''; + echo ' ' . esc_html((string) $reply) . ''; + echo ' ' . esc_html((string) $follow) . ''; + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function resolve_current_plate(): ?\WP_Post { + if (is_singular('aksrt_forum_plate')) { + $p = get_queried_object(); + return $p instanceof \WP_Post ? $p : null; + } + if (is_singular('aksrt_forum_topic')) { + $topic_id = (int) get_queried_object_id(); + $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); + if ($plate_id <= 0) { + // Fallback: first linked board term → plate meta, or parent. + $boards = get_the_terms($topic_id, 'aksrt_forum_board'); + if (!is_wp_error($boards) && !empty($boards[0])) { + $linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true); + if ($linked > 0) { + $plate_id = $linked; + } + } + } + if ($plate_id > 0) { + $p = get_post($plate_id); + if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') { + return $p; + } + } + } + return null; + } + + public function form($instance): void { + echo '

' . esc_html__('无需配置。仅在版块页或帖子页显示对应版块信息。', 'aksrt') . '

'; + } + + public function update($new_instance, $old_instance): array { + return []; + } +} diff --git a/src/AKSRT/Widgets/WidgetForumPlateLists.php b/src/AKSRT/Widgets/WidgetForumPlateLists.php new file mode 100644 index 00000000..0dbe1afd --- /dev/null +++ b/src/AKSRT/Widgets/WidgetForumPlateLists.php @@ -0,0 +1,216 @@ + esc_html__('展示论坛 plate 版块列表,支持排序与卡片样式。', 'aksrt'), + 'classname' => 'widget_aksrt_forum_plate_lists', + ] + ); + } + + public function widget($args, $instance): void { + if (!post_type_exists('aksrt_forum_plate')) { + return; + } + + $limit = max(1, min(30, (int) ($instance['showposts'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'menu_order')); + $style = sanitize_key((string) ($instance['style'] ?? 'default')); + $filter = sanitize_key((string) ($instance['filter'] ?? '')); + $cat_raw = (string) ($instance['cat'] ?? ''); + $cat_ids = array_values(array_filter(array_map('intval', preg_split('/[\s,]+/', $cat_raw) ?: []))); + + $query_args = [ + 'post_type' => 'aksrt_forum_plate', + 'post_status' => 'publish', + 'posts_per_page' => $limit, + 'order' => 'ASC', + ]; + + switch ($orderby) { + case 'date': + $query_args['orderby'] = 'date'; + $query_args['order'] = 'DESC'; + break; + case 'posts': + $query_args['meta_key'] = '_aksrt_posts_count'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + case 'views': + $query_args['meta_key'] = '_aksrt_views'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + case 'follow': + $query_args['meta_key'] = '_aksrt_follow_count'; + $query_args['orderby'] = 'meta_value_num'; + $query_args['order'] = 'DESC'; + break; + default: + $query_args['orderby'] = 'menu_order title'; + } + + if ($cat_ids) { + $query_args['tax_query'] = [[ + 'taxonomy' => 'aksrt_plate_cat', + 'field' => 'term_id', + 'terms' => $cat_ids, + ]]; + } + + if ($filter === 'is_hot') { + $query_args['meta_query'] = [[ + 'key' => '_aksrt_is_hot', + 'value' => '1', + 'compare' => '=', + ]]; + } + + $plates = get_posts($query_args); + if (!$plates) { + 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 + } + + if ($style === 'card') { + echo '
'; + foreach ($plates as $post) { + $this->render_card($post); + } + echo '
'; + } else { + echo '
'; + foreach ($plates as $post) { + $this->render_row($post); + } + echo '
'; + } + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function render_row(\WP_Post $post): void { + $id = (int) $post->ID; + $url = get_permalink($id); + $thumb = get_the_post_thumbnail_url($id, 'thumbnail'); + $icon = (string) get_post_meta($id, '_aksrt_icon', true); + $count = (int) get_post_meta($id, '_aksrt_posts_count', true); + $desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags($post->post_content), 18); + + echo ''; + echo '
'; + if ($thumb) { + echo ''; + } elseif ($icon !== '') { + echo ''; + } else { + echo ''; + } + echo '
'; + echo '
'; + echo '
' . esc_html(get_the_title($post)) . '
'; + if ($desc !== '') { + echo '
' . esc_html($desc) . '
'; + } + echo '
' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . '
'; + echo '
'; + } + + private function render_card(\WP_Post $post): void { + $id = (int) $post->ID; + $url = get_permalink($id); + $thumb = get_the_post_thumbnail_url($id, 'medium'); + $count = (int) get_post_meta($id, '_aksrt_posts_count', true); + $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); + + echo ''; + echo '
'; + echo '
' . esc_html(get_the_title($post)) . '
'; + echo '
'; + echo '' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . ''; + echo '' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . ''; + echo '
'; + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'showposts' => max(1, min(30, (int) ($new_instance['showposts'] ?? 8))), + 'orderby' => $orderby, + 'style' => ($new_instance['style'] ?? '') === 'card' ? 'card' : 'default', + 'filter' => ($new_instance['filter'] ?? '') === 'is_hot' ? 'is_hot' : '', + 'cat' => sanitize_text_field((string) ($new_instance['cat'] ?? '')), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetForumPlateModerator.php b/src/AKSRT/Widgets/WidgetForumPlateModerator.php new file mode 100644 index 00000000..d2fe667d --- /dev/null +++ b/src/AKSRT/Widgets/WidgetForumPlateModerator.php @@ -0,0 +1,131 @@ + esc_html__('显示当前版块的版主与超级版主。', 'aksrt'), + 'classname' => 'widget_aksrt_forum_plate_moderator', + ] + ); + } + + public function widget($args, $instance): void { + $plate = $this->resolve_current_plate(); + if (!$plate) { + return; + } + + $id = (int) $plate->ID; + $author_id = (int) $plate->post_author; + $mods = get_post_meta($id, '_aksrt_moderators', true); + if (!is_array($mods)) { + $mods = []; + } + $mods = array_values(array_unique(array_filter(array_map('intval', $mods)))); + + $people = []; + if ($author_id > 0) { + $people[] = ['id' => $author_id, 'role' => 'author']; + } + foreach ($mods as $mid) { + if ($mid === $author_id) { + continue; + } + $people[] = ['id' => $mid, 'role' => 'moderator']; + } + if (!$people) { + return; + } + + $title_text = (string) ($instance['title_text'] ?? __('版主', 'aksrt')); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + echo '
'; + if ($title_text !== '') { + echo '
' . esc_html($title_text) . '
'; + } + echo '
'; + foreach ($people as $row) { + $uid = (int) $row['id']; + $user = get_userdata($uid); + if (!$user) { + continue; + } + $avatar = function_exists('aksrt_get_avatar_url') + ? aksrt_get_avatar_url($uid, ['size' => 64]) + : get_avatar_url($uid, ['size' => 64]); + $home = function_exists('aksrt_get_user_home_url') + ? aksrt_get_user_home_url($uid) + : get_author_posts_url($uid); + $role_label = $row['role'] === 'author' + ? __('超级版主', 'aksrt') + : __('版主', 'aksrt'); + echo ''; + echo ''; + echo '' . esc_html($user->display_name) . ''; + echo '' . esc_html($role_label) . ''; + echo ''; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function resolve_current_plate(): ?\WP_Post { + if (is_singular('aksrt_forum_plate')) { + $p = get_queried_object(); + return $p instanceof \WP_Post ? $p : null; + } + if (is_singular('aksrt_forum_topic')) { + $topic_id = (int) get_queried_object_id(); + $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); + if ($plate_id <= 0) { + $boards = get_the_terms($topic_id, 'aksrt_forum_board'); + if (!is_wp_error($boards) && !empty($boards[0])) { + $linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true); + if ($linked > 0) { + $plate_id = $linked; + } + } + } + if ($plate_id > 0) { + $p = get_post($plate_id); + if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') { + return $p; + } + } + } + return null; + } + + public function form($instance): void { + ?> +

+ +

+

+ sanitize_text_field((string) ($new_instance['title_text'] ?? '')), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetIframe.php b/src/AKSRT/Widgets/WidgetIframe.php new file mode 100644 index 00000000..eaeeb2cc --- /dev/null +++ b/src/AKSRT/Widgets/WidgetIframe.php @@ -0,0 +1,113 @@ + esc_html__('嵌入在线视频/音乐播放器或任意 iframe 内容。', 'aksrt'), + 'classname' => 'widget_aksrt_iframe', + ] + ); + } + + public function widget($args, $instance): void { + $raw = trim((string) ($instance['url'] ?? '')); + if ($raw === '') { + return; + } + $aspect = max(20, min(300, (int) ($instance['aspect'] ?? 55))); + $allow_fs = !empty($instance['allowfullscreen']); + + 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 + } + + $iframe_html = ''; + if (stripos($raw, '') !== false) { + $iframe_html = wp_kses( + $raw, + [ + 'iframe' => [ + 'src' => true, + 'width' => true, + 'height' => true, + 'frameborder' => true, + 'allow' => true, + 'allowfullscreen' => true, + 'loading' => true, + 'class' => true, + 'style' => true, + 'referrerpolicy' => true, + 'title' => true, + 'sandbox' => true, + 'data-src' => true, + ], + ] + ); + } else { + $url = esc_url($raw); + if ($url === '') { + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + $fs = $allow_fs ? ' allowfullscreen="allowfullscreen"' : ''; + $iframe_html = ''; + } + + echo '
'; + echo '
'; + echo $iframe_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + public function form($instance): void { + $title = (string) ($instance['title'] ?? ''); + $url = (string) ($instance['url'] ?? ''); + $aspect = (int) ($instance['aspect'] ?? 55); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'url' => trim((string) ($new_instance['url'] ?? '')), + 'aspect' => max(20, min(300, (int) ($new_instance['aspect'] ?? 55))), + 'allowfullscreen' => !empty($new_instance['allowfullscreen']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetPartnersScroll.php b/src/AKSRT/Widgets/WidgetPartnersScroll.php new file mode 100644 index 00000000..c1633cb4 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetPartnersScroll.php @@ -0,0 +1,348 @@ + esc_html__('横向无缝滚动的合作伙伴/友情链接条。', 'aksrt'), + 'classname' => 'widget_aksrt_partners_scroll', + ] + ); + } + + public function widget($args, $instance): void { + $partners = $this->resolve_partners($instance); + if (!$partners) { + return; + } + + $rows = max(1, min(4, (int) ($instance['rows'] ?? 1))); + $m_rows = max(1, min(4, (int) ($instance['m_rows'] ?? 1))); + $style_type = sanitize_key((string) ($instance['style_type'] ?? 'card')); + if (!in_array($style_type, ['card', 'logo', 'chip'], true)) { + $style_type = 'card'; + } + $card_w = max(60, min(400, (int) ($instance['card_width'] ?? 220))); + $card_h = max(40, min(200, (int) ($instance['card_height'] ?? 90))); + $speed = max(1, min(20, (int) ($instance['speed'] ?? 3))); + $direction = sanitize_key((string) ($instance['direction'] ?? 'alternate')); + $pause = !empty($instance['pause_hover']) || !isset($instance['pause_hover']); + $card_bg = !empty($instance['card_bg']) || !isset($instance['card_bg']); + $end_fade = !empty($instance['end_fade']) || !isset($instance['end_fade']); + $go_link = !empty($instance['go_link']); + $show_bg = !empty($instance['show_widget_bg']) || !isset($instance['show_widget_bg']); + $radius = max(0, min(40, (int) ($instance['card_radius'] ?? 10))); + + $use_rows = wp_is_mobile() ? $m_rows : $rows; + $rows_data = array_fill(0, $use_rows, []); + foreach (array_values($partners) as $i => $p) { + $rows_data[$i % $use_rows][] = $p; + } + $rows_data = array_values(array_filter($rows_data, static fn($r) => !empty($r))); + + 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 + } + + $wrap_class = 'aksrt-partners-scroll aksrt-ps-style-' . $style_type; + if ($pause) { + $wrap_class .= ' aksrt-ps-pause-hover'; + } + if ($card_bg) { + $wrap_class .= ' aksrt-ps-card-bg'; + } + if ($end_fade) { + $wrap_class .= ' aksrt-ps-end-fade'; + } + + $style = sprintf( + '--aksrt-ps-w:%dpx;--aksrt-ps-h:%dpx;--aksrt-ps-radius:%dpx;--aksrt-ps-speed:%d;', + $card_w, + $card_h, + $radius, + $speed + ); + + $outer = $show_bg ? 'aksrt-partners-scroll-wrap' : 'aksrt-partners-scroll-wrap is-bare'; + echo '
'; + echo '
'; + + foreach ($rows_data as $row_index => $row_partners) { + switch ($direction) { + case 'left': + $dir = 'left'; + break; + case 'right': + $dir = 'right'; + break; + default: + $dir = ($row_index % 2 === 0) ? 'left' : 'right'; + } + echo '
'; + echo '
'; + // Duplicate group for seamless CSS marquee. + for ($dup = 0; $dup < 2; $dup++) { + echo '
'; + foreach ($row_partners as $partner) { + echo $this->render_card($partner, $style_type, $go_link); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } + echo '
'; + } + + echo '
'; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param array $partner + */ + private function render_card(array $partner, string $style_type, bool $go_link): string { + $title = trim((string) ($partner['title'] ?? '')); + $desc = trim((string) ($partner['desc'] ?? '')); + $image = (string) ($partner['image'] ?? ''); + $url = (string) ($partner['url'] ?? ''); + $target = (string) ($partner['target'] ?? ''); + + if ($image === '' && $title === '') { + return ''; + } + + $alt = esc_attr($title !== '' ? $title : __('合作伙伴', 'aksrt')); + $img_tag = $image !== '' + ? '' . $alt . '' + : ''; + + $inner = ''; + if ($style_type === 'logo') { + if ($img_tag === '') { + return ''; + } + $inner = $img_tag; + } elseif ($style_type === 'chip') { + if ($img_tag !== '') { + $inner .= '' . $img_tag . ''; + } + $inner .= '' . esc_html($title) . ''; + } else { + if ($img_tag !== '') { + $inner .= '
' . $img_tag . '
'; + } + $inner .= '
'; + if ($title !== '') { + $inner .= '
' . esc_html($title) . '
'; + } + if ($desc !== '') { + $inner .= '
' . esc_html($desc) . '
'; + } + $inner .= '
'; + } + + if ($url !== '') { + if ($go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $tgt = $target !== '' ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' : ''; + return '' . $inner . ''; + } + + return '
' . $inner . '
'; + } + + /** + * @param array $instance + * @return array + */ + private function resolve_partners(array $instance): array { + $source = sanitize_key((string) ($instance['link_type'] ?? 'manual')); + if ($source === 'link') { + $args = [ + 'orderby' => sanitize_key((string) ($instance['link_orderby'] ?? 'name')) ?: 'name', + 'order' => strtoupper((string) ($instance['link_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC', + 'limit' => max(1, min(100, (int) ($instance['link_limit'] ?? 50))), + ]; + $cats = $instance['link_cats'] ?? ''; + if (is_array($cats)) { + $cats = implode(',', array_map('intval', $cats)); + } + $cats = trim((string) $cats); + if ($cats !== '') { + $args['category'] = $cats; + } + $bookmarks = get_bookmarks($args); + $out = []; + foreach ($bookmarks as $b) { + $out[] = [ + 'title' => (string) $b->link_name, + 'desc' => (string) $b->link_description, + 'image' => (string) $b->link_image, + 'url' => (string) $b->link_url, + 'target' => (string) $b->link_target, + ]; + } + return $out; + } + + return $this->normalize_manual($instance['partners'] ?? []); + } + + /** + * @param mixed $raw + * @return array + */ + private function normalize_manual($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'] ?? '')); + $image = esc_url_raw(trim((string) ($row['image'] ?? ''))); + $url = esc_url_raw(trim((string) ($row['url'] ?? $row['link'] ?? ''))); + if ($title === '' && $image === '') { + continue; + } + $out[] = [ + 'title' => $title, + 'desc' => sanitize_text_field((string) ($row['desc'] ?? '')), + 'image' => $image, + 'url' => $url, + 'target' => sanitize_text_field((string) ($row['target'] ?? '_blank')), + ]; + if (count($out) >= 40) { + break; + } + } + return $out; + } + + public function form($instance): void { + $title = (string) ($instance['title'] ?? ''); + $link_type = (string) ($instance['link_type'] ?? 'manual'); + $partners_json = is_string($instance['partners'] ?? null) + ? (string) $instance['partners'] + : wp_json_encode($this->normalize_manual($instance['partners'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ + +

+

+ +

+

+ +

+

+ +

+

+ + +

+

+ + + +

+

+ +

+

+
+
+
+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'link_type' => in_array(($new_instance['link_type'] ?? ''), ['manual', 'link'], true) ? $new_instance['link_type'] : 'manual', + 'partners' => $this->normalize_manual($new_instance['partners'] ?? []), + 'link_cats' => sanitize_text_field((string) ($new_instance['link_cats'] ?? '')), + 'link_limit' => max(1, min(100, (int) ($new_instance['link_limit'] ?? 50))), + 'link_orderby' => sanitize_key((string) ($new_instance['link_orderby'] ?? 'name')), + 'link_order' => strtoupper((string) ($new_instance['link_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC', + 'style_type' => in_array(($new_instance['style_type'] ?? ''), ['card', 'logo', 'chip'], true) ? $new_instance['style_type'] : 'card', + 'rows' => max(1, min(4, (int) ($new_instance['rows'] ?? 1))), + 'm_rows' => max(1, min(4, (int) ($new_instance['m_rows'] ?? 1))), + 'card_width' => max(60, min(400, (int) ($new_instance['card_width'] ?? 220))), + 'card_height' => max(40, min(200, (int) ($new_instance['card_height'] ?? 90))), + 'card_radius' => max(0, min(40, (int) ($new_instance['card_radius'] ?? 10))), + 'speed' => max(1, min(20, (int) ($new_instance['speed'] ?? 3))), + 'direction' => in_array(($new_instance['direction'] ?? ''), ['alternate', 'left', 'right'], true) ? $new_instance['direction'] : 'alternate', + 'pause_hover' => !empty($new_instance['pause_hover']) ? 1 : 0, + 'card_bg' => !empty($new_instance['card_bg']) ? 1 : 0, + 'end_fade' => !empty($new_instance['end_fade']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'show_widget_bg' => !empty($new_instance['show_widget_bg']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetTextTitle.php b/src/AKSRT/Widgets/WidgetTextTitle.php new file mode 100644 index 00000000..76a64bac --- /dev/null +++ b/src/AKSRT/Widgets/WidgetTextTitle.php @@ -0,0 +1,138 @@ + esc_html__('自定义标题 + 副标题区块,用于模块分隔。', 'aksrt'), + 'classname' => 'widget_aksrt_text_title', + ] + ); + } + + public function widget($args, $instance): void { + $title = trim((string) ($instance['title'] ?? '')); + $desc = trim((string) ($instance['desc'] ?? '')); + if ($title === '' && $desc === '') { + return; + } + + $align = sanitize_key((string) ($instance['align'] ?? 'center')); + if (!in_array($align, ['left', 'center', 'right'], true)) { + $align = 'center'; + } + $icon_left = sanitize_text_field((string) ($instance['title_icon_left'] ?? '')); + $icon_right = sanitize_text_field((string) ($instance['title_icon_right'] ?? '')); + $title_size = max(12, min(48, (int) ($instance['title_size'] ?? 22))); + $desc_size = max(11, min(24, (int) ($instance['desc_size'] ?? 14))); + $title_color = sanitize_hex_color((string) ($instance['title_color'] ?? '')) ?: ''; + $desc_color = sanitize_hex_color((string) ($instance['desc_color'] ?? '')) ?: ''; + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + $style = sprintf( + '--aksrt-tt-title-size:%dpx;--aksrt-tt-desc-size:%dpx;%s%s', + $title_size, + $desc_size, + $title_color !== '' ? '--aksrt-tt-title-color:' . $title_color . ';' : '', + $desc_color !== '' ? '--aksrt-tt-desc-color:' . $desc_color . ';' : '' + ); + + echo '
'; + if ($title !== '') { + echo '
'; + if ($icon_left !== '') { + echo ''; + } + echo '' . esc_html($title) . ''; + if ($icon_right !== '') { + echo ''; + } + echo '
'; + } + if ($desc !== '') { + echo '
' . esc_html($desc) . '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ + +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'desc' => sanitize_text_field((string) ($new_instance['desc'] ?? '')), + 'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center', + 'title_icon_left' => sanitize_text_field((string) ($new_instance['title_icon_left'] ?? '')), + 'title_icon_right' => sanitize_text_field((string) ($new_instance['title_icon_right'] ?? '')), + 'title_size' => max(12, min(48, (int) ($new_instance['title_size'] ?? 22))), + 'desc_size' => max(11, min(24, (int) ($new_instance['desc_size'] ?? 14))), + 'title_color' => sanitize_hex_color((string) ($new_instance['title_color'] ?? '')) ?: '', + 'desc_color' => sanitize_hex_color((string) ($new_instance['desc_color'] ?? '')) ?: '', + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetUserLists.php b/src/AKSRT/Widgets/WidgetUserLists.php new file mode 100644 index 00000000..52012158 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetUserLists.php @@ -0,0 +1,199 @@ + esc_html__('展示站点用户头像网格,支持排序与筛选。', 'aksrt'), + 'classname' => 'widget_aksrt_user_lists', + ] + ); + } + + public function widget($args, $instance): void { + $number = max(1, min(50, (int) ($instance['number'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'user_registered')); + $order = strtoupper((string) ($instance['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; + $include = $this->parse_ids((string) ($instance['include'] ?? '')); + $exclude = $this->parse_ids((string) ($instance['exclude'] ?? '')); + $hide_box = !empty($instance['hide_box']); + + $query = [ + 'number' => $number, + 'order' => $order, + 'fields' => ['ID', 'display_name', 'user_nicename'], + ]; + + if ($include) { + $query['include'] = $include; + $query['orderby'] = 'include'; + } else { + switch ($orderby) { + case 'display_name': + $query['orderby'] = 'display_name'; + break; + case 'post_count': + $query['orderby'] = 'post_count'; + break; + case 'last_login': + $query['meta_key'] = 'aksrt_last_login'; + $query['orderby'] = 'meta_value'; + break; + case 'followed-user-count': + // Fallback: registered date; fan ranking needs denormalized meta when available. + $query['meta_key'] = 'aksrt_follower_count'; + $query['orderby'] = 'meta_value_num'; + break; + default: + $query['orderby'] = 'user_registered'; + } + } + if ($exclude) { + $query['exclude'] = $exclude; + } + + $users = get_users($query); + if (!$users) { + return; + } + + // If follower meta empty for all, re-query by registered when fan order requested. + if ($orderby === 'followed-user-count' && !$include) { + $has_meta = false; + foreach ($users as $u) { + if ((int) get_user_meta($u->ID, 'aksrt_follower_count', true) > 0) { + $has_meta = true; + break; + } + } + if (!$has_meta && class_exists('\AKSRT\User\Follow')) { + // Rank by live follower counts (small N only). + usort($users, static function ($a, $b) { + $fa = \AKSRT\User\Follow::get_instance()->get_follower_count((int) $a->ID); + $fb = \AKSRT\User\Follow::get_instance()->get_follower_count((int) $b->ID); + return $fb <=> $fa; + }); + } + } + + 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 + } + + $wrap = $hide_box ? 'aksrt-user-lists is-bare' : 'aksrt-user-lists'; + echo '
'; + foreach ($users as $user) { + $uid = (int) $user->ID; + $avatar = function_exists('aksrt_get_avatar_url') + ? aksrt_get_avatar_url($uid, ['size' => 96]) + : get_avatar_url($uid, ['size' => 96]); + $home = function_exists('aksrt_get_user_home_url') + ? aksrt_get_user_home_url($uid) + : get_author_posts_url($uid); + $name = $user->display_name ?: $user->user_nicename; + echo ''; + echo '' . esc_attr($name) . ''; + echo '' . esc_html($name) . ''; + echo ''; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @return array + */ + private function parse_ids(string $raw): array { + if ($raw === '') { + return []; + } + $parts = preg_split('/[\s,]+/', $raw) ?: []; + $ids = []; + foreach ($parts as $p) { + $id = (int) $p; + if ($id > 0) { + $ids[] = $id; + } + } + return array_values(array_unique($ids)); + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'number' => max(1, min(50, (int) ($new_instance['number'] ?? 8))), + 'include' => sanitize_text_field((string) ($new_instance['include'] ?? '')), + 'exclude' => sanitize_text_field((string) ($new_instance['exclude'] ?? '')), + 'orderby' => $orderby, + 'order' => strtoupper((string) ($new_instance['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC', + 'hide_box'=> !empty($new_instance['hide_box']) ? 1 : 0, + ]; + } +} From d8682ce1fe1c8100192bdd75998be3959371978d Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:22:31 +0800 Subject: [PATCH 2/4] feat(widgets): pricing/FAQ/stats + forum topic list and bbs sidebars Add zibll produck widgets (pricing plans, FAQ, stats), forum topic lists, register bbs_* sidebar zones, and wire plate/topic/home/compose templates to render those widget areas. --- assets/css/frontend/widget-batch3.css | 207 +++++++++++++++++ inc/modules/widgets-register.php | 8 + src/AKSRT/Core/SetupPart1.php | 23 ++ src/AKSRT/Widgets/WidgetFaq.php | 189 ++++++++++++++++ src/AKSRT/Widgets/WidgetForumTopicLists.php | 176 +++++++++++++++ src/AKSRT/Widgets/WidgetPricingPlans.php | 236 ++++++++++++++++++++ src/AKSRT/Widgets/WidgetStats.php | 167 ++++++++++++++ templates/forum/forum-compose.php | 13 ++ templates/forum/forum-plate.php | 8 +- templates/forum/forum-topic.php | 5 + templates/pages/page-forums.php | 5 + 11 files changed, 1036 insertions(+), 1 deletion(-) create mode 100644 src/AKSRT/Widgets/WidgetFaq.php create mode 100644 src/AKSRT/Widgets/WidgetForumTopicLists.php create mode 100644 src/AKSRT/Widgets/WidgetPricingPlans.php create mode 100644 src/AKSRT/Widgets/WidgetStats.php diff --git a/assets/css/frontend/widget-batch3.css b/assets/css/frontend/widget-batch3.css index 6853d0f9..5479da84 100644 --- a/assets/css/frontend/widget-batch3.css +++ b/assets/css/frontend/widget-batch3.css @@ -530,3 +530,210 @@ color: var(--color-primary, #3b82f6); flex-shrink: 0; } + + +/* ---- Pricing plans ---- */ +.aksrt-pricing { margin-bottom: 16px; } +.aksrt-pricing__cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} +.aksrt-pricing__card { + position: relative; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 14px; + padding: 22px 18px 18px; + background: var(--surface-1, #fff); + display: flex; + flex-direction: column; + min-height: 100%; +} +.aksrt-pricing__card.is-featured { + box-shadow: 0 10px 28px rgba(0,0,0,.08); + border-color: transparent; + border-top: 3px solid var(--plan-accent, #3b82f6); +} +.aksrt-pricing__card.accent-pink { --plan-accent: #ec4899; } +.aksrt-pricing__card.accent-purple { --plan-accent: #a855f7; } +.aksrt-pricing__card.accent-blue { --plan-accent: #3b82f6; } +.aksrt-pricing__card.accent-green { --plan-accent: #22c55e; } +.aksrt-pricing__card.accent-yellow { --plan-accent: #eab308; } +.aksrt-pricing__card.accent-cyan { --plan-accent: #06b6d4; } +.aksrt-pricing__badge { + position: absolute; + top: 12px; + right: 12px; + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + background: var(--plan-accent, #3b82f6); + color: #fff; +} +.aksrt-pricing__name { font-size: 18px; font-weight: 700; margin: 0 0 6px; } +.aksrt-pricing__desc { font-size: 13px; color: var(--muted-2-color, #888); margin: 0 0 12px; } +.aksrt-pricing__price { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; } +.aksrt-pricing__main { font-size: 32px; font-weight: 800; color: var(--plan-accent, #3b82f6); line-height: 1; } +.aksrt-pricing__mark { font-size: .55em; margin-right: 2px; } +.aksrt-pricing__original { font-size: 13px; color: #999; text-decoration: line-through; } +.aksrt-pricing__features { list-style: none; margin: 0 0 16px; padding: 0; flex: 1; } +.aksrt-pricing__features li { display: flex; gap: 8px; font-size: 13px; padding: 5px 0; } +.aksrt-pricing__features i { color: var(--plan-accent, #3b82f6); margin-top: 2px; } +.aksrt-pricing__btn { + display: block; + text-align: center; + padding: 10px 14px; + border-radius: 999px; + background: var(--plan-accent, #3b82f6); + color: #fff !important; + text-decoration: none; + font-weight: 600; +} +.aksrt-pricing__trust { + display: flex; + flex-wrap: wrap; + gap: 14px 22px; + margin-top: 18px; + justify-content: center; +} +.aksrt-pricing__trust-item { display: flex; gap: 8px; align-items: flex-start; font-size: 13px; } +.aksrt-pricing__trust-item i { color: var(--color-primary, #3b82f6); margin-top: 2px; } +.aksrt-pricing__trust-item strong { display: block; } +.aksrt-pricing__trust-item span { color: var(--muted-2-color, #888); font-size: 12px; } + +/* ---- FAQ ---- */ +.aksrt-faq { margin-bottom: 16px; } +.aksrt-faq__search { display: flex; gap: 8px; margin-bottom: 14px; } +.aksrt-faq__search-input { + flex: 1; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 8px; + padding: 8px 12px; +} +.aksrt-faq__search-btn { + border: 0; + border-radius: 8px; + padding: 8px 14px; + background: var(--color-primary, #3b82f6); + color: #fff; + cursor: pointer; +} +.aksrt-faq__item { + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 10px; + padding: 0; + margin-bottom: 10px; + background: var(--surface-1, #fff); +} +.aksrt-faq__q { + list-style: none; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + cursor: pointer; + font-weight: 600; +} +.aksrt-faq__q::-webkit-details-marker { display: none; } +.aksrt-faq__num { + width: 28px; + height: 28px; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + background: var(--surface-2, #f3f4f6); + color: var(--color-primary, #3b82f6); + flex-shrink: 0; +} +.aksrt-faq__a { + padding: 0 14px 14px 52px; + font-size: 13px; + color: var(--muted-2-color, #666); + line-height: 1.6; +} +.aksrt-faq__aside { display: grid; gap: 12px; margin-top: 14px; } +.aksrt-faq__doc, .aksrt-faq__help { + border-radius: 10px; + border: 1px solid var(--color-line, #e5e7eb); + padding: 14px; + background: var(--surface-2, #f8fafc); +} +.aksrt-faq__help-btn { + display: inline-block; + margin-top: 8px; + padding: 6px 12px; + border-radius: 999px; + background: var(--color-primary, #3b82f6); + color: #fff !important; + text-decoration: none; + font-size: 13px; +} + +/* ---- Stats ---- */ +.aksrt-stats { margin-bottom: 16px; } +.aksrt-stats__intro { margin-bottom: 14px; } +.aksrt-stats__intro-title { font-size: 22px; margin: 0 0 6px; } +.aksrt-stats__intro-sub { color: var(--muted-2-color, #888); margin: 0; font-size: 13px; } +.aksrt-stats__grid { + display: grid; + grid-template-columns: repeat(var(--aksrt-stats-pc, 4), minmax(0, 1fr)); + gap: 12px; +} +@media (max-width: 767.98px) { + .aksrt-stats__grid { + grid-template-columns: repeat(var(--aksrt-stats-m, 2), minmax(0, 1fr)); + } +} +.aksrt-stats__item { + border-radius: 12px; + padding: 16px 14px; + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + text-align: center; +} +.aksrt-stats__item.color-pink { --stat-c: #ec4899; } +.aksrt-stats__item.color-purple { --stat-c: #a855f7; } +.aksrt-stats__item.color-orange { --stat-c: #f97316; } +.aksrt-stats__item.color-blue { --stat-c: #3b82f6; } +.aksrt-stats__item.color-green { --stat-c: #22c55e; } +.aksrt-stats__icon { font-size: 20px; color: var(--stat-c, #3b82f6); margin-bottom: 8px; } +.aksrt-stats__value { font-size: 28px; font-weight: 800; color: var(--stat-c, #3b82f6); line-height: 1.1; } +.aksrt-stats__prefix, .aksrt-stats__unit { font-size: .55em; font-weight: 600; } +.aksrt-stats__label { margin-top: 6px; font-size: 12px; color: var(--muted-2-color, #888); } + +/* ---- Forum topic lists ---- */ +.aksrt-ftopic-list { list-style: none; margin: 0; padding: 0; } +.aksrt-ftopic-list__item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 0; + border-bottom: 1px dashed var(--color-line, #e5e7eb); +} +.aksrt-ftopic-list__item:last-child { border-bottom: 0; } +.aksrt-ftopic-list__link { + flex: 1; + min-width: 0; + color: inherit; + text-decoration: none; + font-size: 13px; + line-height: 1.45; +} +.aksrt-ftopic-list__views { + flex-shrink: 0; + font-size: 11px; + color: var(--muted-2-color, #999); +} +.aksrt-ftopic-list.is-detail .aksrt-ftopic-list__item { flex-direction: column; gap: 4px; } +.aksrt-ftopic-list__meta { + display: flex; + gap: 10px; + font-size: 11px; + color: var(--muted-2-color, #999); +} + +/* Forum widget sidebar shell */ +.aksrt-forum-widget-zone { margin-top: 12px; } +.aksrt-forum-widget-zone .zib-widget { margin-bottom: 14px; } diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index 124e3d15..885cdc44 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -63,6 +63,11 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetTextTitle'); register_widget('\AKSRT\Widgets\WidgetUserLists'); + register_widget('\AKSRT\Widgets\WidgetPricingPlans'); + register_widget('\AKSRT\Widgets\WidgetFaq'); + register_widget('\AKSRT\Widgets\WidgetStats'); + + // Forum widgets if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { register_widget('\AKSRT\Widgets\WidgetForumBoardList'); @@ -82,6 +87,9 @@ function aksrt_register_widgets() { if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) { register_widget('\AKSRT\Widgets\WidgetForumPlateModerator'); } + if (class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { + register_widget('\AKSRT\Widgets\WidgetForumTopicLists'); + } // Shop product widgets (§6.9) register_widget('\AKSRT\Widgets\WidgetProductList'); diff --git a/src/AKSRT/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index 61345257..26634b99 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -382,6 +382,29 @@ trait SetupPart1 { 'after_title' => $title_after, ]); + // ── Forum / BBS widget areas (zibll bbs_* parity lite) ── + $forum_widgets = [ + 'bbs_home_sidebar' => [__('论坛首页 · 侧栏', 'aksrt'), __('论坛首页侧栏区域。', 'aksrt')], + 'bbs_home_top_fluid' => [__('论坛首页 · 顶部全宽', 'aksrt'), __('论坛首页顶部全宽。', 'aksrt')], + 'bbs_plate_sidebar' => [__('论坛版块 · 侧栏', 'aksrt'), __('版块页侧栏。', 'aksrt')], + 'bbs_plate_top_fluid' => [__('论坛版块 · 顶部全宽', 'aksrt'), __('版块页顶部全宽。', 'aksrt')], + 'bbs_posts_sidebar' => [__('论坛帖子 · 侧栏', 'aksrt'), __('帖子页侧栏挂件区。', 'aksrt')], + 'bbs_posts_top_fluid' => [__('论坛帖子 · 顶部全宽', 'aksrt'), __('帖子页顶部全宽。', 'aksrt')], + 'bbs_new_posts_sidebar_top' => [__('论坛发帖 · 侧栏顶部', 'aksrt'), __('发帖页侧栏顶部。', 'aksrt')], + 'bbs_new_posts_sidebar_bottom' => [__('论坛发帖 · 侧栏底部', 'aksrt'), __('发帖页侧栏底部。', 'aksrt')], + ]; + foreach ($forum_widgets as $sid => $meta) { + register_sidebar([ + 'id' => $sid, + 'name' => $meta[0], + 'description' => $meta[1], + 'before_widget' => $card_wrapper_before, + 'after_widget' => '', + 'before_title' => $title_before, + 'after_title' => $title_after, + ]); + } + // ── Shop widget areas (§6.9) ── $shop_widgets = [ 'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'], diff --git a/src/AKSRT/Widgets/WidgetFaq.php b/src/AKSRT/Widgets/WidgetFaq.php new file mode 100644 index 00000000..138a4f8b --- /dev/null +++ b/src/AKSRT/Widgets/WidgetFaq.php @@ -0,0 +1,189 @@ + esc_html__('可折叠 FAQ 列表,支持搜索框与侧栏文档卡。', 'aksrt'), + 'classname' => 'widget_aksrt_faq', + ] + ); + } + + public function widget($args, $instance): void { + $faqs = $this->normalize_faqs($instance['faqs'] ?? []); + if (!$faqs) { + 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 + } + + $uid = 'aksrt-faq-' . esc_attr($this->id); + echo '
'; + + if (!empty($instance['search_s'])) { + $ph = (string) ($instance['search_placeholder'] ?? __('搜索问题…', 'aksrt')); + echo ''; + } + + echo '
'; + $n = 0; + foreach ($faqs as $f) { + $n++; + $num = str_pad((string) $n, 2, '0', STR_PAD_LEFT); + echo '
'; + echo '' . esc_html($num) . '' . esc_html($f['question']) . ''; + if ($f['answer'] !== '') { + echo '
' . wp_kses_post(wpautop($f['answer'])) . '
'; + } + echo '
'; + } + echo '
'; + + $doc_title = trim((string) ($instance['doc_title'] ?? '')); + $doc_desc = trim((string) ($instance['doc_desc'] ?? '')); + $help_title = trim((string) ($instance['help_title'] ?? '')); + $help_url = esc_url_raw(trim((string) ($instance['help_btn_url'] ?? ''))); + $help_btn = trim((string) ($instance['help_btn_text'] ?? '')); + if ($doc_title !== '' || $help_title !== '') { + echo '
'; + if ($doc_title !== '') { + echo '
' . esc_html($doc_title) . ''; + if ($doc_desc !== '') { + echo '

' . esc_html($doc_desc) . '

'; + } + echo '
'; + } + if ($help_title !== '') { + echo '
' . esc_html($help_title) . ''; + $sub = trim((string) ($instance['help_subtitle'] ?? '')); + if ($sub !== '') { + echo '

' . esc_html($sub) . '

'; + } + if ($help_btn !== '' && $help_url !== '') { + echo '' . esc_html($help_btn) . ''; + } + echo '
'; + } + echo '
'; + } + + echo '
'; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param mixed $raw + * @return array + */ + private function normalize_faqs($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; + } + $q = sanitize_text_field((string) ($row['question'] ?? $row['q'] ?? '')); + if ($q === '') { + continue; + } + $a = (string) ($row['answer'] ?? $row['a'] ?? ''); + // Allow basic HTML in answers after kses on output. + $out[] = [ + 'question' => $q, + 'answer' => wp_kses_post($a), + ]; + if (count($out) >= 30) { + break; + } + } + return $out; + } + + public function form($instance): void { + $faqs = is_string($instance['faqs'] ?? null) + ? (string) $instance['faqs'] + : wp_json_encode($this->normalize_faqs($instance['faqs'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'faqs' => $this->normalize_faqs($new_instance['faqs'] ?? []), + 'search_s' => !empty($new_instance['search_s']) ? 1 : 0, + 'search_placeholder' => sanitize_text_field((string) ($new_instance['search_placeholder'] ?? '')), + 'search_btn_text' => sanitize_text_field((string) ($new_instance['search_btn_text'] ?? '')), + 'doc_title' => sanitize_text_field((string) ($new_instance['doc_title'] ?? '')), + 'doc_desc' => sanitize_text_field((string) ($new_instance['doc_desc'] ?? '')), + 'help_title' => sanitize_text_field((string) ($new_instance['help_title'] ?? '')), + 'help_subtitle' => sanitize_text_field((string) ($new_instance['help_subtitle'] ?? '')), + 'help_btn_text' => sanitize_text_field((string) ($new_instance['help_btn_text'] ?? '')), + 'help_btn_url' => esc_url_raw(trim((string) ($new_instance['help_btn_url'] ?? ''))), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetForumTopicLists.php b/src/AKSRT/Widgets/WidgetForumTopicLists.php new file mode 100644 index 00000000..72d559e1 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetForumTopicLists.php @@ -0,0 +1,176 @@ + esc_html__('按排序/版块筛选展示论坛帖子。', 'aksrt'), + 'classname' => 'widget_aksrt_forum_topic_lists', + ] + ); + } + + public function widget($args, $instance): void { + if (!post_type_exists('aksrt_forum_topic')) { + return; + } + + $count = max(1, min(30, (int) ($instance['count'] ?? 8))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $style = sanitize_key((string) ($instance['style'] ?? 'mini')); + $plate_id = (int) ($instance['plate_id'] ?? 0); + $current_plate = !empty($instance['current_plate']); + + if ($current_plate) { + $resolved = $this->resolve_current_plate_id(); + if ($resolved <= 0) { + return; + } + $plate_id = $resolved; + } + + $query = [ + 'post_type' => 'aksrt_forum_topic', + 'post_status' => 'publish', + 'posts_per_page' => $count, + 'order' => 'DESC', + ]; + + switch ($orderby) { + case 'views': + $query['meta_key'] = '_aksrt_views'; + $query['orderby'] = 'meta_value_num'; + break; + case 'comments': + $query['orderby'] = 'comment_count'; + break; + case 'modified': + $query['orderby'] = 'modified'; + break; + default: + $query['orderby'] = 'date'; + } + + if ($plate_id > 0) { + $query['meta_query'] = [[ + 'key' => '_aksrt_plate_id', + 'value' => $plate_id, + ]]; + } + + $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 + } + + $list_class = $style === 'detail' ? 'aksrt-ftopic-list is-detail' : 'aksrt-ftopic-list is-mini'; + echo '
    '; + while ($q->have_posts()) { + $q->the_post(); + $views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true); + echo '
  • '; + echo '' . esc_html(get_the_title()) . ''; + if ($style === 'detail') { + echo '
    '; + echo '' . esc_html(get_the_author()) . ''; + echo '' . esc_html(get_the_date()) . ''; + echo ' ' . esc_html((string) $views) . ''; + echo '
    '; + } else { + echo '' . esc_html((string) $views) . ''; + } + echo '
  • '; + } + echo '
'; + wp_reset_postdata(); + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function resolve_current_plate_id(): int { + if (is_singular('aksrt_forum_plate')) { + return (int) get_queried_object_id(); + } + if (is_singular('aksrt_forum_topic')) { + $topic_id = (int) get_queried_object_id(); + $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); + return max(0, $plate_id); + } + return 0; + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(30, (int) ($new_instance['count'] ?? 8))), + 'orderby' => $orderby, + 'style' => ($new_instance['style'] ?? '') === 'detail' ? 'detail' : 'mini', + 'plate_id' => max(0, (int) ($new_instance['plate_id'] ?? 0)), + 'current_plate' => !empty($new_instance['current_plate']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetPricingPlans.php b/src/AKSRT/Widgets/WidgetPricingPlans.php new file mode 100644 index 00000000..a6d11bdb --- /dev/null +++ b/src/AKSRT/Widgets/WidgetPricingPlans.php @@ -0,0 +1,236 @@ + esc_html__('多列定价卡片 + 信任条(落地页/VIP 介绍)。', 'aksrt'), + 'classname' => 'widget_aksrt_pricing_plans', + ] + ); + } + + public function widget($args, $instance): void { + $plans = $this->normalize_plans($instance['lists'] ?? $instance['plans'] ?? []); + if (!$plans) { + return; + } + $trust = $this->normalize_trust($instance['trust_items'] ?? []); + $currency = sanitize_text_field((string) ($instance['currency'] ?? '¥')); + if ($currency === '') { + $currency = '¥'; + } + + 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 '
'; + foreach ($plans as $plan) { + $accent = sanitize_key((string) ($plan['color'] ?? 'blue')); + if (!in_array($accent, ['pink', 'purple', 'blue', 'green', 'yellow', 'cyan'], true)) { + $accent = 'blue'; + } + $featured = !empty($plan['is_featured']); + $class = 'aksrt-pricing__card accent-' . $accent . ($featured ? ' is-featured' : ''); + echo '
'; + if ($featured || !empty($plan['tag'])) { + $badge = (string) ($plan['tag'] !== '' ? $plan['tag'] : __('推荐', 'aksrt')); + echo '' . esc_html($badge) . ''; + } + echo '

' . esc_html($plan['name']) . '

'; + if ($plan['desc'] !== '') { + echo '

' . esc_html($plan['desc']) . '

'; + } + $price = (float) $plan['price']; + $show = (float) $plan['show_price']; + echo '
'; + echo '' . esc_html($currency) . '' . esc_html($this->fmt($price)) . ''; + if ($show > $price && $show > 0) { + echo '' . esc_html($currency) . '' . esc_html($this->fmt($show)) . ''; + } + echo '
'; + if ($plan['features']) { + echo '
    '; + foreach ($plan['features'] as $ft) { + echo '
  • ' . esc_html($ft) . '
  • '; + } + echo '
'; + } + if ($plan['btn_text'] !== '' && $plan['btn_url'] !== '') { + $url = $plan['btn_url']; + if (!empty($instance['go_link']) && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + echo ''; + } + echo '
'; + } + echo '
'; + + if ($trust) { + echo '
'; + foreach ($trust as $ti) { + echo '
'; + if ($ti['icon'] !== '') { + echo ''; + } + echo '
' . esc_html($ti['title']) . ''; + if ($ti['desc'] !== '') { + echo '' . esc_html($ti['desc']) . ''; + } + echo '
'; + } + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function fmt(float $n): string { + if (abs($n - round($n)) < 0.001) { + return (string) (int) round($n); + } + return number_format($n, 2, '.', ''); + } + + /** + * @param mixed $raw + * @return array> + */ + private function normalize_plans($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'] ?? $row['title'] ?? '')); + if ($name === '') { + continue; + } + $intro = (string) ($row['intro'] ?? $row['features'] ?? ''); + if (is_array($row['features'] ?? null)) { + $features = array_values(array_filter(array_map('sanitize_text_field', $row['features']))); + } else { + $features = array_values(array_filter(array_map('sanitize_text_field', preg_split('/\r\n|\r|\n/', $intro) ?: []))); + } + $btn = is_array($row['btn'] ?? null) ? $row['btn'] : []; + $out[] = [ + 'name' => $name, + 'desc' => sanitize_text_field((string) ($row['desc'] ?? '')), + 'price' => (float) ($row['price'] ?? 0), + 'show_price' => (float) ($row['show_price'] ?? 0), + 'color' => sanitize_key((string) ($row['color'] ?? 'blue')), + 'is_featured' => !empty($row['is_featured']) ? 1 : 0, + 'tag' => sanitize_text_field((string) ($row['tag'] ?? '')), + 'features' => $features, + 'btn_text' => sanitize_text_field((string) ($row['btn_text'] ?? $btn['text'] ?? '')), + 'btn_url' => esc_url_raw(trim((string) ($row['btn_url'] ?? $btn['url'] ?? ''))), + ]; + if (count($out) >= 6) { + break; + } + } + return $out; + } + + /** + * @param mixed $raw + * @return array + */ + private function normalize_trust($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'] ?? '')); + if ($title === '') { + continue; + } + $out[] = [ + 'title' => $title, + 'desc' => sanitize_text_field((string) ($row['desc'] ?? '')), + 'icon' => sanitize_text_field((string) ($row['icon'] ?? 'fa fa-check-circle')), + ]; + if (count($out) >= 6) { + break; + } + } + return $out; + } + + public function form($instance): void { + $plans = is_string($instance['lists'] ?? null) + ? (string) $instance['lists'] + : wp_json_encode($this->normalize_plans($instance['lists'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $trust = is_string($instance['trust_items'] ?? null) + ? (string) $instance['trust_items'] + : wp_json_encode($this->normalize_trust($instance['trust_items'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'currency' => sanitize_text_field((string) ($new_instance['currency'] ?? '¥')), + 'lists' => $this->normalize_plans($new_instance['lists'] ?? []), + 'trust_items' => $this->normalize_trust($new_instance['trust_items'] ?? []), + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetStats.php b/src/AKSRT/Widgets/WidgetStats.php new file mode 100644 index 00000000..5614ee31 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetStats.php @@ -0,0 +1,167 @@ + esc_html__('数字统计卡片网格(站点数据展示)。', 'aksrt'), + 'classname' => 'widget_aksrt_stats', + ] + ); + } + + public function widget($args, $instance): void { + $stats = $this->normalize_stats($instance['stats'] ?? []); + if (!$stats) { + return; + } + $cols = max(1, min(4, (int) ($instance['columns'] ?? 4))); + $m_cols = max(1, min(3, (int) ($instance['mobile_columns'] ?? 2))); + $intro_title = trim((string) ($instance['intro_title'] ?? '')); + $intro_sub = trim((string) ($instance['intro_subtitle'] ?? '')); + + 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-stats-pc:%d;--aksrt-stats-m:%d;', $cols, $m_cols); + echo '
'; + if ($intro_title !== '' || $intro_sub !== '') { + echo '
'; + if ($intro_title !== '') { + echo '

' . esc_html($intro_title) . '

'; + } + if ($intro_sub !== '') { + echo '

' . esc_html($intro_sub) . '

'; + } + echo '
'; + } + echo '
'; + $palette = ['pink', 'purple', 'orange', 'blue', 'green']; + $i = 0; + foreach ($stats as $s) { + $color = sanitize_key((string) ($s['color'] ?? 'auto')); + if ($color === '' || $color === 'auto') { + $color = $palette[$i % count($palette)]; + } + $i++; + echo '
'; + if ($s['icon'] !== '') { + echo '
'; + } + echo '
'; + if ($s['prefix'] !== '') { + echo '' . esc_html($s['prefix']) . ''; + } + echo '' . esc_html($s['number']) . ''; + if ($s['unit'] !== '') { + echo '' . esc_html($s['unit']) . ''; + } + echo '
'; + if ($s['label'] !== '') { + echo '
' . esc_html($s['label']) . '
'; + } + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param mixed $raw + * @return array + */ + private function normalize_stats($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; + } + $number = sanitize_text_field((string) ($row['number'] ?? '')); + if ($number === '') { + continue; + } + $out[] = [ + 'number' => $number, + 'label' => sanitize_text_field((string) ($row['label'] ?? '')), + 'unit' => sanitize_text_field((string) ($row['unit'] ?? '')), + 'prefix' => sanitize_text_field((string) ($row['prefix'] ?? '')), + 'icon' => sanitize_text_field((string) ($row['icon'] ?? '')), + 'color' => sanitize_key((string) ($row['color'] ?? 'auto')), + ]; + if (count($out) >= 12) { + break; + } + } + return $out; + } + + public function form($instance): void { + $stats = is_string($instance['stats'] ?? null) + ? (string) $instance['stats'] + : wp_json_encode($this->normalize_stats($instance['stats'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ + +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'stats' => $this->normalize_stats($new_instance['stats'] ?? []), + 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 4))), + 'mobile_columns' => max(1, min(3, (int) ($new_instance['mobile_columns'] ?? 2))), + 'intro_title' => sanitize_text_field((string) ($new_instance['intro_title'] ?? '')), + 'intro_subtitle' => sanitize_text_field((string) ($new_instance['intro_subtitle'] ?? '')), + ]; + } +} diff --git a/templates/forum/forum-compose.php b/templates/forum/forum-compose.php index 35ba22af..7a09320b 100644 --- a/templates/forum/forum-compose.php +++ b/templates/forum/forum-compose.php @@ -436,6 +436,19 @@ $initial_content = $draft_data ? $draft_data['content'] : ''; + +
+ +
+ + diff --git a/templates/forum/forum-plate.php b/templates/forum/forum-plate.php index 2fcebeed..e6feb75e 100644 --- a/templates/forum/forum-plate.php +++ b/templates/forum/forum-plate.php @@ -56,8 +56,9 @@ $max_pages = (int) ($view['pagination']['max_pages'] ?? 1); ?>
+ '; dynamic_sidebar('bbs_plate_top_fluid'); echo ''; } ?>
-
+
@@ -318,6 +319,11 @@ $max_pages = (int) ($view['pagination']['max_pages'] ?? 1);
+ + +
diff --git a/templates/forum/forum-topic.php b/templates/forum/forum-topic.php index 5ed99a1b..53a223c7 100644 --- a/templates/forum/forum-topic.php +++ b/templates/forum/forum-topic.php @@ -412,6 +412,11 @@ $comments_enabled = !empty($permissions['can_reply']); + +
+ +
+ + +
+ +
+ From 47d202bb96de99606c09a517c44f9a45c5d74ed1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:25:43 +0800 Subject: [PATCH 3/4] 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))), + ]; + } +} From 9356faf13d91fffba882ec1f704fc0b7a9c6db5c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 00:29:37 +0800 Subject: [PATCH 4/4] 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 '
    '; + while ($q->have_posts()) { + $q->the_post(); + $price = (float) get_post_meta(get_the_ID(), '_aksrt_pay_price', true); + $credits = (int) get_post_meta(get_the_ID(), '_aksrt_credits_cost', true); + $thumb = get_the_post_thumbnail_url(get_the_ID(), 'thumbnail'); + echo '
  • '; + if ($thumb) { + echo ''; + } + echo '
    '; + echo '' . esc_html(get_the_title()) . ''; + echo '
    '; + if ($price > 0) { + echo '¥' . esc_html(number_format($price, 2, '.', '')) . ''; + } elseif ($credits > 0) { + echo '' . esc_html(sprintf(__('%d 积分', 'aksrt'), $credits)) . ''; + } else { + echo '' . esc_html__('付费', 'aksrt') . ''; + } + 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'] ?? 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, + ]; + } +}