feat(widgets): AKSRT image slider widget
Lightweight multi-slide carousel with captions, dots, arrows, and CSS bundled via widgets.css import; registered in widgets-register.
This commit is contained in:
1 parent
ac3b765a83
commit
4ce3681900
4 files changed
+338
No files matched your search
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Widget slider (AKSRT 幻灯片).
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
.aksrt-widget-slider {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-2, #f3f4f6);
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--aksrt-slider-h, 220px);
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .35s ease;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide img,
|
||||||
|
.aksrt-widget-slider__link {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__caption {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,.55));
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__caption strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__prev,
|
||||||
|
.aksrt-widget-slider__next {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,0,0,.35);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__prev { left: 8px; }
|
||||||
|
.aksrt-widget-slider__next { right: 8px; }
|
||||||
|
.aksrt-widget-slider__dots {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__dot {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: rgba(255,255,255,.45);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__dot.is-active {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
@import url("widgets/part-2.css");
|
@import url("widgets/part-2.css");
|
||||||
@import url("widgets/part-3.css");
|
@import url("widgets/part-3.css");
|
||||||
@import url("widgets/part-4.css");
|
@import url("widgets/part-4.css");
|
||||||
|
@import url("widget-slider.css");
|
||||||
@@ -53,6 +53,9 @@ function aksrt_register_widgets() {
|
|||||||
// 文章目录树(zibll posts_navs)
|
// 文章目录树(zibll posts_navs)
|
||||||
register_widget('\AKSRT\Widgets\WidgetPostsNavs');
|
register_widget('\AKSRT\Widgets\WidgetPostsNavs');
|
||||||
|
|
||||||
|
// 幻灯片
|
||||||
|
register_widget('\AKSRT\Widgets\WidgetSlider');
|
||||||
|
|
||||||
// Forum widgets
|
// Forum widgets
|
||||||
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
|
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
|
||||||
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
|
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simple image slider widget (zibll widget_ui_slider lite).
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Widgets;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class WidgetSlider extends \WP_Widget {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'aksrt_slider',
|
||||||
|
esc_html__('AKSRT 幻灯片', 'aksrt'),
|
||||||
|
[
|
||||||
|
'description' => esc_html__('多图轮播,支持链接与标题。', 'aksrt'),
|
||||||
|
'classname' => 'widget_aksrt_slider',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function widget($args, $instance): void {
|
||||||
|
$slides = $this->normalize_slides($instance['slides'] ?? []);
|
||||||
|
if (!$slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$interval = max(2, min(20, (int) ($instance['interval'] ?? 4)));
|
||||||
|
$loop = !empty($instance['loop']);
|
||||||
|
$dots = !empty($instance['pagination']);
|
||||||
|
$nav = !empty($instance['button']);
|
||||||
|
$blank = !empty($instance['blank']);
|
||||||
|
$height = max(80, min(800, (int) ($instance['height'] ?? 220)));
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
|
||||||
|
if ($title !== '') {
|
||||||
|
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = 'aksrt-slider-' . esc_attr($this->id);
|
||||||
|
printf(
|
||||||
|
'<div class="aksrt-widget-slider" id="%s" data-interval="%d" data-loop="%s" style="--aksrt-slider-h:%dpx">',
|
||||||
|
$uid,
|
||||||
|
$interval * 1000,
|
||||||
|
$loop ? '1' : '0',
|
||||||
|
$height
|
||||||
|
);
|
||||||
|
echo '<div class="aksrt-widget-slider__track">';
|
||||||
|
foreach ($slides as $i => $slide) {
|
||||||
|
$active = $i === 0 ? ' is-active' : '';
|
||||||
|
echo '<div class="aksrt-widget-slider__slide' . esc_attr($active) . '">';
|
||||||
|
$img = '<img src="' . esc_url($slide['image']) . '" alt="' . esc_attr($slide['title']) . '" loading="lazy" />';
|
||||||
|
if ($slide['href'] !== '') {
|
||||||
|
printf(
|
||||||
|
'<a href="%s"%s class="aksrt-widget-slider__link">%s</a>',
|
||||||
|
esc_url($slide['href']),
|
||||||
|
$blank ? ' target="_blank" rel="noopener noreferrer"' : '',
|
||||||
|
$img // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
if ($slide['title'] !== '' || $slide['desc'] !== '') {
|
||||||
|
echo '<div class="aksrt-widget-slider__caption">';
|
||||||
|
if ($slide['title'] !== '') {
|
||||||
|
echo '<strong>' . esc_html($slide['title']) . '</strong>';
|
||||||
|
}
|
||||||
|
if ($slide['desc'] !== '') {
|
||||||
|
echo '<span>' . esc_html($slide['desc']) . '</span>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
if ($nav) {
|
||||||
|
echo '<button type="button" class="aksrt-widget-slider__prev" aria-label="' . esc_attr__('上一张', 'aksrt') . '">‹</button>';
|
||||||
|
echo '<button type="button" class="aksrt-widget-slider__next" aria-label="' . esc_attr__('下一张', 'aksrt') . '">›</button>';
|
||||||
|
}
|
||||||
|
if ($dots) {
|
||||||
|
echo '<div class="aksrt-widget-slider__dots" role="tablist">';
|
||||||
|
foreach ($slides as $i => $slide) {
|
||||||
|
printf(
|
||||||
|
'<button type="button" class="aksrt-widget-slider__dot%s" data-index="%d" aria-label="%s"></button>',
|
||||||
|
$i === 0 ? ' is-active' : '',
|
||||||
|
(int) $i,
|
||||||
|
esc_attr(sprintf(__('第 %d 张', 'aksrt'), $i + 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
// Inline init once per widget (no extra enqueue dependency).
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var root=document.getElementById(<?php echo wp_json_encode($uid); ?>);
|
||||||
|
if(!root||root._aksrtSlider)return; root._aksrtSlider=1;
|
||||||
|
var slides=[].slice.call(root.querySelectorAll('.aksrt-widget-slider__slide'));
|
||||||
|
var dots=[].slice.call(root.querySelectorAll('.aksrt-widget-slider__dot'));
|
||||||
|
var i=0, timer=null, ms=parseInt(root.getAttribute('data-interval')||'4000',10)||4000;
|
||||||
|
var loop=root.getAttribute('data-loop')==='1';
|
||||||
|
function go(n){
|
||||||
|
if(!slides.length)return;
|
||||||
|
i=(n+slides.length)%slides.length;
|
||||||
|
slides.forEach(function(s,idx){s.classList.toggle('is-active',idx===i);});
|
||||||
|
dots.forEach(function(d,idx){d.classList.toggle('is-active',idx===i);});
|
||||||
|
}
|
||||||
|
function next(){ if(!loop && i>=slides.length-1){go(0);return;} go(i+1); }
|
||||||
|
function start(){ stop(); if(slides.length>1) timer=setInterval(next, ms); }
|
||||||
|
function stop(){ if(timer){clearInterval(timer);timer=null;} }
|
||||||
|
var prev=root.querySelector('.aksrt-widget-slider__prev');
|
||||||
|
var nxt=root.querySelector('.aksrt-widget-slider__next');
|
||||||
|
if(prev) prev.addEventListener('click', function(){ go(i-1); start(); });
|
||||||
|
if(nxt) nxt.addEventListener('click', function(){ go(i+1); start(); });
|
||||||
|
dots.forEach(function(d){ d.addEventListener('click', function(){ go(parseInt(d.getAttribute('data-index')||'0',10)); start(); }); });
|
||||||
|
root.addEventListener('mouseenter', stop);
|
||||||
|
root.addEventListener('mouseleave', start);
|
||||||
|
start();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form($instance): void {
|
||||||
|
$title = (string) ($instance['title'] ?? '');
|
||||||
|
$interval = (int) ($instance['interval'] ?? 4);
|
||||||
|
$height = (int) ($instance['height'] ?? 220);
|
||||||
|
$slides = $this->normalize_slides($instance['slides'] ?? []);
|
||||||
|
if (!$slides) {
|
||||||
|
$slides = [
|
||||||
|
['image' => '', 'href' => '', 'title' => '', 'desc' => ''],
|
||||||
|
['image' => '', 'href' => '', 'title' => '', 'desc' => ''],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr($title); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('切换间隔(秒)', 'aksrt'); ?>
|
||||||
|
<input class="tiny-text" type="number" min="2" max="20" name="<?php echo esc_attr($this->get_field_name('interval')); ?>" value="<?php echo esc_attr((string) $interval); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('高度(px)', 'aksrt'); ?>
|
||||||
|
<input class="tiny-text" type="number" min="80" max="800" name="<?php echo esc_attr($this->get_field_name('height')); ?>" value="<?php echo esc_attr((string) $height); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('loop')); ?>" value="1" <?php checked(!empty($instance['loop']) || !isset($instance['loop'])); ?>> <?php esc_html_e('循环播放', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('button')); ?>" value="1" <?php checked(!empty($instance['button']) || !isset($instance['button'])); ?>> <?php esc_html_e('翻页按钮', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('pagination')); ?>" value="1" <?php checked(!empty($instance['pagination']) || !isset($instance['pagination'])); ?>> <?php esc_html_e('指示点', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('blank')); ?>" value="1" <?php checked(!empty($instance['blank'])); ?>> <?php esc_html_e('新窗口打开', 'aksrt'); ?></label>
|
||||||
|
</p>
|
||||||
|
<?php foreach ($slides as $i => $slide) : ?>
|
||||||
|
<p style="border-top:1px solid #ddd;padding-top:8px;">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('幻灯片 %d', 'aksrt'), $i + 1)); ?></strong><br>
|
||||||
|
<label><?php esc_html_e('图片 URL', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][image]"); ?>" value="<?php echo esc_attr($slide['image']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('链接', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][href]"); ?>" value="<?php echo esc_attr($slide['href']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][title]"); ?>" value="<?php echo esc_attr($slide['title']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('简介', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][desc]"); ?>" value="<?php echo esc_attr($slide['desc']); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<p class="description"><?php esc_html_e('保存后可继续编辑;需要更多张可先填满再在下次编辑时追加(当前表单固定 2–6 槽)。', 'aksrt'); ?></p>
|
||||||
|
<?php
|
||||||
|
// Ensure at least 4 slots for editing convenience.
|
||||||
|
$extra = max(0, 4 - count($slides));
|
||||||
|
for ($j = 0; $j < $extra; $j++) {
|
||||||
|
$i = count($slides) + $j;
|
||||||
|
?>
|
||||||
|
<p style="border-top:1px dashed #eee;padding-top:8px;">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('空槽 %d', 'aksrt'), $i + 1)); ?></strong>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][image]"); ?>" placeholder="<?php esc_attr_e('图片 URL', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][href]"); ?>" placeholder="<?php esc_attr_e('链接', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][title]"); ?>" placeholder="<?php esc_attr_e('标题', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][desc]"); ?>" placeholder="<?php esc_attr_e('简介', 'aksrt'); ?>">
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($new_instance, $old_instance): array {
|
||||||
|
$slides = $this->normalize_slides($new_instance['slides'] ?? []);
|
||||||
|
return [
|
||||||
|
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||||
|
'interval' => max(2, min(20, (int) ($new_instance['interval'] ?? 4))),
|
||||||
|
'height' => max(80, min(800, (int) ($new_instance['height'] ?? 220))),
|
||||||
|
'loop' => !empty($new_instance['loop']) ? 1 : 0,
|
||||||
|
'button' => !empty($new_instance['button']) ? 1 : 0,
|
||||||
|
'pagination' => !empty($new_instance['pagination']) ? 1 : 0,
|
||||||
|
'blank' => !empty($new_instance['blank']) ? 1 : 0,
|
||||||
|
'slides' => $slides,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $raw
|
||||||
|
* @return array<int, array{image:string,href:string,title:string,desc:string}>
|
||||||
|
*/
|
||||||
|
private function normalize_slides($raw): array {
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($raw as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$image = esc_url_raw(trim((string) ($row['image'] ?? $row['link'] ?? '')));
|
||||||
|
if ($image === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'image' => $image,
|
||||||
|
'href' => esc_url_raw(trim((string) ($row['href'] ?? ''))),
|
||||||
|
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
|
||||||
|
'desc' => sanitize_text_field((string) ($row['desc'] ?? $row['dec'] ?? '')),
|
||||||
|
];
|
||||||
|
if (count($out) >= 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user