对小工具进行了一些修复

This commit is contained in:
2026-08-26 11:06:05 +08:00
parent c6b1acfe0e
commit 523af723ae
12 changed files with 433 additions and 37 deletions
+10
View File
@@ -98,6 +98,16 @@
align-items: center;
}
.aksrt-widget-remote-picker { position: relative; }
.aksrt-widget-remote-picker > select { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.aksrt-widget-remote-picker__selected { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; }
.aksrt-widget-remote-picker__tag { padding: 4px 8px; border: 1px solid #c3c4c7; border-radius: 3px; background: #f6f7f7; color: #2c3338; cursor: pointer; }
.aksrt-widget-remote-picker__tag::after { margin-left: 6px; color: #787c82; content: "×"; }
.aksrt-widget-remote-picker__results { position: absolute; z-index: 20; right: 0; left: 0; max-height: 210px; overflow-y: auto; border: 1px solid #8c8f94; border-top: 0; background: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, .12); }
.aksrt-widget-remote-picker__result, .aksrt-widget-remote-picker__status { display: block; width: 100%; padding: 7px 9px; border: 0; background: #fff; color: #2c3338; text-align: left; }
.aksrt-widget-remote-picker__result { cursor: pointer; }
.aksrt-widget-remote-picker__result:hover, .aksrt-widget-remote-picker__result:focus { background: #f0f6fc; color: #135e96; }
.aksrt-widget-upload {
display: flex;
flex-wrap: wrap;
+62
View File
@@ -16,11 +16,73 @@
const output = this.parentElement.querySelector('output');
if (output) this.addEventListener('input', () => { output.textContent = this.value; });
});
initRemoteSelects($root);
evaluateDependencies($root);
updateWidgetTitle($root);
updatePlacementWarning($root);
}
function initRemoteSelects(root) {
$(root).find('select.aksrt-widget-remote-select').each(function () {
const $select = $(this);
if ($select.data('remote-ready')) return;
$select.data('remote-ready', true);
const $picker = $('<div class="aksrt-widget-remote-picker"></div>');
const $selected = $('<div class="aksrt-widget-remote-picker__selected"></div>');
const $search = $('<input type="search" class="widefat aksrt-widget-remote-picker__search" autocomplete="off">').attr('placeholder', String($select.data('placeholder') || '输入关键词以搜索'));
const $results = $('<div class="aksrt-widget-remote-picker__results" role="listbox" hidden></div>');
$select.wrap($picker);
$select.after($selected, $search, $results);
const renderSelected = () => {
$selected.empty();
$select.find('option:selected').each(function () {
const $tag = $('<button type="button" class="aksrt-widget-remote-picker__tag"></button>').text($(this).text()).attr('aria-label', `移除 ${$(this).text()}`);
$tag.on('click', () => { $(this).remove(); $select.trigger('change'); renderSelected(); });
$selected.append($tag);
});
};
let timer = 0;
$search.on('input', function () {
window.clearTimeout(timer);
const query = String(this.value || '').trim();
const minLength = Number($select.data('min-length') || 0);
if (query.length < minLength) { $results.prop('hidden', true).empty(); return; }
timer = window.setTimeout(() => {
$results.prop('hidden', false).empty().append($('<span class="aksrt-widget-remote-picker__status"></span>').text(config.i18n?.loading || '正在加载…'));
$.post(config.ajaxUrl, {
action: config.searchAction,
nonce: config.nonce,
source: $select.data('option-source'),
queryArgs: $select.attr('data-query-args') || '{}',
search: query,
}).done((response) => {
$results.empty();
const items = response?.success && Array.isArray(response.data?.results) ? response.data.results : [];
if (!items.length) { $results.append($('<span class="aksrt-widget-remote-picker__status"></span>').text('未找到匹配内容')); return; }
items.forEach((item) => {
const id = String(item.id || '');
const text = String(item.text || `#${id}`);
const $button = $('<button type="button" class="aksrt-widget-remote-picker__result" role="option"></button>').text(text);
$button.on('click', () => {
let $option = $select.find('option').filter(function () { return String(this.value) === id; }).first();
if (!$option.length) $option = $('<option></option>').val(id).text(text).appendTo($select);
$option.prop('selected', true);
$select.trigger('change');
renderSelected();
$search.val('').trigger('focus');
$results.prop('hidden', true).empty();
});
$results.append($button);
});
}).fail(() => $results.empty().append($('<span class="aksrt-widget-remote-picker__status"></span>').text(config.i18n?.error || '加载失败,请重试。')));
}, 250);
});
renderSelected();
});
}
function updateWidgetTitle(root) {
const $root = $(root);
const title = String($root.find('.aksrt-widget-field[data-field-id="title"] input').first().val() || '').trim();
+23 -5
View File
@@ -99,15 +99,27 @@
.aksrt-widget-text-title { padding: 18px 0; }.aksrt-widget-text-title.is-center { text-align: center; }.aksrt-widget-text-title.is-right { text-align: right; }
.aksrt-widget-text-title h2 { margin: 0; font-size: clamp(26px,4vw,44px); line-height: 1.25; }.aksrt-widget-text-title p { margin: 12px 0 0; color: var(--muted-color,#8a8c98); line-height: 1.8; }
.aksrt-widget-buttons { display: flex; flex-wrap: wrap; gap: var(--aksrt-button-gap,10px); }.aksrt-widget-buttons.is-center { justify-content: center; }.aksrt-widget-buttons.is-right { justify-content: flex-end; }
.aksrt-widget-button { display: inline-flex; min-height: 40px; padding: 9px 18px; align-items: center; justify-content: center; gap: 7px; border: 0; border-radius: 9px; background: var(--focus-color,#ff5473); color: #fff !important; font-weight: 600; cursor: pointer; transition: filter .2s, transform .2s; }
.aksrt-widget-button:hover { filter: brightness(1.07); transform: translateY(-1px); }.aksrt-widget-button.is-secondary { background: rgba(127,127,127,.12); color: inherit !important; }
.aksrt-user-card__guest-actions .aksrt-widget-button, .aksrt-user-card__actions > .aksrt-widget-button { display: inline-flex; min-height: 40px; padding: 9px 18px; align-items: center; justify-content: center; gap: 7px; border: 0; border-radius: 9px; background: var(--focus-color,#ff5473); color: #fff !important; font-weight: 600; cursor: pointer; transition: filter .2s, transform .2s; }
.aksrt-user-card__guest-actions .aksrt-widget-button:hover, .aksrt-user-card__actions > .aksrt-widget-button:hover { filter: brightness(1.07); transform: translateY(-1px); }.aksrt-user-card__guest-actions .aksrt-widget-button.is-secondary { background: rgba(127,127,127,.12); color: inherit !important; }
.aksrt-widget-gap { height: var(--aksrt-gap); }
.aksrt-widget-tag-cloud { display: flex; flex-wrap: wrap; gap: 8px; }.aksrt-widget-tag-cloud a { padding: 6px 11px; border-radius: 6px; background: rgba(127,127,127,.1); font-size: 13px; transition: background .2s,color .2s; }.aksrt-widget-tag-cloud a:hover { background: var(--focus-color,#ff5473); color: #fff; }.aksrt-widget-tag-cloud small { margin-left: 5px; opacity: .65; }
.aksrt-widget-comments { display: grid; gap: 15px; margin: 0; padding: 0; list-style: none; }.aksrt-widget-comments li { display: flex; gap: 11px; }.aksrt-widget-comments__avatar img { width: 42px; height: 42px; border-radius: 50%; }.aksrt-widget-comments p { margin: 3px 0; color: var(--muted-color,#8a8c98); font-size: 13px; line-height: 1.55; }.aksrt-widget-comments__source { font-size: 12px; opacity: .65; }
.aksrt-widget-links { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; }.aksrt-widget-links a { display: flex; flex-direction: column; gap: 4px; padding: 10px 8px; border-radius: var(--main-radius,8px); background: var(--muted-border-color,#f2f3f5); color:var(--key-color,#333); }.aksrt-widget-links small { color: var(--muted-color,#8a8c98); }.aksrt-widget-links--simple { display: flex; flex-wrap: wrap; }.aksrt-widget-links--simple a { padding: 5px 9px; box-shadow: none; background: transparent; }
.aksrt-widget-notice { display: flex; height: 48px; align-items: center; gap: 12px; padding: 0 15px; overflow: hidden; border-radius: var(--main-radius,8px); background: var(--main-bg-color,#fff); box-shadow: 0 0 10px var(--main-shadow,rgba(116,116,116,.08)); }.aksrt-widget-notice .swiper { height: 48px; flex: 1; }.aksrt-widget-notice .swiper-slide, .aksrt-widget-notice .swiper-slide a { display: flex; height: 48px; align-items: center; gap: 10px; }.aksrt-widget-notice .swiper-slide span { overflow: hidden; color: var(--muted-color,#8a8c98); text-overflow: ellipsis; white-space: nowrap; }
.aksrt-widget-swiper { position: relative; }
.widget-buttons-wrap { margin: calc(0px - var(--pc-gap, 10px)); }
.widget-buttons-wrap > div { margin: var(--pc-gap, 10px); }
.widget-buttons-wrap .but { width: var(--pc-width, auto); }
.widget-buttons-wrap.align-center { justify-content: center; }
.widget-buttons-wrap.align-right { justify-content: flex-end; }
.widget-buttons-wrap.align-left { justify-content: flex-start; }
.widget-buttons.pc-size-small .but { padding: .3em .6em; }
.widget-buttons.pc-size-medium .but { padding: 8px 16px; }
.widget-buttons.pc-size-large .but { padding: 12px 24px; }
.swiper-bulletin { padding: .1em 6px; background: var(--this-bg); }
.swiper-bulletin .new-swiper { line-height: 2.4em; height: 2.4em; overflow: hidden; }
.swiper-bulletin a { color: var(--this-color); }
.bulletin-icon { background: var(--this-color); width: 26px; height: 26px; text-align: center; border-radius: 30px; color: #fff; font-size: 18px; }
.swiper-bulletin[class*="b-"] .bulletin-icon { color: var(--this-bg); }
.swiper-bulletin[class*="jb-"] .bulletin-icon { color: var(--this-bg-b); }
.aksrt-term-card { display: block; overflow: hidden; border-radius: var(--aksrt-widget-radius); background: var(--main-bg-color,#fff); box-shadow: 0 0 10px var(--main-shadow,rgba(116,116,116,.08)); }.aksrt-term-card__media { display: block; aspect-ratio: 16/9; background: var(--muted-border-color,#eee); }.aksrt-term-card__media img { width: 100%; height: 100%; object-fit: cover; }.aksrt-term-card__body { display: flex; flex-direction: column; gap: 5px; padding: 14px; }.aksrt-term-card__body small { min-height: 38px; color: var(--muted-color,#8a8c98); }.aksrt-term-card__body em { font-size: 12px; color: var(--focus-color,#ff5473); font-style: normal; }
.aksrt-term-aggregation { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 15px; }.aksrt-term-aggregation__card { padding: 15px; border-radius: var(--aksrt-widget-radius); background: var(--main-bg-color,#fff); box-shadow: 0 0 10px var(--main-shadow,rgba(116,116,116,.08)); }.aksrt-term-aggregation header a { display: flex; justify-content: space-between; }.aksrt-term-aggregation header span { color: var(--muted-color,#8a8c98); font-size: 13px; }.aksrt-term-aggregation ul { display: grid; gap: 10px; margin: 14px 0 0; padding: 0; list-style: none; }.aksrt-term-aggregation li a { display: flex; align-items: center; gap: 10px; }.aksrt-term-aggregation li img { width: 55px; height: 42px; border-radius: 6px; object-fit: cover; }
@@ -133,6 +145,12 @@
.aksrt-widget-grid { grid-template-columns: repeat(var(--aksrt-cols-m,1),minmax(0,1fr)); gap: 10px; }
.aksrt-widget__header { align-items: flex-start; flex-direction: column; }.aksrt-widget__title-links { justify-content: flex-start; }
.aksrt-widget-links { grid-template-columns: repeat(2,minmax(0,1fr)); }
.widget-buttons-wrap { margin: calc(0px - var(--m-gap, 5px)); }
.widget-buttons-wrap > div { margin: var(--m-gap, 5px); }
.widget-buttons-wrap .but { width: var(--m-width, auto); }
.widget-buttons.m-size-small .but { padding: .3em .6em; }
.widget-buttons.m-size-medium .but { padding: 8px 16px; }
.widget-buttons.m-size-large .but { padding: 12px 24px; }
.aksrt-widget-partners__track { grid-template-rows: repeat(var(--aksrt-partner-rows-m),70px); }
.aksrt-term-aggregation, .aksrt-feature-intro__panels article, .aksrt-slider-combo { grid-template-columns: 1fr; }
.aksrt-highlight--large { grid-column: auto; }
+37
View File
@@ -9,10 +9,12 @@ defined('ABSPATH') || exit;
*/
final class AdminFormController {
public const AJAX_ACTION = 'aksrt_widget_form';
public const SEARCH_ACTION = 'aksrt_widget_remote_options';
private const NONCE_ACTION = 'aksrt_widget_form';
public static function register_hooks(): void {
add_action('wp_ajax_' . self::AJAX_ACTION, [self::class, 'ajax_form']);
add_action('wp_ajax_' . self::SEARCH_ACTION, [self::class, 'ajax_remote_options']);
add_action('admin_enqueue_scripts', [self::class, 'enqueue']);
add_action('customize_controls_enqueue_scripts', [self::class, 'enqueue']);
}
@@ -33,6 +35,7 @@ final class AdminFormController {
wp_localize_script('aksrt-widget-admin', 'aksrtWidgetAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'action' => self::AJAX_ACTION,
'searchAction' => self::SEARCH_ACTION,
'nonce' => wp_create_nonce(self::NONCE_ACTION),
'transferNonce' => wp_create_nonce('aksrt_widgets_transfer'),
'i18n' => [
@@ -64,5 +67,39 @@ final class AdminFormController {
$html = (string) ob_get_clean();
wp_send_json_success(['html' => $html]);
}
public static function ajax_remote_options(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => __('权限不足。', 'aksrt')], 403);
}
$source = isset($_POST['source']) ? sanitize_key(wp_unslash($_POST['source'])) : '';
$search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
$query_json = isset($_POST['queryArgs']) ? wp_unslash($_POST['queryArgs']) : '';
$query_args = is_string($query_json) ? json_decode($query_json, true) : [];
$query_args = is_array($query_args) ? $query_args : [];
$results = [];
if ($source === 'post' && sanitize_key((string) ($query_args['post_type'] ?? '')) === 'plate') {
$posts = get_posts([
'post_type' => 'plate',
'post_status' => 'publish',
'posts_per_page' => 20,
's' => $search,
'orderby' => 'relevance date',
'order' => 'DESC',
]);
foreach ($posts as $post) $results[] = ['id' => (string) $post->ID, 'text' => get_the_title($post) ?: '#' . $post->ID];
} elseif ($source === 'categories') {
$taxonomy = sanitize_key((string) ($query_args['taxonomy'] ?? ''));
if (in_array($taxonomy, ['forum_topic', 'forum_tag'], true) && taxonomy_exists($taxonomy)) {
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 20, 'search' => $search]);
if (!is_wp_error($terms)) foreach ($terms as $term) $results[] = ['id' => (string) $term->term_id, 'text' => (string) $term->name];
}
}
wp_send_json_success(['results' => $results]);
}
}
+7 -4
View File
@@ -81,10 +81,13 @@ final class CommonFields {
public static function palette(): array {
return [
'focus-color' => '主题色', 'jb-blue' => '渐变蓝', 'jb-cyan' => '渐变青',
'jb-green' => '渐变绿', 'jb-yellow' => '渐变黄', 'jb-red' => '渐变红',
'jb-pink' => '渐变粉', 'jb-purple' => '渐变紫', 'c-blue' => '蓝色',
'c-green' => '绿色', 'c-yellow' => '黄色', 'c-red' => '红色',
'focus-color' => '主题色', 'key-color' => '主要文字', 'muted-color' => '次要文字',
'c-red' => '红色', 'c-red-2' => '深红', 'c-yellow' => '黄色', 'c-yellow-2' => '深黄',
'c-cyan' => '青色', 'c-blue' => '蓝色', 'c-blue-2' => '深蓝', 'c-green' => '绿色', 'c-green-2' => '深绿',
'c-purple' => '紫色', 'c-purple-2' => '深紫', 'b-red' => '红色背景', 'b-yellow' => '黄色背景',
'b-cyan' => '青色背景', 'b-blue' => '蓝色背景', 'b-green' => '绿色背景', 'b-purple' => '紫色背景', 'b-black' => '黑色背景',
'jb-blue' => '渐变蓝', 'jb-cyan' => '渐变青', 'jb-green' => '渐变绿', 'jb-yellow' => '渐变黄',
'jb-red' => '渐变红', 'jb-pink' => '渐变粉', 'jb-purple' => '渐变紫', 'jb-vip1' => '会员金', 'jb-vip2' => '会员黑',
];
}
}
+18 -1
View File
@@ -84,6 +84,9 @@ final class FieldSchema {
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_textarea_field((string) $value);
case 'text':
default:
if (($field['sanitize'] ?? '') === 'url') {
return esc_url_raw((string) $value);
}
if (($field['sanitize'] ?? '') === 'ids') {
return implode(',', array_values(array_unique(array_filter(array_map('absint', preg_split('/[,,\s]+/', (string) $value))))));
}
@@ -122,7 +125,21 @@ final class FieldSchema {
}
private static function choice(array $field, $value) {
$options = (array) ($field['options'] ?? []);
$raw_options = $field['options'] ?? [];
// Reference-theme AJAX selects declare an option source (for example
// `post` or `categories`) instead of an inline option map. Preserve the
// selected IDs until the remote picker resolves their labels.
if (!is_array($raw_options)) {
if (!empty($field['multiple'])) {
$values = is_array($value) ? $value : preg_split('/[,,\s]+/', (string) $value, -1, PREG_SPLIT_NO_EMPTY);
return array_values(array_unique(array_filter(array_map(static function ($item): string {
$item = trim((string) $item);
return preg_match('/^\d+$/', $item) ? (string) (int) $item : '';
}, $values ?: []))));
}
return sanitize_text_field((string) $value);
}
$options = $raw_options;
if (!empty($field['multiple'])) {
$values = is_array($value) ? $value : [];
return array_values(array_filter(array_map('strval', $values), static fn($item) => array_key_exists($item, $options)));
+35
View File
@@ -128,6 +128,10 @@ final class FormRenderer {
private static function select(string $id, string $name, array $field, $value): void {
$multiple = !empty($field['multiple']);
if (!is_array($field['options'] ?? [])) {
self::remote_select($id, $name, $field, $value);
return;
}
echo '<select class="widefat" id="' . esc_attr($id) . '" name="' . esc_attr($name) . ($multiple ? '[]' : '') . '"' . ($multiple ? ' multiple' : '') . '>';
$values = $multiple ? array_map('strval', (array) $value) : [(string) $value];
foreach ((array) ($field['options'] ?? []) as $key => $label) {
@@ -136,6 +140,37 @@ final class FormRenderer {
echo '</select>';
}
private static function remote_select(string $id, string $name, array $field, $value): void {
$values = array_values(array_unique(array_filter(array_map('absint', (array) $value))));
$options = self::remote_selected_options($field, $values);
$source = (string) ($field['options'] ?? '');
$query_args = wp_json_encode((array) ($field['query_args'] ?? []));
$min_length = max(0, (int) ($field['settings']['min_length'] ?? 0));
$placeholder = (string) ($field['placeholder'] ?? __('输入关键词以搜索', 'aksrt'));
echo '<select class="widefat aksrt-widget-remote-select" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '[]" multiple data-option-source="' . esc_attr($source) . '" data-query-args="' . esc_attr((string) $query_args) . '" data-min-length="' . $min_length . '" data-placeholder="' . esc_attr($placeholder) . '">';
foreach ($options as $option_id => $label) {
echo '<option value="' . esc_attr((string) $option_id) . '" selected>' . esc_html($label) . '</option>';
}
echo '</select>';
}
private static function remote_selected_options(array $field, array $values): array {
$options = [];
$source = (string) ($field['options'] ?? '');
$query_args = (array) ($field['query_args'] ?? []);
foreach ($values as $value) {
$label = '';
if ($source === 'post' && function_exists('get_the_title')) {
$label = (string) get_the_title($value);
} elseif ($source === 'categories' && function_exists('get_term')) {
$term = get_term($value, sanitize_key((string) ($query_args['taxonomy'] ?? '')));
if ($term && !is_wp_error($term)) $label = (string) $term->name;
}
$options[$value] = $label !== '' ? $label : '#' . $value;
}
return $options;
}
private static function checkboxes(string $id, string $name, array $field, $value): void {
$options = (array) ($field['options'] ?? []);
if (!$options) {
+1 -1
View File
@@ -23,7 +23,7 @@ final class FrontendRenderer {
}
AssetManager::enqueue_for($definition);
if (in_array($definition->slug(), ['tab_posts', 'slider', 'slider_media_combo', 'icon_cards', 'icon_cover_cards'], true)) {
if (in_array($definition->slug(), ['tab_posts', 'slider', 'slider_media_combo', 'icon_cards', 'icon_cover_cards', 'buttons', 'notice'], true)) {
echo (string) ($args['before_widget'] ?? '');
echo self::reference_title($instance);
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+105 -14
View File
@@ -3,6 +3,7 @@
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
use AKSRT\WidgetSystem\PostQuery;
defined('ABSPATH') || exit;
@@ -156,15 +157,35 @@ final class GeneralRenderer {
}
private static function render_buttons(array $i): string {
$html = '<div class="aksrt-widget-buttons is-' . esc_attr((string) $i['alignment']) . '" style="--aksrt-button-gap:' . (float) $i['gap'] . 'px">';
foreach ((array) $i['buttons'] as $button) {
$link = RendererHelpers::link((array) ($button['link'] ?? []));
if ($link['url'] === '') {
continue;
}
$html .= '<a class="aksrt-widget-button ' . esc_attr((string) ($button['color'] ?? 'jb-blue')) . '" href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . (!empty($button['icon']) ? '<i class="' . esc_attr((string) $button['icon']) . '" aria-hidden="true"></i>' : '') . '<span>' . esc_html((string) ($button['text'] ?? $link['text'])) . '</span></a>';
$padding_pc = in_array((string) ($i['pc_padding'] ?? 'large'), ['small', 'medium', 'large'], true) ? (string) $i['pc_padding'] : 'large';
$padding_m = in_array((string) ($i['m_padding'] ?? 'medium'), ['small', 'medium', 'large'], true) ? (string) $i['m_padding'] : 'medium';
$align = in_array((string) ($i['align'] ?? 'center'), ['left', 'center', 'right'], true) ? (string) $i['align'] : 'center';
$style = '';
foreach (['pc_width' => '--pc-width', 'm_width' => '--m-width', 'pc_gap' => '--pc-gap', 'm_gap' => '--m-gap'] as $key => $property) {
if (!empty($i[$key]) && is_numeric($i[$key])) $style .= $property . ':' . (float) $i[$key] . 'px;';
}
return $html . '</div>';
$button_class = 'but' . (!empty($i['radius']) ? ' radius' : '') . (!empty($i['hollow']) ? ' hollow' : '');
$buttons = '';
foreach ((array) ($i['items'] ?? []) as $index => $item) {
if (!is_array($item) || trim((string) ($item['text'] ?? '')) === '') continue;
$link = RendererHelpers::link((array) ($item['link'] ?? []));
$url = $link['url'] !== '' ? $link['url'] : 'javascript:void(0)';
if (!empty($i['go_link']) && $link['url'] !== '' && function_exists('aksrt_go_link')) $url = esc_url(aksrt_go_link($link['url'], true));
$target = $link['target'] === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
$color = sanitize_html_class((string) ($item['color'] ?? ''));
$item_class = trim($button_class . ' ' . $color);
$icon_left = !empty($item['icon_left']) ? self::icon_markup((string) $item['icon_left']) : '';
$icon_right = !empty($item['icon_right']) ? '<span class="ml6">' . self::icon_markup((string) $item['icon_right']) . '</span>' : '';
$animation = '';
$attr = '';
if (!empty($i['obs_animation'])) {
$animation = ' obs-animate ani-' . sanitize_html_class((string) $i['obs_animation']);
$attr = ' data-animation="' . esc_attr((string) $i['obs_animation']) . '" style="--delay:' . (float) $index * 0.15 . 's;"' . (!empty($i['animation_repeat']) ? ' data-animation-repeat="true"' : '');
}
$buttons .= '<div class="' . trim($animation) . '"' . $attr . '><a class="' . esc_attr($item_class) . '" href="' . $url . '"' . $target . '>' . $icon_left . wp_kses_post((string) $item['text']) . $icon_right . '</a></div>';
}
if ($buttons === '') return '';
return '<div class="widget-buttons mb20 pc-size-' . esc_attr($padding_pc) . ' m-size-' . esc_attr($padding_m) . '"><div class="widget-buttons-wrap flex hh align-' . esc_attr($align) . '" style="' . esc_attr($style) . '">' . $buttons . '</div></div>';
}
private static function render_layout_gap(array $i): string {
@@ -218,12 +239,82 @@ final class GeneralRenderer {
}
private static function render_notice(array $i): string {
$slides = '';
foreach ((array) $i['items'] as $item) {
$link = RendererHelpers::link((array) ($item['link'] ?? []));
$content = '<strong>' . esc_html((string) ($item['title'] ?? '')) . '</strong><span>' . esc_html((string) ($item['description'] ?? '')) . '</span>';
$slides .= '<div class="swiper-slide">' . ($link['url'] ? '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . $content . '</a>' : $content) . '</div>';
$source = (string) ($i['source'] ?? '');
$lists = (array) ($i['img_ids'] ?? []);
$posts_query = false;
if ($source === 'posts' && class_exists(PostQuery::class)) {
$query_args = array_merge((array) ($i['posts_opts'] ?? []), ['count' => (int) ($i['count'] ?? 8), 'paginate' => '']);
$posts_query = PostQuery::build($query_args, 1);
$lists = [];
if ($posts_query && method_exists($posts_query, 'have_posts')) {
while ($posts_query->have_posts()) {
$posts_query->the_post();
$subtitle = class_exists('AKSRT\\Core\\PostMetaBox') ? \AKSRT\Core\PostMetaBox::get_meta_value(get_the_ID(), 'subtitle', '') : '';
$title = trim(wp_strip_all_tags(get_the_title())) . trim(wp_strip_all_tags((string) $subtitle));
if (!empty($i['desc_s'])) {
$description = class_exists('AKSRT\\Core\\PostMetaBox') ? \AKSRT\Core\PostMetaBox::get_meta_value(get_the_ID(), 'description', '') : '';
if (trim((string) $description) === '') $description = get_the_excerpt();
$title .= ': ' . wp_html_excerpt(wp_strip_all_tags((string) $description), 160, '...');
}
$lists[] = ['title' => $title, 'href' => get_permalink()];
}
wp_reset_postdata();
}
} elseif ($source === 'forums' && function_exists('zib_bbs_get_posts_query') && (!function_exists('_pz') || _pz('bbs_s'))) {
$query_args = (array) ($i['forums_opts'] ?? []);
$query_args['plate'] = $query_args['plate'] ?? 0;
$query_args['paged_size'] = (int) ($i['count'] ?? 8);
$query_args['paged'] = 1;
$query_args['no_found_rows'] = true;
$posts_query = zib_bbs_get_posts_query($query_args);
$lists = [];
if ($posts_query && method_exists($posts_query, 'have_posts')) {
while ($posts_query->have_posts()) {
$posts_query->the_post();
$post_id = get_the_ID();
$subtitle = class_exists('AKSRT\\Core\\PostMetaBox') ? \AKSRT\Core\PostMetaBox::get_meta_value($post_id, 'subtitle', '') : '';
$title = trim(wp_strip_all_tags(get_the_title())) . trim(wp_strip_all_tags((string) $subtitle));
if (!empty($i['desc_s'])) {
$description = class_exists('AKSRT\\Core\\PostMetaBox') ? \AKSRT\Core\PostMetaBox::get_meta_value($post_id, 'description', '') : '';
if (trim((string) $description) === '') $description = get_the_excerpt();
$title .= ': ' . wp_html_excerpt(wp_strip_all_tags((string) $description), 160, '...');
}
$lists[] = ['title' => $title, 'href' => get_permalink()];
}
wp_reset_postdata();
}
}
return '<div class="aksrt-widget-notice"><i class="fa fas fa-bullhorn" aria-hidden="true"></i><div class="new-swiper aksrt-widget-swiper" data-aksrt-swiper="notice" data-direction="vertical" data-loop="true" data-autoplay="1" data-interval="' . ((int) $i['interval'] * 1000) . '"><div class="swiper-wrapper">' . $slides . '</div></div></div>';
$msg_icon = ($posts_query && !empty($i['msg_icon'])) ? self::notice_icon((string) $i['msg_icon']) : '';
$slides = '';
$blank = !empty($i['blank']) ? ' target="_blank" rel="noopener noreferrer"' : '';
foreach ($lists as $notice) {
if (!is_array($notice) || trim((string) ($notice['title'] ?? '')) === '') continue;
$href = !empty($notice['href']) ? esc_url((string) $notice['href']) : 'javascript:void(0);';
$icon = !empty($notice['icon']) ? self::notice_icon((string) $notice['icon']) : '';
$title = wp_kses_post((string) ($notice['title'] ?? ''));
$slides .= '<div class="swiper-slide notice-slide"><a class="flex ac"' . $blank . ' href="' . $href . '">' . $icon . '<span class="ml6 text-ellipsis"><span class="">' . $title . '</span></span></a></div>';
}
$class = 'swiper-bulletin mb20 ' . sanitize_html_class((string) ($i['color'] ?? 'c-blue')) . (!empty($i['radius']) ? ' radius' : ' main-radius') . ($posts_query ? ' has-posts flex ac' : '');
$html = '<div class="new-swiper" data-interval="' . ((int) ($i['interval'] ?? 3) * 1000) . '" data-direction="vertical" data-loop="true" data-autoplay="1"><div class="swiper-wrapper">' . $slides . '</div></div>';
return '<div class="' . esc_attr($class) . '">' . $msg_icon . $html . '</div>';
}
private static function notice_icon(string $icon): string {
return '<span class="relative bulletin-icon flex jc shrink0">' . self::icon_markup($icon) . '</span>';
}
private static function icon_markup(string $icon): string {
$icon = trim($icon);
if ($icon === '') return '';
if (str_starts_with($icon, 'customsvg-')) {
return wp_kses(substr($icon, 10), [
'svg' => ['class' => true, 'viewbox' => true, 'viewBox' => true, 'width' => true, 'height' => true, 'fill' => true, 'xmlns' => true, 'aria-hidden' => true],
'use' => ['href' => true, 'xlink:href' => true],
'path' => ['d' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true],
]);
}
if (str_starts_with($icon, 'zibsvg-')) return '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-' . esc_attr(substr($icon, 7)) . '"></use></svg>';
if (!str_contains($icon, 'fa ') && str_contains($icon, 'fa-')) $icon = 'fa ' . $icon;
return '<i class="' . esc_attr($icon) . '" aria-hidden="true"></i>';
}
}
+3 -2
View File
@@ -70,8 +70,9 @@ final class WidgetCatalog {
'renderer' => $slug,
'size' => $size,
'common' => [
'title' => !in_array($slug, ['post_pay', 'layout_gap', 'slider', 'slider_media_combo'], true),
'affix' => $size === 'sidebar',
'title' => !in_array($slug, ['post_pay', 'layout_gap', 'slider', 'slider_media_combo', 'buttons'], true),
'affix' => $size === 'sidebar' || in_array($slug, ['buttons', 'notice'], true),
'animation' => $slug !== 'buttons',
],
'fields' => WidgetSchemas::for($slug),
'assets' => self::assets($slug),
+91 -10
View File
@@ -89,8 +89,29 @@ final class WidgetSchemas {
];
case 'notice':
return [
['id' => 'interval', 'type' => 'spinner', 'label' => '滚动间隔(秒)', 'default' => 4, 'min' => 1, 'max' => 60],
['id' => 'items', 'type' => 'group', 'label' => '公告内容', 'max_items' => 30, 'fields' => self::card_fields(true)],
['id' => 'blank', 'type' => 'switcher', 'label' => '链接新窗口打开', 'default' => false],
['id' => 'radius', 'type' => 'switcher', 'label' => '两边显示为圆形', 'default' => false],
['id' => 'interval', 'type' => 'spinner', 'label' => '滚动间隔', 'default' => 3, 'min' => 1, 'max' => 10, 'step' => 1, 'unit' => '秒'],
['id' => 'color', 'type' => 'palette', 'label' => '颜色', 'default' => 'c-blue', 'options' => self::zib_palette()],
['id' => 'source', 'type' => 'radio', 'label' => '内容来源', 'default' => '', 'options' => ['' => '自定义', 'posts' => '文章', 'forums' => '论坛帖子']],
['id' => 'msg_icon', 'type' => 'icon', 'label' => '消息图标', 'default' => 'zibsvg-hot', 'dependency' => ['source', '!=', '']],
['id' => 'count', 'type' => 'spinner', 'label' => '获取帖子或文章数量', 'default' => 8, 'min' => 1, 'max' => 50, 'dependency' => ['source', '!=', '']],
['id' => 'desc_s', 'type' => 'switcher', 'label' => '显示简介(关闭则只显示标题)', 'default' => false],
['id' => 'forums_opts', 'type' => 'fieldset', 'label' => '论坛帖子筛选', 'default' => [], 'dependency' => ['source', '==', 'forums'], 'fields' => self::notice_forum_fields()],
['id' => 'posts_opts', 'type' => 'fieldset', 'label' => '文章筛选', 'default' => [], 'dependency' => ['source', '==', 'posts'], 'fields' => self::notice_post_fields()],
[
'id' => 'img_ids', 'type' => 'group', 'label' => '消息列表', 'max_items' => 50,
'dependency' => ['source', '==', ''],
'default' => [
['title' => '子比主题,更优雅的Wordpress主题', 'icon' => 'zibsvg-hot', 'href' => 'https://zibll.com'],
['title' => '更优雅的WordPress网站主题:子比主题!全面开启', 'icon' => 'zibsvg-hot', 'href' => 'https://zibll.com'],
],
'fields' => [
['id' => 'title', 'type' => 'text', 'label' => '消息内容', 'default' => '', 'allow_html' => true],
['id' => 'icon', 'type' => 'icon', 'label' => '图标', 'default' => 'fa-home'],
['id' => 'href', 'type' => 'text', 'label' => '跳转链接', 'default' => '', 'sanitize' => 'url'],
],
],
];
case 'text_title':
return [
@@ -100,9 +121,25 @@ final class WidgetSchemas {
];
case 'buttons':
return [
['id' => 'alignment', 'type' => 'button_set', 'label' => '对齐', 'default' => 'center', 'options' => ['left' => '左', 'center' => '中', 'right' => '右']],
['id' => 'gap', 'type' => 'slider', 'label' => '按钮间距', 'default' => 10, 'min' => 0, 'max' => 60],
['id' => 'buttons', 'type' => 'group', 'label' => '按钮', 'max_items' => 12, 'fields' => self::button_fields()],
['id' => 'obs_animation', 'type' => 'select', 'label' => '入场动画', 'default' => '', 'options' => self::animation_options()],
['id' => 'animation_repeat', 'type' => 'switcher', 'label' => '重复动画', 'default' => false, 'dependency' => ['obs_animation', '!=', '']],
['id' => 'pc_width', 'type' => 'spinner', 'label' => '按钮宽度(PC端宽度,0为自动)', 'default' => 0, 'min' => 0, 'max' => 500, 'step' => 4, 'unit' => 'px'],
['id' => 'm_width', 'type' => 'spinner', 'label' => '移动端宽度(0为自动)', 'default' => 0, 'min' => 0, 'max' => 500, 'step' => 4, 'unit' => 'px'],
['id' => 'pc_padding', 'type' => 'radio', 'label' => 'PC端按钮尺寸', 'default' => 'large', 'options' => ['small' => '小尺寸', 'medium' => '中尺寸', 'large' => '大尺寸']],
['id' => 'm_padding', 'type' => 'radio', 'label' => '移动端按钮尺寸', 'default' => 'medium', 'options' => ['small' => '小尺寸', 'medium' => '中尺寸', 'large' => '大尺寸']],
['id' => 'pc_gap', 'type' => 'spinner', 'label' => 'PC端按钮间距', 'default' => 12, 'min' => 0, 'max' => 60, 'step' => 4, 'unit' => 'px'],
['id' => 'm_gap', 'type' => 'spinner', 'label' => '移动端按钮间距', 'default' => 6, 'min' => 0, 'max' => 60, 'step' => 4, 'unit' => 'px'],
['id' => 'align', 'type' => 'radio', 'label' => '对齐方式', 'default' => 'center', 'options' => ['left' => '左对齐', 'center' => '居中', 'right' => '右对齐']],
['id' => 'radius', 'type' => 'switcher', 'label' => '两端圆角', 'default' => false],
['id' => 'hollow', 'type' => 'switcher', 'label' => '空心样式', 'default' => false],
['id' => 'go_link', 'type' => 'switcher', 'label' => '外链重定向', 'default' => true],
['id' => 'items', 'type' => 'group', 'label' => '添加按钮', 'max_items' => 50, 'default' => [], 'fields' => [
['id' => 'text', 'type' => 'text', 'label' => '按钮文字', 'default' => '', 'allow_html' => true],
['id' => 'icon_left', 'type' => 'icon', 'label' => '按钮前图标', 'default' => ''],
['id' => 'icon_right', 'type' => 'icon', 'label' => '按钮后图标', 'default' => ''],
['id' => 'link', 'type' => 'link', 'label' => '按钮链接', 'default' => []],
['id' => 'color', 'type' => 'palette', 'label' => '按钮颜色', 'default' => '', 'options' => self::button_palette()],
]],
];
case 'partners':
return [
@@ -407,12 +444,56 @@ final class WidgetSchemas {
return $fields;
}
private static function button_fields(): array {
private static function animation_options(): array {
return ['' => '无动画', 'fade' => '淡入', 'slideup' => '从下往上滑出', 'slidedown' => '从上往下滑出', 'slideright' => '从左往右滑出', 'slideleft' => '从右往左滑出', 'zoomin' => '由小变大', 'zoomout' => '由大变小'];
}
private static function button_palette(): array {
return ['' => '默认'] + self::zib_palette();
}
private static function zib_palette(): array {
return [
['id' => 'text', 'type' => 'text', 'label' => '文字', 'default' => '按钮'],
['id' => 'icon', 'type' => 'icon', 'label' => '图标', 'default' => ''],
['id' => 'color', 'type' => 'palette', 'label' => '颜色', 'default' => 'jb-blue', 'options' => CommonFields::palette()],
['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []],
'c-red' => '红色', 'c-red-2' => '深红', 'c-yellow' => '黄色', 'c-yellow-2' => '深黄',
'c-cyan' => '青色', 'c-blue' => '蓝色', 'c-blue-2' => '深蓝', 'c-green' => '绿色',
'c-green-2' => '深绿', 'c-purple' => '紫色', 'c-purple-2' => '深紫',
'b-red' => '红色背景', 'b-yellow' => '黄色背景', 'b-cyan' => '青色背景', 'b-blue' => '蓝色背景',
'b-green' => '绿色背景', 'b-purple' => '紫色背景', 'b-black' => '黑色背景',
'jb-red' => '渐变红', 'jb-pink' => '渐变粉', 'jb-yellow' => '渐变黄', 'jb-cyan' => '渐变青',
'jb-blue' => '渐变蓝', 'jb-green' => '渐变绿', 'jb-purple' => '渐变紫', 'jb-vip1' => '会员金', 'jb-vip2' => '会员黑',
];
}
private static function notice_post_fields(): array {
return [
['id' => 'cat', 'type' => 'text', 'label' => '分类限制', 'default' => '', 'sanitize' => 'reference_ids'],
['id' => 'topics', 'type' => 'text', 'label' => '专题限制', 'default' => '', 'sanitize' => 'reference_ids'],
['id' => 'zibpay_type', 'type' => 'checkbox', 'label' => '商品类型筛选', 'default' => [], 'options' => ['1' => '付费阅读', '2' => '付费下载', '5' => '付费图片', '6' => '付费视频']],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => '', 'options' => self::posts_orderby()],
['id' => 'order', 'type' => 'radio', 'label' => '顺序', 'default' => 'desc', 'options' => ['desc' => '升序', 'asc' => '降序']],
];
}
private static function notice_forum_fields(): array {
return [
['id' => 'plate', 'type' => 'select', 'label' => '包含版块', 'default' => [], 'options' => 'post', 'query_args' => ['post_type' => 'plate'], 'ajax' => true, 'settings' => ['min_length' => 2], 'placeholder' => '输入关键词以搜索', 'chosen' => true, 'multiple' => true, 'sortable' => true],
['id' => 'plate_exclude', 'type' => 'select', 'label' => '排除版块', 'default' => [], 'options' => 'post', 'query_args' => ['post_type' => 'plate'], 'ajax' => true, 'settings' => ['min_length' => 2], 'placeholder' => '输入关键词以搜索版块分类', 'chosen' => true, 'multiple' => true, 'sortable' => true, 'dependency' => ['plate', '==', '', '', 'visible']],
['id' => 'topic', 'type' => 'select', 'label' => '包含话题', 'default' => [], 'options' => 'categories', 'query_args' => ['taxonomy' => 'forum_topic'], 'placeholder' => '输入关键词以搜索', 'chosen' => true, 'multiple' => true, 'sortable' => true, 'ajax' => true, 'settings' => ['min_length' => 2]],
['id' => 'tag', 'type' => 'select', 'label' => '包含标签', 'default' => [], 'options' => 'categories', 'query_args' => ['taxonomy' => 'forum_tag'], 'placeholder' => '输入关键词以搜索', 'chosen' => true, 'multiple' => true, 'sortable' => true, 'ajax' => true, 'settings' => ['min_length' => 2]],
['id' => 'bbs_type', 'type' => 'checkbox', 'label' => '类型筛选', 'default' => [], 'options' => ['' => '标准', 'question' => '提问']],
['id' => 'allow_view', 'type' => 'checkbox', 'label' => '阅读权限筛选', 'default' => [], 'options' => ['signin' => '登录后查看', 'comment' => '评论后查看', 'pay' => '付费查看', 'points' => '支付积分查看', 'roles' => '部分用户可查看']],
['id' => 'filter', 'type' => 'checkbox', 'label' => '其它筛选', 'default' => [], 'options' => ['topping' => '置顶帖子', 'vote' => '投票帖子', 'essence' => '精华帖子', 'question_status' => '提问已解决', 'is_hot' => '热门帖子']],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => 'date', 'options' => self::notice_forum_orderby()],
['id' => 'order', 'type' => 'radio', 'label' => '顺序', 'default' => 'desc', 'options' => ['desc' => '升序', 'asc' => '降序']],
];
}
private static function notice_forum_orderby(): array {
return [
'name' => '标题名称', 'date' => '最新发布', 'modified' => '最近更新', 'last_reply' => '最新回复',
'dynamic_score' => '推荐指数', 'views' => '最多查看', 'score' => '评分最高', 'comment_count' => '最多回复',
'favorite_count' => '最多收藏', 'zibpay_price' => '售价金额', 'zibpay_points_price' => '积分金额',
'sales_volume' => '销售数量', 'rand' => '随机',
];
}
@@ -42,6 +42,47 @@ test('icon card widgets expose Zibll vertical and horizontal contracts', () => {
assert.match(helper, /wp_kses_post\(\$desc\)/);
});
test('button group uses the complete Zibll schema, DOM and responsive sizing contract', () => {
const schema = read('src/WidgetSystem/WidgetSchemas.php');
const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const css = read('assets/widgets/frontend.css');
for (const field of ['obs_animation', 'animation_repeat', 'pc_width', 'm_width', 'pc_padding', 'm_padding', 'pc_gap', 'm_gap', "'id' => 'align'", "'id' => 'radius'", "'id' => 'hollow'", 'go_link', 'icon_left', 'icon_right']) {
assert.match(schema, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
for (const token of ['widget-buttons mb20 pc-size-', 'm-size-', 'widget-buttons-wrap flex hh align-', 'class="ml6"', 'obs-animate ani-']) {
assert.match(renderer, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.doesNotMatch(renderer, /aksrt-widget-buttons/);
assert.match(schema, /'id' => 'text'[^\n]+allow_html/);
assert.match(renderer, /wp_kses_post\(\(string\) \$item\['text'\]\)/);
assert.match(css, /\.widget-buttons\.pc-size-large \.but\s*\{\s*padding:\s*12px 24px/);
assert.match(css, /\.widget-buttons\.m-size-medium \.but\s*\{\s*padding:\s*8px 16px/);
});
test('vertical notice uses Zibll sources, searchable forum fields and swiper sizing', () => {
const schema = read('src/WidgetSystem/WidgetSchemas.php');
const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const form = read('src/WidgetSystem/FormRenderer.php');
const css = read('assets/widgets/frontend.css');
for (const field of ["'id' => 'blank'", "'id' => 'radius'", "'id' => 'interval'", "'id' => 'color'", "'id' => 'source'", 'msg_icon', "'id' => 'count'", 'desc_s', 'forums_opts', 'posts_opts', 'img_ids']) {
assert.match(schema, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
for (const field of ['plate', 'plate_exclude', 'topic', 'tag']) {
assert.match(schema, new RegExp(`'id' => '${field}', 'type' => 'select'[^\n]+multiple`));
}
assert.match(form, /aksrt-widget-remote-select/);
for (const token of ['swiper-bulletin mb20 ', 'notice-slide', 'new-swiper', 'data-direction="vertical"', 'data-loop="true"', 'data-autoplay="1"', 'bulletin-icon flex jc shrink0']) {
assert.match(renderer, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.doesNotMatch(renderer, /aksrt-widget-notice/);
assert.match(schema, /'id' => 'title'[^\n]+allow_html/);
assert.match(renderer, /wp_html_excerpt\([^\n]+160, '\.\.\.'/);
assert.match(renderer, /str_starts_with\(\$icon, 'customsvg-'\)/);
assert.match(css, /\.swiper-bulletin \.new-swiper\s*\{[^}]*height:\s*2\.4em/s);
assert.match(css, /\.swiper-bulletin \.new-swiper\s*\{[^}]*line-height:\s*2\.4em/s);
assert.match(css, /\.bulletin-icon\s*\{[^}]*width:\s*26px[^}]*height:\s*26px[^}]*font-size:\s*18px/s);
});
test('admin core supports composite dependencies and nested repeater renumbering', () => {
const core = require(path.join(root, 'assets/widgets/admin-core.js'));
assert.equal(core.dependencyVisible([['direction', '!=', 'vertical'], ['scale_height', '!=', '']], { direction: 'horizontal', scale_height: '1' }), true);