feat(widgets): search hot keywords + multi-item scroll notice

Deepen zibll search/notice widgets: hot keywords from settings,
custom keywords, more-link, and multi-message marquee notice bar.
This commit is contained in:
2026-07-19 01:12:10 +08:00
parent ff919da342
commit 491fc6baf4
3 changed files with 334 additions and 98 deletions
+62
View File
@@ -305,3 +305,65 @@
.aksrt-bookmark-links__text { min-width: 0; display: flex; flex-direction: column; }
.aksrt-bookmark-links__name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aksrt-bookmark-links__desc { font-size: 11px; color: var(--muted-2-color, #999); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Search keywords */
.aksrt-search-widget__keywords { margin-top: 10px; }
.aksrt-search-widget__kw-title {
font-size: 12px;
color: var(--muted-2-color, #888);
margin-bottom: 6px;
}
.aksrt-search-widget__kw-list { display: flex; flex-wrap: wrap; gap: 6px; }
.aksrt-search-widget__kw {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
background: var(--surface-2, #f3f4f6);
color: inherit;
text-decoration: none;
font-size: 12px;
}
.aksrt-search-widget__more { margin-top: 8px; text-align: right; font-size: 12px; }
.aksrt-search-widget__more a { color: var(--color-primary, #3b82f6); text-decoration: none; }
/* Scroll notice */
.aksrt-scroll-notice {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 10px;
background: #eff6ff;
color: #1d4ed8;
margin-bottom: 8px;
overflow: hidden;
}
.aksrt-scroll-notice.is-radius { border-radius: 999px; }
.aksrt-scroll-notice.color-c-green { background: #ecfdf5; color: #047857; }
.aksrt-scroll-notice.color-c-yellow { background: #fffbeb; color: #b45309; }
.aksrt-scroll-notice.color-c-red { background: #fef2f2; color: #b91c1c; }
.aksrt-scroll-notice.color-c-purple { background: #f5f3ff; color: #6d28d9; }
.aksrt-scroll-notice__icon { flex-shrink: 0; }
.aksrt-scroll-notice__viewport { position: relative; flex: 1; min-width: 0; height: 1.4em; }
.aksrt-scroll-notice__item {
position: absolute;
inset: 0;
opacity: 0;
transform: translateY(8px);
transition: opacity .35s ease, transform .35s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
line-height: 1.4;
}
.aksrt-scroll-notice__item.is-active {
opacity: 1;
transform: translateY(0);
}
.aksrt-scroll-notice__item a {
color: inherit;
text-decoration: none;
}
.aksrt-scroll-notice__item i { margin-right: 6px; opacity: .85; }
+159 -56
View File
@@ -1,5 +1,7 @@
<?php
/**
* Scrolling notice bar (zibll widget_ui_notice lite).
*
* @package AKSRT
*/
@@ -9,64 +11,165 @@ defined('ABSPATH') || exit;
class NoticeWidget extends \WP_Widget {
public function __construct() {
parent::__construct('aksrt_notice', esc_html__('AKSRT 公告', 'aksrt'), [
'description' => esc_html__('显示公告/通知信息', 'aksrt'),
]);
}
public function __construct() {
parent::__construct('aksrt_notice', esc_html__('AKSRT 滚动公告', 'aksrt'), [
'description' => esc_html__('多条公告滚动显示。', 'aksrt'),
]);
}
public function widget($args, $instance): void {
echo $args['before_widget'];
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
$content = wp_kses_post($instance['content'] ?? '');
$style = sanitize_key($instance['style'] ?? 'info');
$icon = sanitize_text_field($instance['icon'] ?? 'fas fa-bullhorn');
public function widget($args, $instance): void {
$items = $this->normalize_items($instance);
if (!$items) {
return;
}
if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title'];
?>
<div class="widget-notice widget-notice--<?php echo esc_attr($style); ?>">
<?php if ($icon) : ?>
<div class="widget-notice__icon"><i class="<?php echo esc_attr($icon); ?>" aria-hidden="true"></i></div>
<?php endif; ?>
<div class="widget-notice__content"><?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
</div>
<?php
echo $args['after_widget'];
}
$color = sanitize_html_class((string) ($instance['color'] ?? 'c-blue'));
$blank = !empty($instance['blank']);
$radius = !empty($instance['radius']);
$interval = max(2, min(15, (int) ($instance['interval'] ?? 4)));
public function form($instance): void {
?>
<p>
<label for="<?php echo $this->get_field_id('title'); ?>"><?php esc_html_e('标题(可选):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($instance['title'] ?? ''); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('content'); ?>"><?php esc_html_e('公告内容:', 'aksrt'); ?></label>
<textarea class="widefat" rows="4" id="<?php echo $this->get_field_id('content'); ?>" name="<?php echo $this->get_field_name('content'); ?>"><?php echo esc_textarea($instance['content'] ?? ''); ?></textarea>
</p>
<p>
<label for="<?php echo $this->get_field_id('style'); ?>"><?php esc_html_e('样式:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo $this->get_field_id('style'); ?>" name="<?php echo $this->get_field_name('style'); ?>">
<option value="info" <?php selected($instance['style'] ?? '', 'info'); ?>><?php esc_html_e('信息(蓝色)', 'aksrt'); ?></option>
<option value="success" <?php selected($instance['style'] ?? '', 'success'); ?>><?php esc_html_e('成功(绿色)', 'aksrt'); ?></option>
<option value="warning" <?php selected($instance['style'] ?? '', 'warning'); ?>><?php esc_html_e('警告(黄色)', 'aksrt'); ?></option>
<option value="danger" <?php selected($instance['style'] ?? '', 'danger'); ?>><?php esc_html_e('危险(红色)', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo $this->get_field_id('icon'); ?>"><?php esc_html_e('图标类名:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo $this->get_field_id('icon'); ?>" name="<?php echo $this->get_field_name('icon'); ?>" value="<?php echo esc_attr($instance['icon'] ?? 'fas fa-bullhorn'); ?>">
<small style="color:#666;"><?php esc_html_e('Font Awesome 类名,如: fas fa-bullhorn', 'aksrt'); ?></small>
</p>
<?php
}
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
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'content' => wp_kses_post($new_instance['content'] ?? ''),
'style' => in_array($new_instance['style'] ?? '', ['info', 'success', 'warning', 'danger'], true) ? $new_instance['style'] : 'info',
'icon' => sanitize_text_field($new_instance['icon'] ?? 'fas fa-bullhorn'),
];
}
$uid = 'aksrt-notice-' . esc_attr($this->id);
$class = 'aksrt-scroll-notice color-' . $color . ($radius ? ' is-radius' : '');
echo '<div class="' . esc_attr($class) . '" id="' . $uid . '" data-interval="' . esc_attr((string) ($interval * 1000)) . '">';
echo '<div class="aksrt-scroll-notice__icon"><i class="fas fa-bullhorn" aria-hidden="true"></i></div>';
echo '<div class="aksrt-scroll-notice__viewport">';
foreach ($items as $i => $item) {
$active = $i === 0 ? ' is-active' : '';
$inner = '';
if ($item['icon'] !== '') {
$inner .= '<i class="' . esc_attr($item['icon']) . '" aria-hidden="true"></i> ';
}
$inner .= esc_html($item['title']);
echo '<div class="aksrt-scroll-notice__item' . esc_attr($active) . '">';
if ($item['href'] !== '') {
$tgt = $blank ? ' target="_blank" rel="noopener noreferrer"' : '';
echo '<a href="' . esc_url($item['href']) . '"' . $tgt . '>' . $inner . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
echo '<span>' . $inner . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
}
echo '</div></div>';
if (count($items) > 1) {
?>
<script>
(function(){
var root=document.getElementById(<?php echo wp_json_encode($uid); ?>);
if(!root||root._aksrtNotice)return; root._aksrtNotice=1;
var items=[].slice.call(root.querySelectorAll('.aksrt-scroll-notice__item'));
var i=0, ms=parseInt(root.getAttribute('data-interval')||'4000',10)||4000;
setInterval(function(){
if(items.length<2)return;
items[i].classList.remove('is-active');
i=(i+1)%items.length;
items[i].classList.add('is-active');
}, ms);
})();
</script>
<?php
}
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param array<string,mixed> $instance
* @return array<int, array{title:string,icon:string,href:string}>
*/
private function normalize_items(array $instance): array {
$raw = $instance['items'] ?? $instance['img_ids'] ?? null;
if ($raw === null && !empty($instance['content'])) {
// Legacy single content.
return [[
'title' => wp_strip_all_tags((string) $instance['content']),
'icon' => sanitize_text_field((string) ($instance['icon'] ?? 'fas fa-bullhorn')),
'href' => '',
]];
}
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$title = sanitize_text_field((string) ($row['title'] ?? $row['content'] ?? ''));
if ($title === '') {
continue;
}
$out[] = [
'title' => $title,
'icon' => sanitize_text_field((string) ($row['icon'] ?? '')),
'href' => esc_url_raw(trim((string) ($row['href'] ?? $row['url'] ?? $row['link'] ?? ''))),
];
if (count($out) >= 12) {
break;
}
}
return $out;
}
public function form($instance): void {
$items = $this->normalize_items($instance);
if (!$items) {
$items = [
['title' => '', 'icon' => 'fas fa-bullhorn', 'href' => ''],
['title' => '', 'icon' => 'fas fa-info-circle', 'href' => ''],
];
}
$json = wp_json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<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((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('公告条目 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="6" name="<?php echo esc_attr($this->get_field_name('items')); ?>" placeholder='[{"title":"公告内容","icon":"fas fa-bullhorn","href":""}]'><?php echo esc_textarea((string) $json); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('切换间隔(秒)', 'aksrt'); ?>
<input class="tiny-text" type="number" min="2" max="15" name="<?php echo esc_attr($this->get_field_name('interval')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['interval'] ?? 4))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('主题色 class', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('color')); ?>">
<?php foreach (['c-blue' => '蓝', 'c-green' => '绿', 'c-yellow' => '黄', 'c-red' => '红', 'c-purple' => '紫'] as $k => $label) : ?>
<option value="<?php echo esc_attr($k); ?>" <?php selected(($instance['color'] ?? 'c-blue'), $k); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</label>
</p>
<p>
<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><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('radius')); ?>" value="1" <?php checked(!empty($instance['radius'])); ?>> <?php esc_html_e('两端圆形', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'items' => $this->normalize_items(['items' => $new_instance['items'] ?? []]),
'interval' => max(2, min(15, (int) ($new_instance['interval'] ?? 4))),
'color' => sanitize_html_class((string) ($new_instance['color'] ?? 'c-blue')),
'blank' => !empty($new_instance['blank']) ? 1 : 0,
'radius' => !empty($new_instance['radius']) ? 1 : 0,
];
}
}
+113 -42
View File
@@ -1,5 +1,7 @@
<?php
/**
* Search box with hot keywords (zibll widget_ui_search lite).
*
* @package AKSRT
*/
@@ -9,49 +11,118 @@ defined('ABSPATH') || exit;
class SearchWidget extends \WP_Widget {
public function __construct() {
parent::__construct('aksrt_search', esc_html__('AKSRT 搜索', 'aksrt'), [
'description' => esc_html__('带样式的搜索框', 'aksrt'),
]);
}
public function __construct() {
parent::__construct('aksrt_search', esc_html__('AKSRT 搜索', 'aksrt'), [
'description' => esc_html__('搜索框,可显示热门关键词。', 'aksrt'),
]);
}
public function widget($args, $instance): void {
echo $args['before_widget'];
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title'];
$placeholder = sanitize_text_field($instance['placeholder'] ?? __('搜索文章...', 'aksrt'));
?>
<div class="widget-search-box">
<form role="search" method="get" action="<?php echo esc_url(home_url('/')); ?>">
<div class="widget-search-box__inner">
<input type="search" name="s" class="widget-search-box__input" placeholder="<?php echo esc_attr($placeholder); ?>" value="<?php echo esc_attr(get_search_query()); ?>" autocomplete="off">
<button type="submit" class="widget-search-box__btn" aria-label="<?php esc_attr_e('搜索', 'aksrt'); ?>">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
<?php
echo $args['after_widget'];
}
public function widget($args, $instance): void {
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
}
public function form($instance): void {
?>
<p>
<label for="<?php echo $this->get_field_id('title'); ?>"><?php esc_html_e('标题(可选):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($instance['title'] ?? ''); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('placeholder'); ?>"><?php esc_html_e('占位文本:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo $this->get_field_id('placeholder'); ?>" name="<?php echo $this->get_field_name('placeholder'); ?>" value="<?php echo esc_attr($instance['placeholder'] ?? __('搜索文章...', 'aksrt')); ?>">
</p>
<?php
}
$placeholder = sanitize_text_field((string) ($instance['placeholder'] ?? __('开启精彩搜索', 'aksrt')));
$show_kw = !empty($instance['show_keywords']);
$kw_title = sanitize_text_field((string) ($instance['keywords_title'] ?? __('热门搜索', 'aksrt')));
$more_but = sanitize_text_field((string) ($instance['more_but'] ?? ''));
$more_url = esc_url_raw(trim((string) ($instance['more_but_url'] ?? '')));
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'placeholder' => sanitize_text_field($new_instance['placeholder'] ?? ''),
];
}
$keywords = [];
if ($show_kw) {
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'hot_search_keywords')) {
$keywords = \AKSRT\Settings\Settings::hot_search_keywords();
} elseif (function_exists('aksrt_get_setting')) {
$raw = (string) aksrt_get_setting('hot_search_keywords', '');
$keywords = array_values(array_filter(array_map('trim', preg_split('/[\r\n,,]+/', $raw) ?: [])));
}
$custom = trim((string) ($instance['keywords'] ?? ''));
if ($custom !== '') {
$keywords = array_values(array_filter(array_map('trim', preg_split('/[\r\n,,]+/', $custom) ?: [])));
}
$keywords = array_slice($keywords, 0, 12);
}
?>
<div class="widget-search-box aksrt-search-widget">
<form role="search" method="get" action="<?php echo esc_url(home_url('/')); ?>">
<div class="widget-search-box__inner">
<input type="search" name="s" class="widget-search-box__input" placeholder="<?php echo esc_attr($placeholder); ?>" value="<?php echo esc_attr(get_search_query()); ?>" autocomplete="off">
<button type="submit" class="widget-search-box__btn" aria-label="<?php esc_attr_e('搜索', 'aksrt'); ?>">
<i class="fas fa-search"></i>
</button>
</div>
</form>
<?php if ($keywords) : ?>
<div class="aksrt-search-widget__keywords">
<?php if ($kw_title !== '') : ?>
<div class="aksrt-search-widget__kw-title"><?php echo esc_html($kw_title); ?></div>
<?php endif; ?>
<div class="aksrt-search-widget__kw-list">
<?php foreach ($keywords as $kw) : ?>
<a class="aksrt-search-widget__kw" href="<?php echo esc_url(add_query_arg('s', $kw, home_url('/'))); ?>"><?php echo esc_html($kw); ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if ($more_but !== '' && $more_url !== '') : ?>
<div class="aksrt-search-widget__more">
<a href="<?php echo esc_url($more_url); ?>"><?php echo esc_html($more_but); ?></a>
</div>
<?php endif; ?>
</div>
<?php
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
?>
<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((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('占位文本', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('placeholder')); ?>" value="<?php echo esc_attr((string) ($instance['placeholder'] ?? __('开启精彩搜索', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_keywords')); ?>" value="1" <?php checked(!empty($instance['show_keywords'])); ?>> <?php esc_html_e('显示热门关键词', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('关键词标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('keywords_title')); ?>" value="<?php echo esc_attr((string) ($instance['keywords_title'] ?? __('热门搜索', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('自定义关键词(逗号/换行;空=后台配置)', 'aksrt'); ?>
<textarea class="widefat" rows="2" name="<?php echo esc_attr($this->get_field_name('keywords')); ?>"><?php echo esc_textarea((string) ($instance['keywords'] ?? '')); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('更多按钮文案', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('more_but')); ?>" value="<?php echo esc_attr((string) ($instance['more_but'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('更多按钮链接', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('more_but_url')); ?>" value="<?php echo esc_attr((string) ($instance['more_but_url'] ?? '')); ?>">
</label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'placeholder' => sanitize_text_field((string) ($new_instance['placeholder'] ?? '')),
'show_keywords' => !empty($new_instance['show_keywords']) ? 1 : 0,
'keywords_title' => sanitize_text_field((string) ($new_instance['keywords_title'] ?? '')),
'keywords' => sanitize_textarea_field((string) ($new_instance['keywords'] ?? '')),
'more_but' => sanitize_text_field((string) ($new_instance['more_but'] ?? '')),
'more_but_url' => esc_url_raw(trim((string) ($new_instance['more_but_url'] ?? ''))),
];
}
}