diff --git a/assets/widgets/admin.js b/assets/widgets/admin.js index 6fee714b..d18eca4e 100644 --- a/assets/widgets/admin.js +++ b/assets/widgets/admin.js @@ -165,6 +165,7 @@ frame.on('select', () => { const attachment = frame.state().get('selection').first().toJSON(); $wrap.find('input[type="url"]').val(attachment.url).trigger('change'); + if ($wrap.data('media-library') === 'video') return; let $image = $wrap.find('img'); if (!$image.length) $image = $('').prependTo($wrap); $image.attr('src', attachment.url); diff --git a/src/WidgetSystem/FormRenderer.php b/src/WidgetSystem/FormRenderer.php index 828a4fae..1a0ac2d3 100644 --- a/src/WidgetSystem/FormRenderer.php +++ b/src/WidgetSystem/FormRenderer.php @@ -193,7 +193,8 @@ final class FormRenderer { private static function upload(string $id, string $name, string $value, string $library = ''): void { $library = in_array($library, ['image', 'video'], true) ? $library : ''; echo '
'; - if ($value !== '') { + // 参考主题:视频上传字段不展示预览图(视频 URL 作为 img src 会得到破图)。 + if ($value !== '' && $library !== 'video') { echo ''; } echo ''; @@ -240,19 +241,20 @@ final class FormRenderer { private static function group(SchemaWidget $widget, array $field, array $rows, string $key): void { $token = '__index_' . substr(md5($key), 0, 8) . '__'; + $button_title = (string) ($field['button_title'] ?? __('添加项目', 'aksrt')); echo '
'; foreach ($rows as $index => $row) { self::group_row($widget, $field, is_array($row) ? $row : [], $key, (string) $index); } echo '
'; + echo '
'; } private static function group_row(SchemaWidget $widget, array $field, array $row, string $key, string $index): void { $prefix = (string) ($field['item_label'] ?? $field['label'] ?? __('项目', 'aksrt')); $number = ctype_digit($index) ? ((int) $index + 1) : 0; - $title = $prefix . ($number ? ' ' . $number : ''); + $title = $prefix . $number; if (!empty($row['title'])) $title .= ':' . strip_tags((string) $row['title']); echo '
' . esc_html($title) . '
'; foreach ((array) ($field['fields'] ?? []) as $child) { diff --git a/src/WidgetSystem/Renderers/SliderRenderer.php b/src/WidgetSystem/Renderers/SliderRenderer.php index 54d437a6..e5d8b1b4 100644 --- a/src/WidgetSystem/Renderers/SliderRenderer.php +++ b/src/WidgetSystem/Renderers/SliderRenderer.php @@ -10,101 +10,179 @@ defined('ABSPATH') || exit; final class SliderRenderer { public static function render(string $slug, array $instance, array $context = []): string { unset($context); - return $slug === 'slider_media_combo' ? self::combo($instance) : self::slider((array) ($instance['slides'] ?? []), (array) ($instance['option'] ?? []), true); + if ($slug === 'slider_media_combo') { + return self::combo($instance); + } + $slides = (array) ($instance['slides'] ?? []); + if (!$slides) { + // 参考主题幻灯片(新):无可显示内容时仅向超级管理员提示。 + if (function_exists('is_super_admin') && is_super_admin()) { + return '
[' . esc_html__('幻灯片(新)', 'aksrt') . ']' . esc_html__('幻灯片模块:', 'aksrt') . '' . esc_html__('当前配置下没有可显示的内容,请正确配置或移出该模块', 'aksrt') . '
'; + } + return ''; + } + return self::slider($slides, (array) ($instance['option'] ?? []), true); } private static function slider(array $slides, array $option, bool $with_margin = false): string { $option = array_merge(['direction' => 'horizontal', 'loop' => true, 'button' => true, 'pagination' => true, 'effect' => 'slide', 'scale_height' => true, 'scale' => 35, 'auto_height' => false, 'max_height' => 500, 'min_height' => 180, 'pc_height' => 400, 'm_height' => 200, 'spacebetween' => 15, 'speed' => 0, 'autoplay' => true, 'interval' => 4], $option); $is_mobile = function_exists('wp_is_mobile') && wp_is_mobile(); + $lazy = aksrt_is_lazy('lazy_sider'); + $lazy_src = aksrt_get_lazy_thumb('lg'); $items = ''; $count = 0; foreach ($slides as $slide) { if (!is_array($slide)) continue; $hide = (string) ($slide['hide'] ?? ''); if (($is_mobile && $hide === 'm') || (!$is_mobile && $hide === 'pc')) continue; - $background = esc_url((string) ($slide['background'] ?? '')); $video = esc_url((string) ($slide['background_video'] ?? '')); - if ($background === '' && ($video === '' || $is_mobile)) continue; - $alt = (string) (($slide['text']['title'] ?? '') ?: __('幻灯片', 'aksrt')); - $slide_class = ''; - if ($video !== '' && !$is_mobile) { $slide_class = ' slide-background-video'; $media = '
'; } - else $media = '' . esc_attr($alt) . ''; + $slide_class = (string) ($slide['class'] ?? ''); + $background = (string) ($slide['background'] ?? ''); + $video = (string) ($slide['background_video'] ?? ''); + $media = ''; + if ($video !== '' && !$is_mobile) { + $slide_class .= ' slide-background-video'; + $poster = $background !== '' ? ' poster="' . esc_url($background) . '"' : ''; + $media = '
'; + } + if ($media === '' && $background !== '') { + $alt = (string) (($slide['text']['title'] ?? '') ?: ($slide['link']['text'] ?? '') ?: __('幻灯片', 'aksrt')); + $media = '' . esc_attr($alt) . ''; + } + if ($media === '') continue; $layers = ''; foreach ((array) ($slide['image_layer'] ?? []) as $layer) { if (!is_array($layer) || empty($layer['image'])) continue; - $attrs = ''; $parallax = (int) ($layer['parallax'] ?? 0); $scale = (float) ($layer['parallax_scale'] ?? 100) / 100; $opacity = (float) ($layer['parallax_opacity'] ?? 100) / 100; - if ($parallax) $attrs .= ' data-swiper-parallax="' . $parallax . '%"'; if ($scale !== 1.0) $attrs .= ' data-swiper-parallax-scale="' . esc_attr((string) $scale) . '"'; if ($opacity !== 1.0) $attrs .= ' data-swiper-parallax-opacity="' . esc_attr((string) $opacity) . '"'; + $parallax = (int) ($layer['parallax'] ?? 0); + $parallax_attr = $parallax ? ' data-swiper-parallax="' . $parallax . '%"' : ''; + $opacity = (float) ($layer['parallax_opacity'] ?? 100) / 100; + $parallax_attr .= ($parallax && $opacity && $opacity !== 1.0) ? ' data-swiper-parallax-opacity="' . esc_attr((string) $opacity) . '"' : ''; + $scale = (float) ($layer['parallax_scale'] ?? 100) / 100; + $parallax_attr .= ($scale && $scale !== 1.0) ? ' data-swiper-parallax-scale="' . esc_attr((string) $scale) . '"' : ''; $class = !empty($layer['free_size']) ? ' slide-layer text-' . esc_attr((string) ($layer['align'] ?? 'center')) : ''; - $layers .= '
' . esc_attr($alt) . '
'; + $layers .= '
' . esc_attr((string) (($slide['text']['title'] ?? '') ?: __('幻灯片', 'aksrt'))) . '
'; } $text = (array) ($slide['text'] ?? []); $copy = ''; if (!empty($text['title'])) { $style = ''; if (!empty($text['text_size_pc'])) $style .= '--text-size-pc:' . (int) $text['text_size_pc'] . 'px;'; if (!empty($text['text_size_m'])) $style .= '--text-size-m:' . (int) $text['text_size_m'] . 'px;'; $copy = '
' . wp_kses_post((string) $text['title']) . '
' . (!empty($text['desc']) ? '
' . wp_kses_post((string) $text['desc']) . '
' : '') . '
'; if (!empty($text['parallax'])) $copy = '
' . $copy . '
'; } - $body = $media . $layers . $copy; $link = RendererHelpers::link((array) ($slide['link'] ?? [])); if ($link['url'] !== '') $body = '' . $body . ''; - $items .= '
' . $body . '
'; $count++; + $body = $media . $layers . $copy; + $link = RendererHelpers::link((array) ($slide['link'] ?? [])); + $body = $link['url'] !== '' + ? '' . $body . '' + : '' . $body . ''; + $items .= '
' . $body . '
'; $count++; } - if (!$count) return RendererHelpers::empty_state(__('请配置幻灯片', 'aksrt')); - $class = 'new-swiper slide-widget' . ($with_margin ? ' mb20' : '') . ($option['scale_height'] && $option['direction'] !== 'vertical' ? ' scale-height' : '') . ($count === 1 ? ' swiper-container-initialized' : ''); $attrs = ' data-direction="' . esc_attr((string) $option['direction']) . '" data-effect="' . esc_attr((string) $option['effect']) . '"'; - if (!empty($option['loop']) && $count > 1) $attrs .= ' data-loop="true"'; if (!empty($option['autoplay']) && $count > 1) $attrs .= ' data-autoplay="1" data-interval="' . ((int) $option['interval'] * 1000) . '"'; if ((int) $option['speed'] > 0) $attrs .= ' data-speed="' . (int) $option['speed'] . '"'; $attrs .= ' data-spaceBetween="' . (int) $option['spacebetween'] . '"'; - if ($option['auto_height']) $attrs .= ' auto-height="true" style="--max-height:' . (int) $option['max_height'] . 'px;--min-height:' . (int) $option['min_height'] . 'px;"'; else $attrs .= ' style="' . esc_attr($option['scale_height'] && $option['direction'] !== 'vertical' ? '--scale-height:' . (int) $option['scale'] . '%;' : '--pc-height:' . (int) $option['pc_height'] . 'px;--m-height:' . (int) $option['m_height'] . 'px;') . '"'; - $html = '
' . $items . '
'; if (!empty($option['button']) && $count > 1) $html .= '
'; if (!empty($option['pagination']) && $count > 1) $html .= '
'; return '
' . $html . '
'; + if (!$count) return ''; + $class = 'new-swiper slide-widget' . ($with_margin ? ' mb20' : '') . ($option['scale_height'] && $option['direction'] !== 'vertical' ? ' scale-height' : '') . ($count === 1 ? ' swiper-container-initialized' : ''); + $attrs = ' data-direction="' . esc_attr((string) $option['direction']) . '" data-effect="' . esc_attr((string) $option['effect']) . '"'; + if (!empty($option['loop'])) $attrs .= ' data-loop="true"'; + $interval = (int) $option['interval']; + if ($interval < 999) $interval *= 1000; + if (!empty($option['autoplay'])) $attrs .= ' data-autoplay="1" data-interval="' . $interval . '"'; + if ((int) $option['speed'] > 299) $attrs .= ' data-speed="' . (int) $option['speed'] . '"'; + $attrs .= ' data-spaceBetween="' . (int) $option['spacebetween'] . '"'; + if ($option['auto_height'] && $option['direction'] !== 'vertical') { + $attrs .= ' auto-height="true" style="--max-height:' . (int) $option['max_height'] . 'px;--min-height:' . (int) $option['min_height'] . 'px;"'; + } elseif ($option['scale_height'] && $option['direction'] !== 'vertical') { + $attrs .= ' style="--scale-height:' . (int) $option['scale'] . '%;"'; + } else { + $attrs .= ' style="--pc-height:' . (int) $option['pc_height'] . 'px;--m-height:' . (int) $option['m_height'] . 'px;"'; + } + $html = '
' . $items . '
'; + if (!empty($option['button']) && $count > 1) $html .= '
'; + if (!empty($option['pagination']) && $count > 1) $html .= '
'; + return '
' . $html . '
'; } private static function combo(array $instance): string { - $option = (array) ($instance['option'] ?? []); $option['direction'] = 'horizontal'; $option['scale_height'] = true; $option['scale'] = (int) ($instance['slide_scale'] ?? 50); - $slide_list = (array) ($instance['slides'] ?? []); - $slider = !empty($slide_list[0]) ? self::slider($slide_list, $option) : ''; + $has_slider = !empty($instance['slides'][0]); + $has_covers = !empty($instance['covers'][0]['image']) || !empty($instance['covers'][0]['video']); + // 参考主题 is_show 门控:幻灯片需含背景媒体才视为有内容。 + $show_slider = $has_slider && (!empty($instance['slides'][0]['background']) || !empty($instance['slides'][0]['background_video'])); + if (!$show_slider && !$has_covers) return ''; + + $slide_scale = max(0, (int) ($instance['slide_scale'] ?? 50)); + $slider = ''; + if ($has_slider && is_array($instance['option'])) { + $option = $instance['option']; + $option['class'] = 'slide-widget'; + $option['direction'] = 'horizontal'; + $option['scale_height'] = true; + $option['scale'] = $slide_scale; + $slider = self::slider((array) $instance['slides'], $option); + } + + $m_layout_type = (string) ($instance['m_layout_type'] ?? ''); + $layout_ratio = max(0, (int) ($instance['layout_ratio'] ?? 60)); + $cover_row_count = max(1, (int) ($instance['pc_row'] ?? 2)); + $cover_col_count = max(1, (int) ($instance['pc_col'] ?? 2)); + $cover_gap = 3; + + // 右侧卡片整体长宽比由左侧幻灯片宽度占比与长宽比推导,gap 与参考主题一致。 + $cover_base_ratio = $layout_ratio < 100 ? (($layout_ratio * $slide_scale) / (100 - $layout_ratio)) : 0; + $cover_ratio = (($cover_base_ratio - ($cover_col_count - 1) * $cover_gap) / $cover_col_count) * $cover_row_count; + + $cover_m_row = max(1, (int) ($instance['m_row'] ?? 2)); + $m_cover_ratio = (int) ($instance['m_cover_ratio'] ?? 45); + if ($m_cover_ratio < 0) $m_cover_ratio = (int) $cover_ratio; + if ($m_layout_type === 'default') $cover_m_row = $cover_row_count; + + $style_attr = ' --layout-ratio:' . $layout_ratio . '%; --cover-ratio:' . $cover_ratio . '%; --m-cover-ratio:' . $m_cover_ratio . '%;'; + $is_mobile = function_exists('wp_is_mobile') && wp_is_mobile(); - $cover_list = array_values((array) ($instance['covers'] ?? [])); + $cover_list = (array) ($instance['covers'] ?? []); $is_row = count($cover_list) > 1; - $layout_type = (string) ($instance['m_layout_type'] ?? 'vertical'); - $pc_row = max(1, min(4, (int) ($instance['pc_row'] ?? 2))); - $m_row = $layout_type === 'default' ? $pc_row : max(1, min(6, (int) ($instance['m_row'] ?? 2))); - $row_class = 'col-sm-' . (int) (12 / $pc_row) . ($m_row > 1 ? ' col-xs-' . (int) (12 / $m_row) : ''); - $m_cover_ratio = max(0, (int) ($instance['m_cover_ratio'] ?? 45)); - $mask = max(0, min(90, (int) ($instance['mask_opacity'] ?? 0))) / 100; - $cover_style_vars = '--font-size:' . (int) ($instance['font_size_pc'] ?? 16) . 'px;--font-size-sm:' . (int) ($instance['font_size_m'] ?? 14) . 'px;'; - if (!empty($instance['font_bold'])) $cover_style_vars .= '--font-weight:bold;--font-weight-sm:bold;'; + $row_class = 'col-sm-' . (int) (12 / $cover_row_count); + $row_class .= $cover_m_row > 1 ? ' col-xs-' . (int) (12 / $cover_m_row) : ''; + $animation_class = self::animation_class($instance); + + $font_size_pc = (int) ($instance['font_size_pc'] ?? 16); + $font_size_m = (int) ($instance['font_size_m'] ?? 14); $font_color = (string) ($instance['font_color'] ?? ''); - if ($font_color !== '') $cover_style_vars .= '--color:' . $font_color . ';--color-sm:' . $font_color . ';'; + $font_style = ''; + $font_style .= $font_size_pc && 14 !== $font_size_pc ? '--font-size:' . $font_size_pc . 'px;' : ''; + $font_style .= $font_size_m && 14 !== $font_size_m ? '--font-size-sm:' . $font_size_m . 'px;' : ''; + $font_style .= !empty($instance['font_bold']) ? '--font-weight:bold;--font-weight-sm:bold;' : ''; + $font_style .= $font_color !== '' ? '--color:' . $font_color . ';--color-sm:' . $font_color . ';' : ''; + $font_style_attr = $font_style !== '' ? ' style="' . esc_attr($font_style) . '"' : ''; + $covers = ''; foreach ($cover_list as $cover) { if (!is_array($cover)) continue; $hide = (string) ($cover['hide'] ?? ''); if (($is_mobile && $hide === 'm') || (!$is_mobile && $hide === 'pc')) continue; - $image = esc_url((string) ($cover['image'] ?? '')); - $video = esc_url((string) ($cover['video'] ?? '')); $title = (string) ($cover['title'] ?? ''); $link = RendererHelpers::link((array) ($cover['link'] ?? [])); $card = aksrt_graphic_card([ 'class' => 'noshadow mb10', - 'img' => $image, - 'video' => $video, + 'img' => (string) ($cover['image'] ?? ''), + 'video' => (string) ($cover['video'] ?? ''), 'alt' => wp_strip_all_tags($title), 'link' => ['url' => $link['url'], 'target' => $link['target']], + 'lazy' => aksrt_is_lazy('lazy_cover'), 'more' => trim($title) !== '' ? '
' . wp_kses_post($title) . '
' : '', 'height_scale' => 0, 'mask_opacity' => (int) ($instance['mask_opacity'] ?? 0), ]); - if ($card === '') continue; - $covers .= '
' . $card . '
'; + $col_class = trim($animation_class . ($is_row ? ' ' . $row_class : '')); + $covers .= '
' . $card . '
'; } - if ($covers !== '') { - $covers = '
' . ($is_row ? '
' : '') . $covers . ($is_row ? '
' : '') . '
'; + $cover_html = '
' . ($is_row ? '
' : '') . $covers . ($is_row ? '
' : '') . '
'; + + $class = 'flex widget-slider-graphic m-' . sanitize_html_class($m_layout_type); + $html = '
'; + if ($slider !== '') { + $html .= '
' . $slider . '
'; + } elseif (function_exists('is_super_admin') && is_super_admin()) { + $html .= '
' . esc_html__('当前模块下没有可显示的幻灯片,请添加幻灯片或移除此模块', 'aksrt') . '
'; } - if ($slider === '' && $covers === '') return RendererHelpers::empty_state(__('请配置幻灯片或卡片', 'aksrt')); - $layout = max(0, min(100, (int) ($instance['layout_ratio'] ?? 60))); - $slide_scale = max(0, (int) ($instance['slide_scale'] ?? 50)); - $rows = $pc_row; - $columns = max(1, (int) ($instance['pc_col'] ?? 2)); - $base = $layout < 100 ? (($layout * $slide_scale) / (100 - $layout)) : 0; - $cover_ratio = $columns > 0 ? ((($base - (($columns - 1) * 3)) / $columns) * $rows) : 0; - $style = '--layout-ratio:' . $layout . '%;--cover-ratio:' . $cover_ratio . '%;--m-cover-ratio:' . $m_cover_ratio . '%;'; - $animation = ''; - if (!empty($instance['obs_animation'])) { - $animation = ' obs-animate ani-' . sanitize_html_class((string) $instance['obs_animation']) . (!empty($instance['animation_repeat']) ? ' obs-animate-repeat' : ''); - } - $class = 'flex widget-slider-graphic m-' . sanitize_html_class($layout_type); - $html = '
'; - if ($slider !== '') $html .= '
' . $slider . '
'; - if ($covers !== '') $html .= '
' . $covers . '
'; + $html .= '
' . $cover_html . '
'; return $html . '
'; } + + /** 参考主题 obs_animation 入场动画类(作用于 slider-col 与每个卡片列)。 */ + private static function animation_class(array $instance): string { + $animation = (string) ($instance['obs_animation'] ?? ''); + if ($animation === '') return ''; + $class = ' obs-animate ani-' . sanitize_html_class($animation); + if (!empty($instance['animation_repeat'])) $class .= ' obs-animate-repeat'; + return $class; + } } diff --git a/src/WidgetSystem/WidgetSchemas.php b/src/WidgetSystem/WidgetSchemas.php index cd7cccdd..a6b77801 100644 --- a/src/WidgetSystem/WidgetSchemas.php +++ b/src/WidgetSystem/WidgetSchemas.php @@ -396,25 +396,25 @@ final class WidgetSchemas { $asset_uri = function_exists('get_template_directory_uri') ? get_template_directory_uri() . '/assets/img/' : ''; $align_uri = function_exists('get_template_directory_uri') ? get_template_directory_uri() . '/assets/images/slider-align/' : ''; $slide_fields = [ - ['id' => 'background', 'type' => 'upload', 'label' => '图片背景', 'default' => ''], - ['id' => 'background_video', 'type' => 'upload', 'label' => '视频背景', 'default' => '', 'description' => '图片背景、视频背景至少二选一;PC 端视频优先,移动端只显示图片。'], + ['id' => 'background', 'type' => 'upload', 'label' => '图片背景', 'default' => '', 'library' => 'image'], + ['id' => 'background_video', 'type' => 'upload', 'label' => '视频背景', 'default' => '', 'library' => 'video', 'description' => '(必填)图片背景、视频背景至少二选一,如果同时设置则PC端视频优先(PC端视频加载失败则显示图片)
如果需要在移动端显示,则必须设置图片背景,移动端只显示图片
'], ['id' => 'hide', 'type' => 'radio', 'label' => '显示规则', 'default' => '', 'options' => ['' => '全部显示', 'pc' => 'PC端不显示', 'm' => '移动端不显示']], ['id' => 'link', 'type' => 'link', 'label' => '跳转链接', 'default' => []], - ['id' => 'image_layer', 'type' => 'group', 'label' => '叠加图层', 'max_items' => 12, 'fields' => [ - ['id' => 'image', 'type' => 'upload', 'label' => '图层图片', 'default' => ''], - ['id' => 'free_size', 'type' => 'switcher', 'label' => '自由尺寸', 'default' => false], + ['id' => 'image_layer', 'type' => 'group', 'label' => '叠加图层', 'item_label' => '图层', 'button_title' => '添加图层', 'max_items' => 12, 'description' => '添加额外的幻灯片图层,配合图层设置及幻灯片其它设置,可轻松制作出漂亮无比的幻灯片', 'fields' => [ + ['id' => 'image', 'type' => 'upload', 'label' => '图层图片', 'default' => '', 'library' => 'image'], + ['id' => 'free_size', 'type' => 'switcher', 'label' => '自由尺寸', 'default' => false, 'description' => '如果图层的尺寸和背景图的尺寸不一致,可开启此项以自定义图层对齐方向'], ['id' => 'align', 'type' => 'radio', 'label' => '图层对齐', 'default' => 'center', 'options' => ['left' => '靠左显示', 'center' => '居中显示', 'right' => '靠右显示'], 'dependency' => ['image|free_size', '!=|!=', '|']], - ['id' => 'parallax', 'type' => 'spinner', 'label' => '视差滚动', 'default' => 0, 'min' => -200, 'max' => 200, 'step' => 5, 'dependency' => ['image', '!=', '']], - ['id' => 'parallax_scale', 'type' => 'spinner', 'label' => '视差缩放', 'default' => 100, 'min' => 1, 'max' => 200, 'step' => 5, 'dependency' => ['image', '!=', '']], - ['id' => 'parallax_opacity', 'type' => 'spinner', 'label' => '视差透明', 'default' => 100, 'min' => 1, 'max' => 100, 'step' => 5, 'dependency' => ['image|parallax', '!=|!=', '|']], + ['id' => 'parallax', 'type' => 'spinner', 'label' => '视差滚动', 'default' => 0, 'min' => -200, 'max' => 200, 'step' => 5, 'unit' => '%', 'description' => '提前或延后进入视线,负值为延后,正值为提前,0为关闭[-200~200]', 'dependency' => ['image', '!=', '']], + ['id' => 'parallax_scale', 'type' => 'spinner', 'label' => '视差缩放', 'default' => 100, 'min' => 1, 'max' => 200, 'step' => 5, 'unit' => '%', 'description' => '放大或缩小进入视线,原图大小的百分之多少[1~200]', 'dependency' => ['image', '!=', '']], + ['id' => 'parallax_opacity', 'type' => 'spinner', 'label' => '视差透明', 'default' => 100, 'min' => 1, 'max' => 100, 'step' => 5, 'unit' => '%', 'description' => '以百分之多少的透明度进入视线[1~100]
视差功能对浏览器性能有一定影响,如果图层较多,不建议全部开启', 'dependency' => ['image|parallax', '!=|!=', '|']], ]], ['id' => 'text', 'type' => 'accordion', 'label' => '叠加文案', 'fields' => [ - ['id' => 'title', 'type' => 'textarea', 'label' => '幻灯片文案', 'default' => '', 'allow_html' => true], - ['id' => 'desc', 'type' => 'textarea', 'label' => '幻灯片简介', 'default' => '', 'allow_html' => true, 'dependency' => ['title', '!=', '']], - ['id' => 'text_align', 'type' => 'image_select', 'label' => '显示位置', 'default' => 'left-bottom', 'options' => ['left-bottom' => $align_uri . 'left-bottom.jpg', 'left-conter' => $align_uri . 'left-conter.jpg', 'conter-conter' => $align_uri . 'conter-conter.jpg', 'conter-bottom' => $align_uri . 'conter-bottom.jpg', 'right-conter' => $align_uri . 'right-conter.jpg', 'right-bottom' => $align_uri . 'right-bottom.jpg']], - ['id' => 'text_size_pc', 'type' => 'spinner', 'label' => 'PC端字体大小', 'default' => 30, 'min' => 12, 'max' => 50], - ['id' => 'text_size_m', 'type' => 'spinner', 'label' => '移动端字体大小', 'default' => 20, 'min' => 12, 'max' => 50], - ['id' => 'parallax', 'type' => 'spinner', 'label' => '文案视差滚动', 'default' => 40, 'min' => -200, 'max' => 200, 'step' => 10], + ['id' => 'title', 'type' => 'textarea', 'label' => '幻灯片文案', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]], + ['id' => 'desc', 'type' => 'textarea', 'label' => '幻灯片简介', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1], 'description' => '标题、简介均支持HTML代码,请注意代码规范及标签闭合', 'dependency' => ['title', '!=', '']], + ['id' => 'text_align', 'type' => 'image_select', 'label' => '显示位置', 'default' => 'left-bottom', 'description' => '前景图显示位置及文案位置需合理搭配', 'options' => ['left-bottom' => $align_uri . 'left-bottom.jpg', 'left-conter' => $align_uri . 'left-conter.jpg', 'conter-conter' => $align_uri . 'conter-conter.jpg', 'conter-bottom' => $align_uri . 'conter-bottom.jpg', 'right-conter' => $align_uri . 'right-conter.jpg', 'right-bottom' => $align_uri . 'right-bottom.jpg']], + ['id' => 'text_size_pc', 'type' => 'spinner', 'label' => 'PC端字体大小', 'default' => 30, 'min' => 12, 'max' => 50, 'unit' => 'PX'], + ['id' => 'text_size_m', 'type' => 'spinner', 'label' => '移动端字体大小', 'default' => 20, 'min' => 12, 'max' => 50, 'unit' => 'PX', 'description' => '在此设置标题的字体大小,简介的大小为标题大小的60%,最小12px
字体越大,文案周边的间距也越大!字体大小请根据内容合理调整,避免在某些设备显示不全'], + ['id' => 'parallax', 'type' => 'spinner', 'label' => '文案视差滚动', 'default' => 40, 'min' => -200, 'max' => 200, 'step' => 10, 'unit' => '%', 'description' => '视差滚动功能为较背景滚动的时间差,为负数则滚动慢一拍,为正数则滚动快一拍,为0则关闭'], ]], ]; $fields = [ @@ -466,10 +466,10 @@ final class WidgetSchemas { ['id' => 'm_row', 'type' => 'button_set', 'label' => '移动端卡片单行数量', 'default' => 2, 'dependency' => ['m_layout_type', 'any', 'hide_slide,vertical'], 'options' => [1 => '1个', 2 => '2个', 3 => '3个', 4 => '4个', 6 => '6个']], ['id' => 'm_cover_ratio', 'type' => 'spinner', 'label' => '移动端卡片长宽比', 'default' => 45, 'min' => 5, 'max' => 300, 'step' => 5, 'unit' => '%', 'dependency' => ['m_layout_type', 'any', 'hide_slide,vertical']], ['type' => 'subheading', 'content' => '左侧幻灯片'], - ['id' => 'slides', 'type' => 'group', 'label' => '幻灯片内容', 'item_label' => '幻灯片', 'max_items' => 30, 'min_items' => 1, 'default' => $combo_slides, 'fields' => $slide_fields, 'description' => '注意:当添加的幻灯片内容只有一个的时候,则不会以幻灯片的形式展示(不能滑动切换,相当于单个图片或视频)
由于移动端多数浏览器不支持视频背景功能,所以移动端不会显示视频!
'], + ['id' => 'slides', 'type' => 'group', 'label' => '幻灯片内容', 'item_label' => '幻灯片', 'button_title' => '添加幻灯片', 'max_items' => 30, 'min_items' => 1, 'default' => $combo_slides, 'fields' => $slide_fields, 'description' => '注意:当添加的幻灯片内容只有一个的时候,则不会以幻灯片的形式展示(不能滑动切换,相当于单个图片或视频)
由于移动端多数浏览器不支持视频背景功能,所以移动端不会显示视频!
'], ['id' => 'option', 'type' => 'accordion', 'label' => '幻灯片设置', 'default' => ['button' => true, 'pagination' => true, 'effect' => 'slide', 'spacebetween' => 20, 'speed' => 0, 'interval' => 4, 'autoplay' => true, 'loop' => true], 'fields' => $combo_option], ['type' => 'subheading', 'content' => '右侧图文视频卡片'], - ['id' => 'covers', 'type' => 'group', 'label' => '右侧图文视频卡片', 'item_label' => '卡片', 'max_items' => 30, 'default' => array_fill(0, 4, ['image' => $asset_uri . 'user_t.jpg', 'title' => '']), 'fields' => self::cover_fields(), 'description' => '
注意:由于移动端多数浏览器不支持视频背景功能,所以移动端不会显示视频!
'], + ['id' => 'covers', 'type' => 'group', 'label' => '右侧图文视频卡片', 'item_label' => '卡片', 'button_title' => '添加卡片', 'max_items' => 30, 'default' => array_fill(0, 4, ['image' => $asset_uri . 'user_t.jpg', 'title' => '']), 'fields' => self::cover_fields(), 'description' => '
注意:由于移动端多数浏览器不支持视频背景功能,所以移动端不会显示视频!
'], ['type' => 'subheading', 'content' => '卡片样式配置'], ['id' => 'mask_opacity', 'type' => 'slider', 'label' => '遮罩透明度', 'default' => 0, 'min' => 0, 'max' => 90, 'step' => 1, 'unit' => '%', 'description' => '图片上显示的黑色遮罩层的透明度'], ['id' => 'font_size_pc', 'type' => 'spinner', 'label' => 'PC端字体大小', 'default' => 16, 'min' => 10, 'max' => 80, 'step' => 2, 'unit' => 'px'], @@ -589,7 +589,7 @@ final class WidgetSchemas { private static function cover_fields(): array { return [ ['id' => 'image', 'type' => 'upload', 'label' => '图片背景', 'default' => '', 'library' => 'image'], - ['id' => 'video', 'type' => 'upload', 'label' => '视频背景', 'default' => '', 'library' => 'video', 'description' => '(必填)图片背景、视频背景至少二选一,如果同时设置则PC端视频优先(PC端视频加载失败则显示图片),移动端只显示图片'], + ['id' => 'video', 'type' => 'upload', 'label' => '视频背景', 'default' => '', 'library' => 'video', 'description' => '(必填)图片背景、视频背景至少二选一,如果同时设置则PC端视频优先(PC端视频加载失败则显示图片),移动端只显示图片'], ['id' => 'hide', 'type' => 'radio', 'label' => '显示规则', 'default' => '', 'options' => ['' => '全部显示', 'pc' => 'PC端不显示', 'm' => '移动端不显示']], ['id' => 'title', 'type' => 'textarea', 'label' => '文字', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1], 'description' => '支持HTML代码'], ['id' => 'link', 'type' => 'link', 'label' => '跳转链接', 'default' => []], diff --git a/tests/widget-system/zibll-modern-widgets-contract.test.cjs b/tests/widget-system/zibll-modern-widgets-contract.test.cjs index b1d5f8cb..7b79ed44 100644 --- a/tests/widget-system/zibll-modern-widgets-contract.test.cjs +++ b/tests/widget-system/zibll-modern-widgets-contract.test.cjs @@ -97,3 +97,29 @@ test('admin core supports composite dependencies and nested repeater renumbering assert.equal(core.dependencyVisible(['image|free_size', '!=|!=', '|'], { image: 'x', free_size: '1' }), true); assert.equal(core.replaceRepeaterIndexes('widget[3][0][1]', [4, 2]), 'widget[3][4][2]'); }); + +test('slider media combo replicates the reference module front and back', () => { + const renderer = read('src/WidgetSystem/Renderers/SliderRenderer.php'); + const schema = read('src/WidgetSystem/WidgetSchemas.php'); + const form = read('src/WidgetSystem/FormRenderer.php'); + + // 后台表单:参考主题分组按钮文案与上传字段库约束。 + for (const token of ["'button_title' => '添加幻灯片'", "'button_title' => '添加卡片'", "'button_title' => '添加图层'", "'library' => 'image'", "'library' => 'video'", 'PC端布局配置', '移动端布局方式', '左侧幻灯片', '右侧图文视频卡片', '卡片样式配置']) { + assert.match(schema, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } + assert.match(form, /\$field\['button_title'\]/); + assert.match(form, /\$library !== 'video'/); + + // 前端:幻灯片 span/a 包裹、懒加载契约、视频背景与图层视差。 + for (const token of ["'' . $body . ''", 'lazyload swiper-lazy', 'slide-background-video', "aksrt_is_lazy('lazy_sider')", 'data-swiper-parallax-opacity', 'data-swiper-parallax-scale']) { + assert.match(renderer, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } + assert.doesNotMatch(renderer, /playsinline/); + + // 组合模块:is_show 门控、管理端缺省提示、卡片列动画与比例变量。 + for (const token of ["aksrt_is_lazy('lazy_cover')", 'alert alert-warning', '当前模块下没有可显示的幻灯片', 'obs-animate ani-', "widget-graphic-cover ' . ($is_row ? 'mb10' : 'mb20')", 'row gutters-5', 'col-xs-', 'graphic-text this-font', 'm-cover-ratio', 'cover-ratio', 'layout-ratio', 'data-row=', 'data-col=', "m-' . sanitize_html_class($m_layout_type)"]) { + assert.match(renderer, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } + // 参考主题 is_show:两侧均为空时不输出占位。 + assert.doesNotMatch(renderer, /请配置幻灯片或卡片|empty_state/); +});