' . $items . '
' . $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 . '
';
+
+ $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/);
+});