- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP color sanitizer; replace sanitize_hex_color calls and per-class regexes (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService) - SchemaProvider / ControllerBase sanitize `color` fields as colors; alpha kept only when the schema sets showAlpha - Split SchemaProvider OAuth validation and option matching into sibling traits to stay under the 1000-line limit - Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField, FloatBtnCommonFields and TicketStatusPanel use it - Widget background color input now uses wpColorPicker - Remove dead AksCompositeField dimensions/spacing/background branches and the unused Gutenberg ColorPicker var - Add color-unification contract test; update harness requires and tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
240 lines
9.4 KiB
PHP
240 lines
9.4 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT Slider Helper — 轮播渲染函数
|
||
*
|
||
* 对标 legacy 的 legacy(),提供服务端轮播 HTML 生成。
|
||
* 支持 data-* 属性驱动配置、视差、懒加载、缩略图分页。
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* 渲染 Swiper 轮播
|
||
*
|
||
* @param array $args {
|
||
* @type array $slides 幻灯片数据数组
|
||
* @type string $effect 动画效果: slide, fade, cube, coverflow, flip (默认 slide)
|
||
* @type bool $loop 是否循环 (默认 true)
|
||
* @type bool $autoplay 是否自动播放 (默认 true)
|
||
* @type int $interval 自动播放间隔 ms (默认 5000)
|
||
* @type int $speed 动画速度 ms (0=自动计算)
|
||
* @type string $direction 方向: horizontal, vertical (默认 horizontal)
|
||
* @type int $spaceBetween 幻灯片间距 px (默认 0)
|
||
* @type string $pagination_type 分页器类型: bullets, fraction, progressbar, thumbnail (默认 bullets)
|
||
* @type string $pc_height PC 端高度 CSS 值 (默认 auto)
|
||
* @type string $m_height 移动端高度 CSS 值 (默认 auto)
|
||
* @type bool $scale_height 是否使用比例高度 (默认 false)
|
||
* @type float $scale_ratio 比例值 (默认 0.45 = 45%)
|
||
* @type string $scale_extra 比例高度附加量,如 '50px'(用于封面标题预留,默认空)
|
||
* @type string $wrapper_class 额外 wrapper 类
|
||
* @type string $slider_class 额外 slider 类
|
||
* @type bool $show_nav 是否显示导航箭头 (默认 true)
|
||
* @type bool $show_pagination 是否显示分页器 (默认 true)
|
||
* }
|
||
*
|
||
* slide 数据格式:
|
||
* [
|
||
* 'image' => 'https://...',
|
||
* 'link' => 'https://...',
|
||
* 'title' => '标题',
|
||
* 'desc' => '描述',
|
||
* 'alt' => '图片 alt',
|
||
* 'bg_color' => '#f0f0f0', // 背景色
|
||
* 'target' => '_blank',
|
||
* ]
|
||
*
|
||
* @return string HTML
|
||
*/
|
||
function aksrt_render_slider(array $args = []): string {
|
||
$defaults = [
|
||
'slides' => [],
|
||
'effect' => 'slide',
|
||
'loop' => true,
|
||
'autoplay' => true,
|
||
'interval' => 5000,
|
||
'speed' => 0,
|
||
'direction' => 'horizontal',
|
||
'spaceBetween' => 0,
|
||
'pagination_type' => 'bullets',
|
||
'pc_height' => 'auto',
|
||
'm_height' => 'auto',
|
||
'scale_height' => false,
|
||
'scale_ratio' => 0.45,
|
||
'scale_extra' => '',
|
||
'wrapper_class' => '',
|
||
'slider_class' => '',
|
||
'show_nav' => true,
|
||
'show_pagination' => true,
|
||
// Optional absolute overlay HTML (e.g. single cover title/meta). Not escaped.
|
||
'overlay_html' => '',
|
||
];
|
||
|
||
$args = wp_parse_args($args, $defaults);
|
||
$slides = $args['slides'];
|
||
|
||
if (empty($slides) || !is_array($slides)) {
|
||
return '';
|
||
}
|
||
|
||
// 构建 data 属性
|
||
$data_attrs = [];
|
||
$data_attrs[] = 'data-effect="' . esc_attr($args['effect']) . '"';
|
||
if ($args['loop']) $data_attrs[] = 'data-loop';
|
||
if ($args['autoplay']) {
|
||
$data_attrs[] = 'data-autoplay';
|
||
$data_attrs[] = 'data-interval="' . esc_attr((string) $args['interval']) . '"';
|
||
}
|
||
if ($args['speed'] > 0) $data_attrs[] = 'data-speed="' . esc_attr((string) $args['speed']) . '"';
|
||
if ($args['direction'] !== 'horizontal') $data_attrs[] = 'data-direction="' . esc_attr($args['direction']) . '"';
|
||
if ($args['spaceBetween'] > 0) $data_attrs[] = 'data-spaceBetween="' . esc_attr((string) $args['spaceBetween']) . '"';
|
||
if ($args['pagination_type'] !== 'bullets') $data_attrs[] = 'data-pagination-type="' . esc_attr($args['pagination_type']) . '"';
|
||
|
||
// 高度样式
|
||
$height_style = '';
|
||
$scale_padding = '';
|
||
if ($args['scale_height']) {
|
||
// padding-bottom percentage for ratio box (legacy graphic cover style).
|
||
// 内层 .new-swiper 绝对定位铺满 wrapper,wrapper 决定盒子高度;
|
||
// pc/m-height 100% 让 .swiper-wrapper/.swiper-slide 铺满盒子,
|
||
// 使 object-fit: cover 在确定的盒内居中裁切(对齐单图封面)。
|
||
$ratio_pct = max(10, min(100, (float) $args['scale_ratio'] * 100));
|
||
$scale_padding = $ratio_pct . '%';
|
||
$scale_extra = trim((string) $args['scale_extra']);
|
||
if ($scale_extra !== '') {
|
||
$scale_padding = 'calc(' . $scale_padding . ' + ' . $scale_extra . ')';
|
||
}
|
||
$height_style = '--pc-height:100%;--m-height:100%;';
|
||
} else {
|
||
$height_style = sprintf('--pc-height:%s;--m-height:%s;', esc_attr($args['pc_height']), esc_attr($args['m_height']));
|
||
}
|
||
|
||
// CSS 类
|
||
$slider_classes = ['new-swiper', 'slide-index'];
|
||
if ($args['slider_class']) $slider_classes[] = $args['slider_class'];
|
||
$slider_class_str = implode(' ', $slider_classes);
|
||
|
||
$wrapper_classes = ['aksrt-slider-wrapper'];
|
||
if ($args['wrapper_class']) $wrapper_classes[] = $args['wrapper_class'];
|
||
if ($args['scale_height']) $wrapper_classes[] = 'aksrt-slider-wrapper--scale';
|
||
if ((string) $args['overlay_html'] !== '') $wrapper_classes[] = 'aksrt-slider-wrapper--has-overlay';
|
||
$wrapper_class_str = implode(' ', $wrapper_classes);
|
||
|
||
// 渲染 HTML
|
||
$html = '<div class="' . esc_attr($wrapper_class_str) . '"' . ($args['scale_height'] ? ' style="position:relative;padding-bottom:' . esc_attr($scale_padding) . ';"' : '') . '>';
|
||
$html .= '<div class="' . esc_attr($slider_class_str) . '" ' . implode(' ', $data_attrs) . ' style="' . esc_attr($height_style . ($args['scale_height'] ? 'position:absolute;inset:0;width:100%;height:100%;' : '')) . '">';
|
||
$html .= '<div class="swiper-wrapper">';
|
||
|
||
foreach ($slides as $slide) {
|
||
$image = esc_url($slide['image'] ?? '');
|
||
$link = esc_url($slide['link'] ?? '');
|
||
$title = sanitize_text_field($slide['title'] ?? '');
|
||
$desc = sanitize_text_field($slide['desc'] ?? '');
|
||
$alt = sanitize_text_field($slide['alt'] ?? $title);
|
||
$bg_color = \AKSRT\Core\Color::sanitize($slide['bg_color'] ?? '');
|
||
$target = ($slide['target'] ?? '') === '_blank' ? '_blank' : '_self';
|
||
|
||
$slide_style = $bg_color ? 'background-color:' . esc_attr($bg_color) . ';' : '';
|
||
|
||
$html .= '<div class="swiper-slide"' . ($slide_style ? ' style="' . esc_attr($slide_style) . '"' : '') . '>';
|
||
|
||
if ($link) {
|
||
$html .= '<a href="' . esc_url($link) . '" target="' . esc_attr($target) . '">';
|
||
}
|
||
|
||
if ($image) {
|
||
$html .= '<img' . aksrt_get_lazy_attr('lazy_sider', (string) $image, 'fit-cover', aksrt_get_lazy_thumb('lg')) . ' alt="' . esc_attr($alt) . '">';
|
||
}
|
||
|
||
if ($link) {
|
||
$html .= '</a>';
|
||
}
|
||
|
||
// 文字覆盖层
|
||
if ($title || $desc) {
|
||
$html .= '<div class="aksrt-slide-text">';
|
||
if ($title) $html .= '<div class="aksrt-slide-text__title">' . esc_html($title) . '</div>';
|
||
if ($desc) $html .= '<div class="aksrt-slide-text__desc">' . esc_html($desc) . '</div>';
|
||
$html .= '</div>';
|
||
}
|
||
|
||
$html .= '</div>';
|
||
}
|
||
|
||
$html .= '</div>'; // .swiper-wrapper
|
||
|
||
if ($args['show_nav']) {
|
||
$html .= '<div class="swiper-button-prev"></div>';
|
||
$html .= '<div class="swiper-button-next"></div>';
|
||
}
|
||
|
||
if ($args['show_pagination']) {
|
||
$html .= '<div class="swiper-pagination"></div>';
|
||
}
|
||
|
||
$html .= '</div>'; // .new-swiper
|
||
|
||
// Cover title/meta overlay (trusted HTML from single cover renderer).
|
||
if ((string) $args['overlay_html'] !== '') {
|
||
$html .= '<div class="aksrt-slider-overlay">' . $args['overlay_html'] . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||
}
|
||
|
||
$html .= '</div>'; // .aksrt-slider-wrapper
|
||
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* 渲染水平滚动器(Tab 导航、横向内容列表)
|
||
*
|
||
* @param array $args {
|
||
* @type array $items 内容数组
|
||
* @type string $wrapper_class 额外 wrapper 类
|
||
* @type bool $autoplay 是否自动播放
|
||
* @type int $interval 自动播放间隔
|
||
* @type string $render_item 自定义渲染回调 (callable)
|
||
* }
|
||
* @return string HTML
|
||
*/
|
||
function aksrt_render_scroll_slider(array $args = []): string {
|
||
$defaults = [
|
||
'items' => [],
|
||
'wrapper_class' => '',
|
||
'autoplay' => false,
|
||
'interval' => 4000,
|
||
'render_item' => null,
|
||
];
|
||
|
||
$args = wp_parse_args($args, $defaults);
|
||
$items = $args['items'];
|
||
|
||
if (empty($items)) return '';
|
||
|
||
$data_attrs = ['class="swiper-scroll' . ($args['wrapper_class'] ? ' ' . esc_attr($args['wrapper_class']) : '') . '"'];
|
||
if ($args['autoplay']) {
|
||
$data_attrs[] = 'data-autoplay';
|
||
$data_attrs[] = 'data-interval="' . esc_attr((string) $args['interval']) . '"';
|
||
}
|
||
|
||
$html = '<div ' . implode(' ', $data_attrs) . '>';
|
||
$html .= '<div class="swiper-wrapper">';
|
||
|
||
foreach ($items as $i => $item) {
|
||
$html .= '<div class="swiper-slide">';
|
||
if (is_callable($args['render_item'])) {
|
||
$html .= call_user_func($args['render_item'], $item, $i);
|
||
} else {
|
||
$html .= '<div class="aksrt-scroll-item">' . esc_html($item['label'] ?? '') . '</div>';
|
||
}
|
||
$html .= '</div>';
|
||
}
|
||
|
||
$html .= '</div>';
|
||
$html .= '<div class="swiper-button-prev"></div>';
|
||
$html .= '<div class="swiper-button-next"></div>';
|
||
$html .= '</div>';
|
||
|
||
return $html;
|
||
}
|