From 66ee624cb99ff6095bcd6bafaced9f01cc68dfd1 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Jul 2026 09:22:01 +0800 Subject: [PATCH] feat(widgets): buttons/text-title/yiyan depth + batch15 CSS Buttons gain zibll but/hollow/color + obs animation; text-title supports per-char entrance; yiyan wraps main-bg shell with affix/animate options. --- assets/css/frontend/widget-batch5.css | 184 ++++++++++++++++++++++++++ src/AKSRT/Widgets/ButtonsWidget.php | 182 +++++++++++++++++-------- src/AKSRT/Widgets/WidgetTextTitle.php | 104 ++++++++++++--- src/AKSRT/Widgets/WidgetYiyan.php | 85 +++++++++--- 4 files changed, 457 insertions(+), 98 deletions(-) diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css index 61d5b6d6..45c11701 100644 --- a/assets/css/frontend/widget-batch5.css +++ b/assets/css/frontend/widget-batch5.css @@ -1825,3 +1825,187 @@ font-weight: var(--font-weight-sm, 600); } } + +/* ===== Batch15: icon-card / icon-cover / buttons / yiyan / text-title ===== */ + +.icon-card { + text-align: center; + padding: 16px 10px; +} +.icon-card .card-icon { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + color: var(--theme-color, var(--focus-color, #f04494)); +} +.icon-card .card-icon.toggle-radius { + width: 3.2em; + height: 3.2em; + border-radius: 50%; + background: rgba(240, 68, 148, 0.1); +} +.icon-card .card-icon img { + width: 1em; + height: 1em; + object-fit: contain; +} +.icon-card .main-color { + color: inherit; + text-decoration: none; +} +.icon-card .main-color:hover { + color: var(--theme-color, var(--focus-color, #f04494)); +} + +.icon-cover-card { + display: flex; + align-items: center; + gap: 0; + padding: 12px; + border-radius: 8px; + background: var(--surface-1, #fff); + border: 1px solid var(--color-line, #e5e7eb); + transition: box-shadow .2s ease, transform .2s ease; +} +a.main-color > .icon-cover-card:hover, +.icon-cover-card:hover { + box-shadow: 0 6px 18px rgba(0,0,0,.06); + transform: translateY(-1px); +} +.icon-cover-icon.badg.cir { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.4em; + height: 2.4em; + min-width: 2.4em; + border-radius: 50%; + background: rgba(59, 130, 246, 0.12); + color: #2563eb; + flex-shrink: 0; +} +.icon-cover-icon.radius4 { + border-radius: 10px !important; +} +.icon-cover-icon img { + width: 70%; + height: 70%; + object-fit: contain; +} +.icon-cover-desc { + min-width: 0; +} +.icon-cover-desc .font-bold { + font-weight: 700; +} + +/* Buttons wrap (zibll flex hh) */ +.widget-buttons .widget-buttons-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--pc-gap, 12px); +} +.widget-buttons .widget-buttons-wrap.align-center { justify-content: center; } +.widget-buttons .widget-buttons-wrap.align-left { justify-content: flex-start; } +.widget-buttons .widget-buttons-wrap.align-right { justify-content: flex-end; } +.widget-buttons .widget-buttons-wrap .but { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 8px 16px; + border-radius: 6px; + text-decoration: none; + font-size: 14px; + line-height: 1.3; + background: var(--theme-color, #3b82f6); + color: #fff; + border: 1px solid transparent; + min-width: var(--pc-width, auto); +} +.widget-buttons .widget-buttons-wrap .but.radius { + border-radius: 999px; +} +.widget-buttons .widget-buttons-wrap .but.hollow { + background: transparent; + color: var(--theme-color, #3b82f6); + border-color: currentColor; +} +.widget-buttons .widget-buttons-wrap .but.c-blue { background: #3b82f6; } +.widget-buttons .widget-buttons-wrap .but.c-green { background: #22c55e; } +.widget-buttons .widget-buttons-wrap .but.c-yellow { background: #eab308; color: #1a1a1a; } +.widget-buttons .widget-buttons-wrap .but.c-red { background: #ef4444; } +.widget-buttons .widget-buttons-wrap .but.c-purple { background: #a855f7; } +.widget-buttons .widget-buttons-wrap .but.hollow.c-blue { color: #3b82f6; background: transparent; } +.widget-buttons .widget-buttons-wrap .but.hollow.c-green { color: #22c55e; background: transparent; } +.widget-buttons .widget-buttons-wrap .but.hollow.c-red { color: #ef4444; background: transparent; } +.widget-buttons .widget-buttons-wrap .but.hollow.c-purple { color: #a855f7; background: transparent; } +.widget-buttons .widget-buttons-wrap .but.hollow.c-yellow { color: #ca8a04; background: transparent; } +.widget-buttons.pc-size-small .but { padding: 4px 10px; font-size: 12px; } +.widget-buttons.pc-size-medium .but { padding: 6px 14px; font-size: 13px; } +.widget-buttons.pc-size-large .but { padding: 10px 18px; font-size: 15px; } +@media (max-width: 767px) { + .widget-buttons .widget-buttons-wrap { + gap: var(--m-gap, 6px); + } + .widget-buttons .widget-buttons-wrap .but { + min-width: var(--m-width, auto); + } + .widget-buttons.m-size-small .but { padding: 4px 10px; font-size: 12px; } + .widget-buttons.m-size-medium .but { padding: 6px 12px; font-size: 13px; } + .widget-buttons.m-size-large .but { padding: 8px 14px; font-size: 14px; } +} + +/* Yiyan shell */ +.yiyan-box.main-bg, +.yiyan-box-wrap .yiyan-box { + background: var(--surface-1, #fff); + border-radius: 8px; + box-shadow: 0 0 10px var(--main-shadow, rgba(116,116,116,.08)); + padding: 18px 16px; + text-align: center; +} +.yiyan-box .aksrt-yiyan, +.yiyan-box .yiyan { + margin: 0; +} + +/* Text title char animation inherits data-aksrt-animate */ +.aksrt-text-title__main > span[data-aksrt-animate] { + display: inline-block; +} + +/* Shared icon grid columns (reuse term-cards pattern) */ +.widget_aksrt_icon_cards .row.gutters-5, +.widget_aksrt_icon_cover_cards .row.gutters-5 { + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +} +.widget_aksrt_icon_cards .row.gutters-5 > [class*="col-"], +.widget_aksrt_icon_cover_cards .row.gutters-5 > [class*="col-"] { + padding: 0 4px; + box-sizing: border-box; +} +.widget_aksrt_icon_cards .col-sm-12, +.widget_aksrt_icon_cover_cards .col-sm-12 { width: 100%; } +.widget_aksrt_icon_cards .col-sm-6, +.widget_aksrt_icon_cover_cards .col-sm-6 { width: 50%; } +.widget_aksrt_icon_cards .col-sm-4, +.widget_aksrt_icon_cover_cards .col-sm-4 { width: 33.333%; } +.widget_aksrt_icon_cards .col-sm-3, +.widget_aksrt_icon_cover_cards .col-sm-3 { width: 25%; } +.widget_aksrt_icon_cards .col-sm-2, +.widget_aksrt_icon_cover_cards .col-sm-2 { width: 16.666%; } +@media (max-width: 767px) { + .widget_aksrt_icon_cards .col-xs-12, + .widget_aksrt_icon_cover_cards .col-xs-12 { width: 100%; } + .widget_aksrt_icon_cards .col-xs-6, + .widget_aksrt_icon_cover_cards .col-xs-6 { width: 50%; } + .widget_aksrt_icon_cards .col-xs-4, + .widget_aksrt_icon_cover_cards .col-xs-4 { width: 33.333%; } + .widget_aksrt_icon_cards .col-xs-3, + .widget_aksrt_icon_cover_cards .col-xs-3 { width: 25%; } +} diff --git a/src/AKSRT/Widgets/ButtonsWidget.php b/src/AKSRT/Widgets/ButtonsWidget.php index 20eb79b2..b28a6bb3 100644 --- a/src/AKSRT/Widgets/ButtonsWidget.php +++ b/src/AKSRT/Widgets/ButtonsWidget.php @@ -1,6 +1,6 @@ esc_html__('显示一组自定义按钮,支持外链跳转与样式。', 'aksrt'), + 'description' => esc_html__('显示一组自定义按钮,支持外链跳转、色系与入场动画。', 'aksrt'), + 'classname' => 'widget_aksrt_buttons', ]); } + /** + * @param array $args + * @param array $instance + */ public function widget($args, $instance): void { $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; - $layout = sanitize_key((string) ($instance['layout'] ?? 'horizontal')); - if (!in_array($layout, ['vertical', 'horizontal'], true)) { - $layout = 'horizontal'; + // Also accept zibll-style `items`. + if (!$buttons && isset($instance['items']) && is_array($instance['items'])) { + $buttons = $instance['items']; } + $align = sanitize_key((string) ($instance['align'] ?? 'center')); if (!in_array($align, ['left', 'center', 'right'], true)) { $align = 'center'; @@ -30,31 +36,94 @@ class ButtonsWidget extends \WP_Widget { $radius = !empty($instance['radius']); $hollow = !empty($instance['hollow']); $go_link = !empty($instance['go_link']); - $gap = max(4, min(30, (int) ($instance['gap'] ?? 10))); + $pc_gap = max(0, min(40, (int) ($instance['pc_gap'] ?? $instance['gap'] ?? 12))); + $m_gap = max(0, min(30, (int) ($instance['m_gap'] ?? 6))); + $pc_pad = sanitize_key((string) ($instance['pc_padding'] ?? 'large')); + $m_pad = sanitize_key((string) ($instance['m_padding'] ?? 'medium')); + if (!in_array($pc_pad, ['small', 'medium', 'large'], true)) { + $pc_pad = 'large'; + } + if (!in_array($m_pad, ['small', 'medium', 'large'], true)) { + $m_pad = 'medium'; + } + $anim = sanitize_key((string) ($instance['obs_animation'] ?? '')); + $anim_repeat = !empty($instance['animation_repeat']); + $pc_width = max(0, min(400, (int) ($instance['pc_width'] ?? 0))); + $m_width = max(0, min(400, (int) ($instance['m_width'] ?? 0))); - $items = []; + $but_class = 'but'; + if ($radius) { + $but_class .= ' radius'; + } + if ($hollow) { + $but_class .= ' hollow'; + } + + $buts = ''; + $key = 0; foreach ($buttons as $btn) { if (!is_array($btn)) { continue; } $text = sanitize_text_field((string) ($btn['text'] ?? '')); - $url = esc_url_raw(trim((string) ($btn['url'] ?? ''))); if ($text === '') { continue; } + $url = ''; + $target = ''; + if (is_array($btn['link'] ?? null)) { + $url = esc_url_raw(trim((string) ($btn['link']['url'] ?? ''))); + $target = sanitize_text_field((string) ($btn['link']['target'] ?? '')); + } else { + $url = esc_url_raw(trim((string) ($btn['url'] ?? ''))); + $target = !empty($btn['new_window']) || !empty($btn['target']) ? '_blank' : ''; + } if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { $url = aksrt_go_link($url, true); } - $items[] = [ - 'text' => $text, - 'url' => $url, - 'style' => sanitize_key((string) ($btn['style'] ?? 'primary')), - 'icon' => sanitize_text_field((string) ($btn['icon'] ?? $btn['icon_left'] ?? '')), - 'icon_right' => sanitize_text_field((string) ($btn['icon_right'] ?? '')), - 'new_window' => !empty($btn['new_window']) || !empty($btn['target']), + if ($url === '') { + $url = 'javascript:void(0)'; + } + + $color = sanitize_html_class((string) ($btn['color'] ?? $btn['style'] ?? '')); + // Map legacy style keys to color classes. + $style_map = [ + 'primary' => 'c-blue', + 'secondary' => 'c-blue-2', + 'outline' => '', + 'text' => '', + 'pink' => 'c-red', + 'blue' => 'c-blue', + 'green' => 'c-green', + 'yellow' => 'c-yellow', + 'purple' => 'c-purple', ]; + if (isset($style_map[$color])) { + $color = $style_map[$color]; + } + $item_class = trim($but_class . ' ' . $color); + + $icon_left = sanitize_text_field((string) ($btn['icon'] ?? $btn['icon_left'] ?? '')); + $icon_right = sanitize_text_field((string) ($btn['icon_right'] ?? '')); + $left_html = $icon_left !== '' ? ' ' : ''; + $right_html = $icon_right !== '' ? ' ' : ''; + + $anim_pack = function_exists('aksrt_obs_animate_attrs') + ? aksrt_obs_animate_attrs($anim, $anim_repeat, $key * 0.15) + : ['class' => '', 'attr' => '']; + + $tgt = $target !== '' && strpos($url, 'javascript:') !== 0 + ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' + : ''; + + $buts .= '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $buts .= '' + . $left_html . esc_html($text) . $right_html . ''; + $buts .= '
'; + $key++; } - if (!$items) { + + if ($buts === '') { return; } @@ -64,36 +133,26 @@ class ButtonsWidget extends \WP_Widget { echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - $wrap = 'widget-buttons widget-buttons--' . $layout . ' align-' . $align; - if ($radius) { - $wrap .= ' is-radius'; + $style_attr = ''; + if ($pc_width > 0) { + $style_attr .= '--pc-width:' . $pc_width . 'px;'; } - if ($hollow) { - $wrap .= ' is-hollow'; + if ($m_width > 0) { + $style_attr .= '--m-width:' . $m_width . 'px;'; } - echo '
'; - foreach ($items as $btn) { - $style = in_array($btn['style'], ['primary', 'secondary', 'outline', 'text', 'pink', 'blue', 'green', 'yellow', 'purple'], true) - ? $btn['style'] - : 'primary'; - $cls = 'widget-buttons__btn widget-buttons__btn--' . $style; - $href = $btn['url'] !== '' ? $btn['url'] : 'javascript:void(0)'; - $tgt = $btn['new_window'] && $btn['url'] !== '' ? ' target="_blank" rel="noopener noreferrer"' : ''; - echo ''; - if ($btn['icon'] !== '') { - echo ' '; - } - echo esc_html($btn['text']); - if ($btn['icon_right'] !== '') { - echo ' '; - } - echo ''; - } - echo '
'; + $style_attr .= '--pc-gap:' . $pc_gap . 'px;--m-gap:' . $m_gap . 'px;--btn-gap:' . $pc_gap . 'px;'; + + echo '
'; + echo '
'; + echo $buts; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * @param array $instance + */ public function form($instance): void { $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; while (count($buttons) < 4) { @@ -106,12 +165,6 @@ class ButtonsWidget extends \WP_Widget {

-

+

+ +

$btn) : ?>
" value="" placeholder=""> @@ -132,7 +196,7 @@ class ButtonsWidget extends \WP_Widget { " value="" placeholder=""> " value="" placeholder=""> @@ -141,6 +205,11 @@ class ButtonsWidget extends \WP_Widget { $new_instance + * @param array $old_instance + * @return array + */ public function update($new_instance, $old_instance): array { $buttons = []; if (isset($new_instance['buttons']) && is_array($new_instance['buttons'])) { @@ -166,14 +235,17 @@ class ButtonsWidget extends \WP_Widget { } } return [ - 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), - 'layout' => ($new_instance['layout'] ?? '') === 'vertical' ? 'vertical' : 'horizontal', - 'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center', - 'radius' => !empty($new_instance['radius']) ? 1 : 0, - 'hollow' => !empty($new_instance['hollow']) ? 1 : 0, - 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, - 'gap' => max(4, min(30, (int) ($new_instance['gap'] ?? 10))), - 'buttons' => $buttons, + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center', + 'radius' => !empty($new_instance['radius']) ? 1 : 0, + 'hollow' => !empty($new_instance['hollow']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'gap' => max(4, min(30, (int) ($new_instance['gap'] ?? 12))), + 'pc_gap' => max(0, min(40, (int) ($new_instance['pc_gap'] ?? $new_instance['gap'] ?? 12))), + 'm_gap' => max(0, min(30, (int) ($new_instance['m_gap'] ?? 6))), + 'obs_animation' => sanitize_key((string) ($new_instance['obs_animation'] ?? '')), + 'animation_repeat'=> !empty($new_instance['animation_repeat']) ? 1 : 0, + 'buttons' => $buttons, ]; } } diff --git a/src/AKSRT/Widgets/WidgetTextTitle.php b/src/AKSRT/Widgets/WidgetTextTitle.php index 76a64bac..5b208df7 100644 --- a/src/AKSRT/Widgets/WidgetTextTitle.php +++ b/src/AKSRT/Widgets/WidgetTextTitle.php @@ -1,6 +1,6 @@ esc_html__('自定义标题 + 副标题区块,用于模块分隔。', 'aksrt'), + 'description' => esc_html__('自定义标题 + 副标题区块,支持逐字入场动画。', 'aksrt'), 'classname' => 'widget_aksrt_text_title', ] ); } + /** + * @param array $args + * @param array $instance + */ public function widget($args, $instance): void { $title = trim((string) ($instance['title'] ?? '')); $desc = trim((string) ($instance['desc'] ?? '')); @@ -39,6 +43,9 @@ class WidgetTextTitle extends \WP_Widget { $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'] ?? '')) ?: ''; + $anim = sanitize_key((string) ($instance['obs_animation'] ?? '')); + $anim_repeat = !empty($instance['animation_repeat']); + $icon_width = max(0, min(64, (int) ($instance['title_icon_width'] ?? 0))); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped @@ -50,26 +57,63 @@ class WidgetTextTitle extends \WP_Widget { $desc_color !== '' ? '--aksrt-tt-desc-color:' . $desc_color . ';' : '' ); - echo '
'; - if ($title !== '') { - echo '
'; - if ($icon_left !== '') { - echo ''; + $icon_w_attr = $icon_width > 0 ? ' style="--icon-width:' . (int) $icon_width . 'px;"' : ''; + $left_html = $icon_left !== '' + ? '' + : ''; + $right_html = $icon_right !== '' + ? '' + : ''; + + $title_html = ''; + if ($title !== '' && $anim !== '' && function_exists('aksrt_obs_animate_attrs')) { + // Split title into per-char spans with staggered delay (zibll). + $chars = preg_split('//u', $title, -1, PREG_SPLIT_NO_EMPTY) ?: []; + $pieces = []; + if ($left_html !== '') { + $pieces[] = $left_html; } - echo '' . esc_html($title) . ''; - if ($icon_right !== '') { - echo ''; + foreach ($chars as $ch) { + $pieces[] = $ch; } - echo '
'; + if ($right_html !== '') { + $pieces[] = $right_html; + } + foreach ($pieces as $i => $piece) { + $pack = aksrt_obs_animate_attrs($anim, $anim_repeat, $i * 0.05); + $is_html = str_starts_with((string) $piece, '<'); + $inner = $is_html ? $piece : esc_html((string) $piece); + $title_html .= '' . $inner . ''; + } + } elseif ($title !== '') { + $title_html = $left_html . esc_html($title) . $right_html; } + + $desc_html = ''; if ($desc !== '') { - echo '
' . esc_html($desc) . '
'; + if ($anim !== '' && function_exists('aksrt_obs_animate_attrs')) { + $pack = aksrt_obs_animate_attrs($anim, $anim_repeat, 0.6); + $desc_html = '
' . esc_html($desc) . '
'; + } else { + $desc_html = '
' . esc_html($desc) . '
'; + } } + + echo '
'; + if ($title_html !== '') { + echo '
' . $title_html . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo $desc_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
'; echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * @param array $instance + */ public function form($instance): void { ?>

@@ -109,6 +153,16 @@ class WidgetTextTitle extends \WP_Widget {

+

+ +

'; + if (function_exists('aksrt_render_yiyan_block')) { aksrt_render_yiyan_block([ - 'class' => 'aksrt-yiyan', + 'class' => 'aksrt-yiyan yiyan', 'type' => $type, 'count' => $count, 'interval' => $interval, 'icon' => $icon, ]); } else { - // Fallback if single-render helpers are not loaded. ?> -
'; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * @param array $instance + */ public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : '一言'; + $title = !empty($instance['title']) ? (string) $instance['title'] : '一言'; $count = !empty($instance['count']) ? (int) $instance['count'] : 5; $interval = !empty($instance['interval']) ? (int) $instance['interval'] : 5; $type = self::sanitize_type((string) ($instance['type'] ?? '')); - $icon = !empty($instance['icon']) ? $instance['icon'] : 'fas fa-quote-left'; + $icon = !empty($instance['icon']) ? (string) $instance['icon'] : 'fas fa-quote-left'; $labels = self::type_labels(); - // Curated subset for the form (full map still used for sanitization). $form_types = ['', 'a', 'd', 'i', 'k', 'h', 'j']; ?>

@@ -135,16 +155,37 @@ class WidgetYiyan extends \WP_Widget { name="get_field_name('icon')); ?>" type="text" value="">

+

+ +

+

+ +

$new_instance + * @param array $old_instance + * @return array + */ public function update($new_instance, $old_instance): array { - $instance = []; - $instance['title'] = sanitize_text_field($new_instance['title'] ?? ''); - $instance['count'] = max(1, min(10, absint($new_instance['count'] ?? 5))); - $instance['interval'] = max(3, min(30, absint($new_instance['interval'] ?? 5))); - $instance['type'] = self::sanitize_type((string) ($new_instance['type'] ?? '')); - $instance['icon'] = sanitize_text_field($new_instance['icon'] ?? 'fas fa-quote-left'); - return $instance; + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(10, absint($new_instance['count'] ?? 5))), + 'interval' => max(3, min(30, absint($new_instance['interval'] ?? 5))), + 'type' => self::sanitize_type((string) ($new_instance['type'] ?? '')), + 'icon' => sanitize_text_field((string) ($new_instance['icon'] ?? 'fas fa-quote-left')), + 'in_affix' => !empty($new_instance['in_affix']) ? 1 : 0, + 'obs_animation' => sanitize_key((string) ($new_instance['obs_animation'] ?? '')), + 'animation_repeat' => !empty($new_instance['animation_repeat']) ? 1 : 0, + ]; } }