From 411905f919489d7e0a23db9dffe27d5e14e7c3cd Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 26 Aug 2026 09:30:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E5=B0=8F=E5=B7=A5=E5=85=B7=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=BA=86=E4=B8=80=E4=BA=9B=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/widgets/frontend.css | 33 +++++++--- inc/helpers/aksrt-icon-cards.php | 13 ++-- src/WidgetSystem/FrontendRenderer.php | 2 +- .../Renderers/GeneralRenderer.php | 43 ++++++++----- src/WidgetSystem/WidgetSchemas.php | 61 +++++++++++++++---- .../widget-system/icon-card-contract.test.cjs | 5 +- .../zibll-modern-widgets-contract.test.cjs | 14 +++++ 7 files changed, 128 insertions(+), 43 deletions(-) diff --git a/assets/widgets/frontend.css b/assets/widgets/frontend.css index f640dabe..cd67ceb8 100644 --- a/assets/widgets/frontend.css +++ b/assets/widgets/frontend.css @@ -63,14 +63,31 @@ .aksrt-widget-quote blockquote { margin: 12px 0 0; font-size: 16px; line-height: 1.8; } .aksrt-widget-quote button { position: absolute; right: 13px; bottom: 13px; width: 30px; height: 30px; border: 0; border-radius: 50%; background: rgba(255,255,255,.16); color: #fff; cursor: pointer; } -.aksrt-widget-icon-card { min-height: 130px; max-width:280px; margin-right:auto; margin-left:auto; padding: 20px; border-radius: var(--aksrt-widget-radius); background: var(--main-bg-color, #fff); box-shadow: 0 0 10px var(--main-shadow,rgba(116,116,116,.08)); transition: box-shadow .2s; } -.aksrt-widget-icon-card:hover { box-shadow: 0 0 10px rgba(116,116,116,.14); } -.aksrt-widget-icon-card { display: flex; flex-direction: column; gap: 12px; text-align: center; } -.aksrt-widget-icon-cards--cover .aksrt-widget-icon-card { flex-direction: row; align-items: center; text-align: left; } -.aksrt-widget-icon-card > i { font-size: 34px; color: var(--focus-color, #ff5473); transition:transform .3s; } -.aksrt-widget-icon-card:hover > i { transform:scale(1.05); } -.aksrt-widget-icon-card span { display: flex; flex-direction: column; gap: 5px; } -.aksrt-widget-icon-card small { color: var(--muted-color, #8a8c98); } +/* Zibll icon-card contracts (vertical + horizontal variants). */ +.icon-card { max-width:280px; margin-left:auto; margin-right:auto; text-align:center; color:var(--main-color,#333); } +.icon-card .card-icon { display:inline-block; height:1.1em; transition:.3s; } +.icon-card .card-icon.toggle-radius { width:2.2em; height:2.2em; } +.icon-card .toggle-radius .fa { font-size:.9em; } +.icon-card:hover .card-icon { transform:scale(1.05); } +.icon-card .card-icon img, .icon-cover-icon img { width:1em; height:1em; object-fit:contain; vertical-align:middle; } +.icon-cover-card { min-width:0; color:var(--main-color,#333); } +.icon-cover-card .icon-cover-icon { display:inline-flex; align-items:center; justify-content:center; flex:none; } +.icon-cover-card .icon-cover-desc { min-width:0; } +.icon-cover-card .icon-cover-desc > div { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.icon-cover-card a, .icon-card a { color:inherit; text-decoration:none; } +.icon-cover-card .font-bold, .icon-card .font-bold { font-weight:700; } +.icon-cover-card .muted-color, .icon-card .muted-color { color:var(--muted-color,#8a8c98); } +.icon-cover-card .badg.cir { border-radius:50%; } +.icon-cover-card .badg.cir.radius4 { border-radius:4px; } +.zib-widget { border-radius:var(--main-radius,8px); background:var(--main-bg-color,#fff); box-shadow:0 0 10px var(--main-shadow,rgba(116,116,116,.08)); } +.zib-widget.padding-10 { padding:10px; } +.zib-widget.nobottom.notop { padding-top:0; padding-bottom:0; } +.row.gutters-5 { margin-left:-2.5px; margin-right:-2.5px; } +.row.gutters-5 > [class*="col-"] { padding-left:2.5px; padding-right:2.5px; } +@media (max-width:767px) { + .icon-card, .icon-cover-card { font-size:12px; } + .icon-cover-card { padding-top:8px; padding-bottom:8px; } +} .aksrt-widget-video { position: relative; overflow: hidden; border-radius: var(--aksrt-widget-radius); background: #000; aspect-ratio: 16 / 9; } .aksrt-widget-video--4-3 { aspect-ratio: 4 / 3; }.aksrt-widget-video--1-1 { aspect-ratio: 1; }.aksrt-widget-video--auto { aspect-ratio: auto; } diff --git a/inc/helpers/aksrt-icon-cards.php b/inc/helpers/aksrt-icon-cards.php index d019b4ab..38bfc368 100644 --- a/inc/helpers/aksrt-icon-cards.php +++ b/inc/helpers/aksrt-icon-cards.php @@ -73,6 +73,9 @@ if (!function_exists('aksrt_icon_card')) : $style = ''; $custom_color = sanitize_hex_color((string) $args['icon_custom_color']); + if (!$custom_color && preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/', (string) $args['icon_custom_color'])) { + $custom_color = (string) $args['icon_custom_color']; + } if (!$radius && $custom_color) { $style = ' style="color:' . esc_attr($custom_color) . ';"'; } @@ -100,9 +103,9 @@ if (!function_exists('aksrt_icon_card')) : $title = trim((string) $args['title']); $desc = trim((string) $args['desc']); if ($url !== '' && $title !== '') { - $title_html = '' . esc_html($title) . ''; + $title_html = '' . wp_kses_post($title) . ''; } else { - $title_html = $title !== '' ? esc_html($title) : ''; + $title_html = $title !== '' ? wp_kses_post($title) : ''; } $body = ''; @@ -110,7 +113,7 @@ if (!function_exists('aksrt_icon_card')) : $body .= '
' . $title_html . '
'; } if ($desc !== '') { - $body .= '
' . esc_html($desc) . '
'; + $body .= '
' . wp_kses_post($desc) . '
'; } $class = trim((string) $args['class'] . ' ' . (string) $args['type']); @@ -269,10 +272,10 @@ if (!function_exists('aksrt_icon_cover_card')) : ]; $text = ''; if (trim(wp_strip_all_tags($title)) !== '' || str_contains($title, '<')) { - $text .= '
' . wp_kses($title, $allow) . '
'; + $text .= '
' . wp_kses_post($title) . '
'; } if (trim(wp_strip_all_tags($desc)) !== '' || str_contains($desc, '<')) { - $text .= '
' . wp_kses($desc, $allow) . '
'; + $text .= '
' . wp_kses_post($desc) . '
'; } $html = ''; diff --git a/src/WidgetSystem/FrontendRenderer.php b/src/WidgetSystem/FrontendRenderer.php index d6a5981c..b4bdad08 100644 --- a/src/WidgetSystem/FrontendRenderer.php +++ b/src/WidgetSystem/FrontendRenderer.php @@ -23,7 +23,7 @@ final class FrontendRenderer { } AssetManager::enqueue_for($definition); - if (in_array($definition->slug(), ['tab_posts', 'slider', 'slider_media_combo'], true)) { + if (in_array($definition->slug(), ['tab_posts', 'slider', 'slider_media_combo', 'icon_cards', 'icon_cover_cards'], true)) { echo (string) ($args['before_widget'] ?? ''); echo self::reference_title($instance); echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/src/WidgetSystem/Renderers/GeneralRenderer.php b/src/WidgetSystem/Renderers/GeneralRenderer.php index f58b088f..b4591abd 100644 --- a/src/WidgetSystem/Renderers/GeneralRenderer.php +++ b/src/WidgetSystem/Renderers/GeneralRenderer.php @@ -34,41 +34,54 @@ final class GeneralRenderer { } private static function render_icon_cards(array $i): string { - return self::icon_cards((array) $i['cards'], false, (int) $i['pc_row'], (int) $i['m_row']); + return self::icon_cards($i, false); } private static function render_icon_cover_cards(array $i): string { - return self::icon_cards((array) $i['cards'], true, (int) $i['pc_row'], (int) $i['m_row']); + return self::icon_cards($i, true); } - private static function icon_cards(array $cards, bool $cover, int $pc, int $mobile): string { - $html = '
'; + private static function icon_cards(array $instance, bool $cover): string { + $pc = max(1, (int) ($instance['pc_row'] ?? 4)); + $mobile = max(1, (int) ($instance['m_row'] ?? 2)); + $cards = (array) ($instance['cards'] ?? []); + $is_row = count($cards) > 1; + $row_class = 'col-sm-' . max(1, (int) (12 / $pc)); + if ($mobile > 1) $row_class .= ' col-xs-' . max(1, (int) (12 / $mobile)); + $items = ''; foreach ($cards as $card) { if (!is_array($card)) { continue; } + $icon_size = !$cover && !empty($instance['size']) ? 16 + (int) $instance['size'] : ($cover ? 25 : 0); + $class = $cover ? (!empty($instance['show_widget_bg']) ? 'padding-10' : 'zib-widget mb10') : (!empty($instance['show_widget_bg']) ? 'box-body nopw-sm' : 'mb20'); $args = [ 'icon' => $card['icon'] ?? '', 'customize_icon' => $card['customize_icon'] ?? '', - 'custom_icon' => $card['custom_icon'] ?? '', - 'icon_image' => $card['icon_image'] ?? '', - 'icon_color' => $card['color'] ?? '', - 'icon_class' => $card['color'] ?? '', + 'icon_size' => $icon_size, + 'icon_radius' => !empty($card['icon_radius']) || ($cover && !empty($instance['icon_radius4'])), + 'icon_color' => $card['icon_color'] ?? '', + 'icon_custom_color' => $card['icon_custom_color'] ?? '', + 'icon_class' => $card['icon_class'] ?? '', 'title' => $card['title'] ?? '', - 'desc' => $card['description'] ?? '', + 'desc' => $card['desc'] ?? '', 'link' => $card['link'] ?? [], + 'class' => $class, ]; + $item = ''; if ($cover && function_exists('aksrt_icon_cover_card')) { - $html .= aksrt_icon_cover_card($args); + $item = aksrt_icon_cover_card($args); } elseif (!$cover && function_exists('aksrt_icon_card')) { - $html .= aksrt_icon_card($args); + $item = aksrt_icon_card($args); } else { - $link = RendererHelpers::link((array) ($card['link'] ?? [])); - $tag = $link['url'] ? 'a' : 'div'; - $html .= '<' . $tag . ' class="aksrt-widget-icon-card ' . esc_attr((string) ($card['color'] ?? '')) . '"' . ($link['url'] ? ' href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) : '') . '>' . esc_html((string) ($card['title'] ?? '')) . '' . esc_html((string) ($card['description'] ?? '')) . ''; + $item = ''; } + $items .= $is_row ? '
' . $item . '
' : $item; } - return $html . '
'; + if ($items === '') return ''; + $inner = $is_row ? '
' . $items . '
' : $items; + if ($cover) return '
' . $inner . '
'; + return '
' . (!empty($instance['show_widget_bg']) ? '
' : '') . $inner . (!empty($instance['show_widget_bg']) ? '
' : '') . '
'; } private static function render_video(array $i): string { diff --git a/src/WidgetSystem/WidgetSchemas.php b/src/WidgetSystem/WidgetSchemas.php index abbec296..274c5e38 100644 --- a/src/WidgetSystem/WidgetSchemas.php +++ b/src/WidgetSystem/WidgetSchemas.php @@ -114,12 +114,9 @@ final class WidgetSchemas { case 'combo_info': return [['id' => 'items', 'type' => 'group', 'label' => '信息卡片', 'max_items' => 30, 'fields' => array_merge(self::media_card_fields(), [['id' => 'tags', 'type' => 'text', 'label' => '标签', 'default' => '']])]]; case 'icon_cards': + return self::icon_cards_fields(); case 'icon_cover_cards': - return [ - ['id' => 'pc_row', 'type' => 'spinner', 'label' => 'PC 单行数量', 'default' => 4, 'min' => 1, 'max' => 12], - ['id' => 'm_row', 'type' => 'spinner', 'label' => '移动端单行数量', 'default' => 2, 'min' => 1, 'max' => 6], - ['id' => 'cards', 'type' => 'group', 'label' => '卡片', 'max_items' => 40, 'fields' => self::icon_card_fields()], - ]; + return self::icon_cover_cards_fields(); case 'media_cards': default: return [['id' => 'cards', 'type' => 'group', 'label' => '卡片', 'max_items' => 40, 'fields' => self::media_card_fields()]]; @@ -340,14 +337,54 @@ final class WidgetSchemas { return $fields; } - private static function icon_card_fields(): array { + private static function icon_cards_fields(): array { return [ - ['id' => 'title', 'type' => 'text', 'label' => '标题', 'default' => ''], - ['id' => 'description', 'type' => 'textarea', 'label' => '说明', 'default' => ''], - ['id' => 'icon', 'type' => 'icon', 'label' => '图标', 'default' => 'fa fas fa-star'], - ['id' => 'custom_icon', 'type' => 'upload', 'label' => '自定义图标', 'default' => ''], - ['id' => 'color', 'type' => 'palette', 'label' => '颜色', 'default' => 'jb-blue', 'options' => CommonFields::palette()], - ['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []], + ['id' => 'show_widget_bg', 'type' => 'switcher', 'label' => '显示模块背景', 'default' => false], + ['id' => 'pc_row', 'type' => 'button_set', 'label' => 'PC端单行排列数量', 'default' => 2, 'options' => [1 => '1个', 2 => '2个', 3 => '3个', 4 => '4个', 6 => '6个', 12 => '12个']], + ['id' => 'm_row', 'type' => 'button_set', 'label' => '移动端单行排列数量', 'default' => 2, 'options' => [1 => '1个', 2 => '2个', 3 => '3个', 4 => '4个', 6 => '6个', 12 => '12个']], + ['id' => 'size', 'type' => 'slider', 'label' => '图标尺寸微调', 'default' => 0, 'min' => -10, 'max' => 10, 'step' => 1], + ['id' => 'cards', 'type' => 'group', 'label' => '添加图标', 'max_items' => 40, 'default' => [], 'fields' => [ + ['id' => 'icon', 'type' => 'icon', 'label' => '选择图标', 'default' => 'fa fa-magic'], + ['id' => 'customize_icon', 'type' => 'textarea', 'label' => '自定义图标', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]], + ['id' => 'link', 'type' => 'link', 'label' => '跳转链接', 'default' => []], + ['id' => 'icon_radius', 'type' => 'switcher', 'label' => '显示图标背景', 'default' => false], + ['id' => 'icon_class', 'type' => 'palette', 'label' => '图标样式', 'default' => 'c-yellow', 'options' => self::icon_background_palette(), 'dependency' => ['icon_radius', '!=', '']], + ['id' => 'icon_color', 'type' => 'palette', 'label' => '图标颜色', 'default' => 'key-color', 'options' => self::icon_text_palette(), 'dependency' => ['icon_radius|icon_custom_color', '==|==', '|']], + ['id' => 'icon_custom_color', 'type' => 'color', 'label' => '自定义图标颜色', 'default' => '', 'dependency' => ['icon_radius', '==', '']], + ['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]], + ['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 2]], + ]], + ]; + } + + private static function icon_cover_cards_fields(): array { + return [ + ['id' => 'pc_row', 'type' => 'button_set', 'label' => 'PC端单行排列数量', 'default' => 4, 'options' => [1 => '1个', 2 => '2个', 3 => '3个', 4 => '4个', 6 => '6个']], + ['id' => 'm_row', 'type' => 'button_set', 'label' => '移动端单行排列数量', 'default' => 2, 'options' => [1 => '1个', 2 => '2个']], + ['id' => 'show_widget_bg', 'type' => 'switcher', 'label' => '显示模块背景', 'default' => false], + ['id' => 'icon_radius4', 'type' => 'switcher', 'label' => '方形图标', 'default' => false], + ['id' => 'cards', 'type' => 'group', 'label' => '添加卡片', 'max_items' => 40, 'default' => [], 'fields' => [ + ['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]], + ['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 2]], + ['id' => 'icon', 'type' => 'icon', 'label' => '选择图标', 'default' => 'fa fa-magic'], + ['id' => 'customize_icon', 'type' => 'textarea', 'label' => '自定义图标', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]], + ['id' => 'icon_class', 'type' => 'palette', 'label' => '图标颜色', 'default' => 'c-yellow', 'options' => self::icon_background_palette()], + ['id' => 'link', 'type' => 'link', 'label' => '跳转链接', 'default' => []], + ]], + ]; + } + + private static function icon_background_palette(): array { + return [ + 'transparent' => '透明', 'c-red' => '红色', 'c-red-2' => '深红', 'c-yellow' => '黄色', 'c-yellow-2' => '深黄', 'c-cyan' => '青色', 'c-blue' => '蓝色', 'c-blue-2' => '深蓝', 'c-green' => '绿色', 'c-green-2' => '深绿', 'c-purple' => '紫色', 'c-purple-2' => '深紫', + 'b-red' => '红色背景', 'b-yellow' => '黄色背景', 'b-cyan' => '青色背景', 'b-blue' => '蓝色背景', 'b-green' => '绿色背景', 'b-purple' => '紫色背景', 'b-black' => '黑色背景', + 'jb-red' => '渐变红', 'jb-pink' => '渐变粉', 'jb-yellow' => '渐变黄', 'jb-cyan' => '渐变青', 'jb-blue' => '渐变蓝', 'jb-green' => '渐变绿', 'jb-purple' => '渐变紫', 'jb-vip1' => '会员金', 'jb-vip2' => '会员黑', + ]; + } + + private static function icon_text_palette(): array { + return [ + 'key-color' => '主要文字', 'c-red' => '红色', 'c-red-2' => '深红', 'c-yellow' => '黄色', 'c-yellow-2' => '深黄', 'c-cyan' => '青色', 'c-blue' => '蓝色', 'c-blue-2' => '深蓝', 'c-green' => '绿色', 'c-green-2' => '深绿', 'c-purple' => '紫色', 'c-purple-2' => '深紫', ]; } diff --git a/tests/widget-system/icon-card-contract.test.cjs b/tests/widget-system/icon-card-contract.test.cjs index 868ef493..2ac5a42c 100644 --- a/tests/widget-system/icon-card-contract.test.cjs +++ b/tests/widget-system/icon-card-contract.test.cjs @@ -19,8 +19,9 @@ test('icon card renderers keep vertical and horizontal legacy DOM contracts sepa assert.match(helper, /icon-cover-desc ml10 flex1 px12-sm/); assert.doesNotMatch(helper, /aksrt-icon-cover-card/); assert.match(widgets, /'customize_icon' => \$card\['customize_icon'\] \?\? ''/); - assert.match(widgets, /'custom_icon'\s+=> \$card\['custom_icon'\] \?\? ''/); - assert.match(widgets, /'icon_image'\s+=> \$card\['icon_image'\] \?\? ''/); + assert.doesNotMatch(widgets, /\$card\['custom_icon'\]/); + assert.doesNotMatch(widgets, /\$card\['icon_image'\]/); + assert.doesNotMatch(widgets, /\$card\['description'\]/); assert.match(header, /empty\(\$card\['custom_icon'\]\)/); }); diff --git a/tests/widget-system/zibll-modern-widgets-contract.test.cjs b/tests/widget-system/zibll-modern-widgets-contract.test.cjs index 647672a2..6266588d 100644 --- a/tests/widget-system/zibll-modern-widgets-contract.test.cjs +++ b/tests/widget-system/zibll-modern-widgets-contract.test.cjs @@ -28,6 +28,20 @@ test('reference slider CSS includes responsive height and combo selectors', () = for (const token of ['.new-swiper.slide-widget.scale-height', '--scale-height', '--pc-height', '--m-height', '.widget-slider-graphic', '.m-hide_slide', '.m-hide_cover', '--m-cover-ratio']) assert.match(css, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); }); +test('icon card widgets expose Zibll vertical and horizontal contracts', () => { + const schemas = read('src/WidgetSystem/WidgetSchemas.php'); + const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php'); + const helper = read('inc/helpers/aksrt-icon-cards.php'); + for (const token of ['show_widget_bg', "'id' => 'size'", 'icon_radius', 'icon_custom_color', "'id' => 'icon_radius4'", 'icon_background_palette', 'icon_text_palette']) { + assert.match(schemas, new RegExp(token.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'))); + } + for (const token of ['clearfix', 'row gutters-5', 'icon-card', 'icon-cover-card', 'icon-cover-icon', 'icon-cover-desc']) { + assert.match(renderer + helper, new RegExp(token.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'))); + } + assert.match(helper, /wp_kses_post\(\$title\)/); + assert.match(helper, /wp_kses_post\(\$desc\)/); +}); + test('admin core supports composite dependencies and nested repeater renumbering', () => { const core = require(path.join(root, 'assets/widgets/admin-core.js')); assert.equal(core.dependencyVisible([['direction', '!=', 'vertical'], ['scale_height', '!=', '']], { direction: 'horizontal', scale_height: '1' }), true);