feat: rewrite schema-driven widget system

This commit is contained in:
2026-08-17 17:29:54 +08:00
parent 6c6c462df8
commit ad99ab53ce
28 changed files with 3489 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Lazy widget form endpoint and admin-only assets.
*/
final class AdminFormController {
public const AJAX_ACTION = 'aksrt_widget_form';
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('admin_enqueue_scripts', [self::class, 'enqueue']);
add_action('customize_controls_enqueue_scripts', [self::class, 'enqueue']);
}
public static function enqueue(): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || !in_array($screen->id, ['widgets', 'customize'], true)) {
return;
}
wp_enqueue_media();
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('jquery-ui-sortable');
wp_enqueue_style('aksrt-widget-admin', AKSRT_URI . '/assets/widgets/admin.css', ['wp-color-picker'], AKSRT_VERSION);
wp_enqueue_script('aksrt-widget-admin', AKSRT_URI . '/assets/widgets/admin.js', ['jquery', 'jquery-ui-sortable', 'wp-color-picker'], AKSRT_VERSION, true);
wp_localize_script('aksrt-widget-admin', 'aksrtWidgetAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'action' => self::AJAX_ACTION,
'nonce' => wp_create_nonce(self::NONCE_ACTION),
'i18n' => [
'loading' => __('正在加载模块配置…', 'aksrt'),
'error' => __('加载失败,请重试。', 'aksrt'),
],
]);
}
public static function ajax_form(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => __('权限不足。', 'aksrt')], 403);
}
$id_base = isset($_POST['id_base']) ? sanitize_key(wp_unslash($_POST['id_base'])) : '';
$number = isset($_POST['widget_number']) ? absint($_POST['widget_number']) : 0;
$definition = WidgetRegistry::get_by_id_base($id_base);
if (!$definition || $number < 1) {
wp_send_json_error(['message' => __('无效的小工具实例。', 'aksrt')], 400);
}
$widget = new SchemaWidget($definition);
$widget->_set($number);
$settings = $widget->get_settings();
$instance = isset($settings[$number]) && is_array($settings[$number]) ? $settings[$number] : [];
ob_start();
$widget->render_full_form($instance);
$html = (string) ob_get_clean();
wp_send_json_success(['html' => $html]);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/** Registers the isolated widget bundle and optional interaction libraries. */
final class AssetManager {
private static bool $registered = false;
private static bool $configured = false;
public static function enqueue_for(WidgetDefinition $definition): void {
self::register();
foreach ($definition->assets() as $asset) {
self::enqueue_optional($asset);
}
wp_enqueue_style('aksrt-widgets');
wp_enqueue_script('aksrt-widgets');
if (!self::$configured) {
$config = [
'restUrl' => esc_url_raw(rest_url('aksrt/v1/widgets/')),
'quoteUrl' => esc_url_raw(rest_url('aksrt/v1/widgets/quote')),
'i18n' => [
'loading' => __('正在加载…', 'aksrt'),
'error' => __('加载失败,请重试。', 'aksrt'),
'retry' => __('重试', 'aksrt'),
],
];
wp_add_inline_script('aksrt-widgets', 'window.aksrtWidgets=' . wp_json_encode($config) . ';', 'before');
self::$configured = true;
}
}
private static function register(): void {
if (self::$registered) {
return;
}
$root = get_template_directory();
$uri = get_template_directory_uri();
$css = $root . '/assets/widgets/frontend.css';
$js = $root . '/assets/widgets/frontend.js';
wp_register_style('aksrt-widgets', $uri . '/assets/widgets/frontend.css', [], is_file($css) ? (string) filemtime($css) : null);
wp_register_script('aksrt-widgets', $uri . '/assets/widgets/frontend.js', [], is_file($js) ? (string) filemtime($js) : null, true);
if (!wp_style_is('aksrt-swiper', 'registered')) {
wp_register_style('aksrt-swiper', $uri . '/assets/css/frontend/swiper-bundle.min.css', [], '11.0');
}
if (!wp_script_is('aksrt-swiper', 'registered')) {
wp_register_script('aksrt-swiper', $uri . '/assets/js/swiper-bundle.min.js', [], '11.0', true);
}
wp_register_style('aksrt-widget-dplayer', 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css', [], '1.27.1');
wp_register_script('aksrt-widget-hls', 'https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js', [], '1.5.18', true);
wp_register_script('aksrt-widget-flv', 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js', [], '1.6.2', true);
wp_register_script('aksrt-widget-dash', 'https://cdn.jsdelivr.net/npm/dashjs@4.7.4/dist/dash.all.min.js', [], '4.7.4', true);
wp_register_script('aksrt-widget-dplayer', 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js', ['aksrt-widget-hls', 'aksrt-widget-flv', 'aksrt-widget-dash'], '1.27.1', true);
wp_register_script('aksrt-widget-tilt', 'https://cdn.jsdelivr.net/npm/vanilla-tilt@1.8.1/dist/vanilla-tilt.min.js', [], '1.8.1', true);
self::$registered = true;
}
private static function enqueue_optional(string $asset): void {
if ($asset === 'swiper') {
wp_enqueue_style('aksrt-swiper');
wp_enqueue_script('aksrt-swiper');
} elseif ($asset === 'dplayer') {
wp_enqueue_style('aksrt-widget-dplayer');
wp_enqueue_script('aksrt-widget-dplayer');
} elseif (in_array($asset, ['hls', 'flv', 'dash'], true)) {
wp_enqueue_script('aksrt-widget-' . $asset);
} elseif ($asset === 'tilt') {
wp_enqueue_script('aksrt-widget-tilt');
}
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Reference-equivalent fields shared by all modern widgets.
*/
final class CommonFields {
public static function for_definition(array $flags): array {
$fields = [];
if (!empty($flags['title'])) {
$fields = array_merge($fields, self::title_fields());
}
if (!empty($flags['affix'])) {
$fields[] = ['id' => 'sidebar_affix', 'type' => 'switcher', 'label' => '侧栏随动', 'description' => '仅在侧边栏有效', 'default' => false];
}
if (!empty($flags['visibility'])) {
$fields = array_merge($fields, self::visibility_fields());
}
if (!empty($flags['animation'])) {
$fields = array_merge($fields, self::animation_fields());
}
if (!empty($flags['layout'])) {
$fields = array_merge($fields, self::layout_fields());
}
return $fields;
}
private static function title_fields(): array {
$link = static fn(string $id, string $label) => ['id' => $id, 'type' => 'link', 'label' => $label, 'default' => ['url' => '', 'text' => '', 'target' => '']];
return [
['id' => 'title', 'type' => 'textarea', 'label' => '模块标题', 'default' => ''],
['id' => 'subtitle', 'type' => 'textarea', 'label' => '副标题', 'default' => '', 'dependency' => ['title', '!=', '']],
['id' => 'title_style', 'type' => 'radio', 'label' => '标题样式', 'default' => '', 'options' => ['' => '简约', 'big' => '居中大标题'], 'dependency' => ['title', '!=', '']],
['id' => 'title_highlight', 'type' => 'text', 'label' => '标题选字高亮', 'default' => '', 'dependency' => ['title', '!=', '']],
['id' => 'title_highlight_color', 'type' => 'palette', 'label' => '高亮颜色', 'default' => 'focus-color', 'options' => self::palette()],
$link('title_link', '标题链接'),
$link('title_link2', '标题链接 2'),
$link('title_link3', '标题链接 3'),
[
'id' => 'title_link_style', 'type' => 'accordion', 'label' => '标题链接样式',
'fields' => [
['id' => 'color_1', 'type' => 'palette', 'label' => '链接 1 颜色', 'default' => 'jb-blue', 'options' => self::palette()],
['id' => 'color_2', 'type' => 'palette', 'label' => '链接 2 颜色', 'default' => 'jb-yellow', 'options' => self::palette()],
['id' => 'color_3', 'type' => 'palette', 'label' => '链接 3 颜色', 'default' => 'jb-green', 'options' => self::palette()],
],
],
];
}
private static function visibility_fields(): array {
return [
['id' => 'show_type', 'type' => 'radio', 'label' => '显示规则', 'default' => 'all', 'options' => ['all' => '全部显示', 'only_pc' => '仅 PC 端', 'only_sm' => '仅移动端']],
['id' => 'show_id_type', 'type' => 'radio', 'label' => '按 ID 显示或隐藏', 'default' => '', 'options' => ['' => '不限制', 'show' => '仅指定 ID 显示', 'hide' => '指定 ID 隐藏']],
['id' => 'show_ids', 'type' => 'text', 'label' => '文章 ID', 'description' => '多个 ID 使用逗号分隔', 'default' => '', 'sanitize' => 'ids', 'dependency' => ['show_id_type', '!=', '']],
];
}
private static function animation_fields(): array {
return [
['id' => 'animation_in', 'type' => 'select', 'label' => '模块入场动画', 'default' => '', 'options' => ['' => '无动画', 'fade' => '淡入', 'slideup' => '从下往上', 'slidedown' => '从上往下', 'slideright' => '从左往右', 'slideleft' => '从右往左', 'zoomin' => '由小变大', 'zoomout' => '由大变小']],
['id' => 'animation_repeat', 'type' => 'switcher', 'label' => '重复动画', 'default' => false],
];
}
private static function layout_fields(): array {
return [
['id' => 'layout_pin_pc', 'type' => 'spacing', 'label' => '模块额外间距(PC)', 'default' => ['top' => '', 'right' => '', 'bottom' => '', 'left' => '']],
['id' => 'layout_pin_m', 'type' => 'spacing', 'label' => '模块额外间距(移动端)', 'default' => ['top' => '', 'right' => '', 'bottom' => '', 'left' => '']],
[
'id' => 'layout_bg', 'type' => 'accordion', 'label' => '区块背景',
'fields' => [
['id' => 'light', 'type' => 'background', 'label' => '日间模式背景', 'default' => []],
['id' => 'dark', 'type' => 'background', 'label' => '夜间模式背景', 'default' => []],
],
],
];
}
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' => '红色',
];
}
}
+195
View File
@@ -0,0 +1,195 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Field schema defaults and recursive value normalization.
*/
final class FieldSchema {
private const DISPLAY_ONLY = ['content', 'subheading', 'submessage'];
public static function supported_types(): array {
return [
'text', 'textarea', 'number', 'spinner', 'slider', 'switcher', 'radio',
'button_set', 'select', 'checkbox', 'color', 'palette', 'icon', 'upload',
'link', 'spacing', 'background', 'group', 'fieldset', 'accordion',
'content', 'subheading', 'submessage',
];
}
public static function defaults(array $fields): array {
$defaults = [];
foreach ($fields as $field) {
$id = (string) ($field['id'] ?? '');
$type = (string) ($field['type'] ?? 'text');
if ($id === '' || in_array($type, self::DISPLAY_ONLY, true)) {
continue;
}
$defaults[$id] = self::normalize_value($field, $field['default'] ?? self::empty_value($type));
}
return $defaults;
}
public static function normalize(array $fields, array $input): array {
$output = [];
foreach ($fields as $field) {
$id = (string) ($field['id'] ?? '');
$type = (string) ($field['type'] ?? 'text');
if ($id === '' || in_array($type, self::DISPLAY_ONLY, true)) {
continue;
}
$value = array_key_exists($id, $input) ? $input[$id] : ($field['default'] ?? self::empty_value($type));
$output[$id] = self::normalize_value($field, $value);
}
return $output;
}
public static function normalize_value(array $field, $value) {
$type = (string) ($field['type'] ?? 'text');
switch ($type) {
case 'switcher':
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
case 'number':
case 'spinner':
case 'slider':
return self::number($field, $value);
case 'radio':
case 'button_set':
case 'select':
case 'palette':
return self::choice($field, $value);
case 'checkbox':
return self::checkbox($field, $value);
case 'link':
return self::link($value);
case 'spacing':
return self::spacing($value);
case 'background':
return self::background($value);
case 'group':
return self::group($field, $value);
case 'fieldset':
case 'accordion':
return self::nested($field, $value);
case 'upload':
return esc_url_raw((string) $value);
case 'color':
return self::color((string) $value);
case 'icon':
return sanitize_text_field((string) $value);
case 'textarea':
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_textarea_field((string) $value);
case 'text':
default:
if (($field['sanitize'] ?? '') === 'ids') {
return implode(',', array_values(array_unique(array_filter(array_map('absint', preg_split('/[,,\s]+/', (string) $value))))));
}
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_text_field((string) $value);
}
}
private static function empty_value(string $type) {
if ($type === 'switcher') {
return false;
}
if (in_array($type, ['group', 'checkbox'], true)) {
return [];
}
if (in_array($type, ['link', 'spacing', 'background', 'fieldset', 'accordion'], true)) {
return [];
}
return '';
}
private static function number(array $field, $value) {
$number = is_numeric($value) ? (float) $value : (float) ($field['default'] ?? 0);
if (isset($field['min'])) {
$number = max((float) $field['min'], $number);
}
if (isset($field['max'])) {
$number = min((float) $field['max'], $number);
}
$step = (float) ($field['step'] ?? 1);
return $step >= 1 ? (int) round($number) : $number;
}
private static function choice(array $field, $value) {
$options = (array) ($field['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)));
}
$key = (string) $value;
return array_key_exists($key, $options) ? $key : (string) ($field['default'] ?? array_key_first($options) ?? '');
}
private static function checkbox(array $field, $value) {
$options = (array) ($field['options'] ?? []);
if (!$options) {
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
$values = is_array($value) ? $value : [];
return array_values(array_filter(array_map('strval', $values), static fn($item) => array_key_exists($item, $options)));
}
private static function link($value): array {
$value = is_array($value) ? $value : [];
return [
'url' => esc_url_raw((string) ($value['url'] ?? '')),
'text' => sanitize_text_field((string) ($value['text'] ?? '')),
'target' => ($value['target'] ?? '') === '_blank' ? '_blank' : '',
];
}
private static function spacing($value): array {
$value = is_array($value) ? $value : [];
$result = [];
foreach (['top', 'right', 'bottom', 'left'] as $edge) {
$result[$edge] = isset($value[$edge]) && is_numeric($value[$edge]) ? (int) $value[$edge] : '';
}
return $result;
}
private static function background($value): array {
$value = is_array($value) ? $value : [];
return [
'color' => self::color((string) ($value['color'] ?? '')),
'image' => esc_url_raw((string) ($value['image'] ?? '')),
'position' => self::allow((string) ($value['position'] ?? 'center center'), ['center center', 'top center', 'bottom center', 'center left', 'center right'], 'center center'),
'repeat' => self::allow((string) ($value['repeat'] ?? 'no-repeat'), ['no-repeat', 'repeat', 'repeat-x', 'repeat-y'], 'no-repeat'),
'attachment' => self::allow((string) ($value['attachment'] ?? 'scroll'), ['scroll', 'fixed'], 'scroll'),
'size' => self::allow((string) ($value['size'] ?? 'cover'), ['cover', 'contain', 'auto'], 'cover'),
];
}
private static function group(array $field, $value): array {
$rows = is_array($value) ? array_values($value) : [];
$max = max(1, (int) ($field['max_items'] ?? 50));
$children = (array) ($field['fields'] ?? []);
return array_map(static fn($row) => self::normalize($children, is_array($row) ? $row : []), array_slice($rows, 0, $max));
}
private static function nested(array $field, $value): array {
$children = (array) ($field['fields'] ?? []);
return self::normalize($children, is_array($value) ? $value : []);
}
private static function color(string $value): string {
$value = trim($value);
if ($value === '') {
return '';
}
$hex = sanitize_hex_color($value);
if ($hex) {
return $hex;
}
return preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/', $value) ? $value : '';
}
private static function allow(string $value, array $allowed, string $fallback): string {
return in_array($value, $allowed, true) ? $value : $fallback;
}
}
+232
View File
@@ -0,0 +1,232 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Server-rendered, schema-driven widget form.
*/
final class FormRenderer {
public static function render(SchemaWidget $widget, array $fields, array $instance): void {
echo '<div class="aksrt-widget-form" data-aksrt-widget-form="1">';
foreach ($fields as $field) {
self::field($widget, $field, $instance[(string) ($field['id'] ?? '')] ?? ($field['default'] ?? ''), '');
}
echo '</div>';
}
private static function field(SchemaWidget $widget, array $field, $value, string $parent): void {
$type = (string) ($field['type'] ?? 'text');
$id = (string) ($field['id'] ?? '');
if (in_array($type, ['content', 'subheading', 'submessage'], true)) {
self::display_field($field, $type);
return;
}
if ($id === '') {
return;
}
$key = $parent === '' ? $id : $parent . '[' . $id . ']';
$field_id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
$field_name = $widget->get_field_name($key);
$dependency = !empty($field['dependency']) ? wp_json_encode($field['dependency']) : '';
echo '<div class="aksrt-widget-field aksrt-widget-field--' . esc_attr($type) . '" data-field-id="' . esc_attr($id) . '"';
if ($dependency !== '') {
echo ' data-dependency="' . esc_attr($dependency) . '"';
}
echo '>';
if (!empty($field['label'])) {
echo '<label class="aksrt-widget-field__label" for="' . esc_attr($field_id) . '">' . esc_html((string) $field['label']) . '</label>';
}
if (!empty($field['description'])) {
echo '<p class="description">' . wp_kses_post((string) $field['description']) . '</p>';
}
switch ($type) {
case 'switcher':
self::switcher($field_id, $field_name, (bool) $value);
break;
case 'radio':
case 'button_set':
case 'palette':
self::radios($field_id, $field_name, (array) ($field['options'] ?? []), (string) $value, $type);
break;
case 'select':
self::select($field_id, $field_name, $field, $value);
break;
case 'checkbox':
self::checkboxes($field_id, $field_name, $field, $value);
break;
case 'number':
case 'spinner':
case 'slider':
self::number($field_id, $field_name, $field, $value, $type);
break;
case 'textarea':
echo '<textarea class="widefat" rows="4" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '">' . esc_textarea((string) $value) . '</textarea>';
break;
case 'upload':
self::upload($field_id, $field_name, (string) $value);
break;
case 'color':
echo '<input class="widefat aksrt-widget-color" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '">';
break;
case 'icon':
echo '<div class="aksrt-widget-icon"><i class="' . esc_attr((string) $value) . '" aria-hidden="true"></i><input class="widefat" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '" placeholder="fa fa-star"></div>';
break;
case 'link':
self::link($widget, $key, is_array($value) ? $value : []);
break;
case 'spacing':
self::spacing($widget, $key, is_array($value) ? $value : []);
break;
case 'background':
self::background($widget, $key, is_array($value) ? $value : []);
break;
case 'group':
self::group($widget, $field, is_array($value) ? $value : [], $key);
break;
case 'fieldset':
case 'accordion':
self::nested($widget, $field, is_array($value) ? $value : [], $key);
break;
case 'text':
default:
echo '<input class="widefat" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '">';
}
echo '</div>';
}
private static function display_field(array $field, string $type): void {
$content = (string) ($field['content'] ?? $field['label'] ?? '');
echo '<div class="aksrt-widget-field aksrt-widget-field--' . esc_attr($type) . '">' . wp_kses_post($content) . '</div>';
}
private static function switcher(string $id, string $name, bool $value): void {
echo '<input type="hidden" name="' . esc_attr($name) . '" value="0">';
echo '<label class="aksrt-widget-switch"><input type="checkbox" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="1" ' . checked($value, true, false) . '><span aria-hidden="true"></span></label>';
}
private static function radios(string $id, string $name, array $options, string $value, string $type): void {
echo '<div class="aksrt-widget-choices aksrt-widget-choices--' . esc_attr($type) . '">';
foreach ($options as $key => $label) {
$option_id = $id . '-' . sanitize_html_class((string) $key);
echo '<label for="' . esc_attr($option_id) . '"><input type="radio" id="' . esc_attr($option_id) . '" name="' . esc_attr($name) . '" value="' . esc_attr((string) $key) . '" ' . checked($value, (string) $key, false) . '><span>' . esc_html((string) $label) . '</span></label>';
}
echo '</div>';
}
private static function select(string $id, string $name, array $field, $value): void {
$multiple = !empty($field['multiple']);
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) {
echo '<option value="' . esc_attr((string) $key) . '" ' . selected(in_array((string) $key, $values, true), true, false) . '>' . esc_html((string) $label) . '</option>';
}
echo '</select>';
}
private static function checkboxes(string $id, string $name, array $field, $value): void {
$options = (array) ($field['options'] ?? []);
if (!$options) {
self::switcher($id, $name, (bool) $value);
return;
}
$values = array_map('strval', (array) $value);
foreach ($options as $key => $label) {
echo '<label><input type="checkbox" name="' . esc_attr($name) . '[]" value="' . esc_attr((string) $key) . '" ' . checked(in_array((string) $key, $values, true), true, false) . '> ' . esc_html((string) $label) . '</label> ';
}
}
private static function number(string $id, string $name, array $field, $value, string $type): void {
$min = isset($field['min']) ? ' min="' . esc_attr((string) $field['min']) . '"' : '';
$max = isset($field['max']) ? ' max="' . esc_attr((string) $field['max']) . '"' : '';
$step = ' step="' . esc_attr((string) ($field['step'] ?? 1)) . '"';
$input_type = $type === 'slider' ? 'range' : 'number';
echo '<div class="aksrt-widget-number"><input class="widefat" type="' . esc_attr($input_type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . $min . $max . $step . '><output>' . esc_html((string) $value) . '</output></div>';
}
private static function upload(string $id, string $name, string $value): void {
echo '<div class="aksrt-widget-upload">';
if ($value !== '') {
echo '<img src="' . esc_url($value) . '" alt="" loading="lazy">';
}
echo '<input class="widefat" type="url" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '">';
echo '<button type="button" class="button aksrt-widget-upload__choose">' . esc_html__('选择媒体', 'aksrt') . '</button> <button type="button" class="button-link-delete aksrt-widget-upload__remove">' . esc_html__('移除', 'aksrt') . '</button></div>';
}
private static function link(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-link">';
self::plain_input($widget, $key . '[url]', (string) ($value['url'] ?? ''), 'url', 'URL');
self::plain_input($widget, $key . '[text]', (string) ($value['text'] ?? ''), 'text', '链接文字');
$name = $widget->get_field_name($key . '[target]');
echo '<label><input type="checkbox" name="' . esc_attr($name) . '" value="_blank" ' . checked(($value['target'] ?? '') === '_blank', true, false) . '> ' . esc_html__('新窗口打开', 'aksrt') . '</label></div>';
}
private static function spacing(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-spacing">';
foreach (['top' => '上', 'right' => '右', 'bottom' => '下', 'left' => '左'] as $edge => $label) {
echo '<label><span>' . esc_html($label) . '</span>';
self::plain_input($widget, $key . '[' . $edge . ']', (string) ($value[$edge] ?? ''), 'number', 'px');
echo '</label>';
}
echo '</div>';
}
private static function background(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-background">';
self::plain_input($widget, $key . '[color]', (string) ($value['color'] ?? ''), 'text', '背景颜色');
self::plain_input($widget, $key . '[image]', (string) ($value['image'] ?? ''), 'url', '背景图片 URL');
foreach ([
'position' => ['center center' => '居中', 'top center' => '顶部', 'bottom center' => '底部', 'center left' => '左侧', 'center right' => '右侧'],
'repeat' => ['no-repeat' => '不重复', 'repeat' => '平铺', 'repeat-x' => '水平平铺', 'repeat-y' => '垂直平铺'],
'attachment' => ['scroll' => '滚动', 'fixed' => '固定'],
'size' => ['cover' => '覆盖', 'contain' => '包含', 'auto' => '原始尺寸'],
] as $part => $options) {
$name = $widget->get_field_name($key . '[' . $part . ']');
echo '<select name="' . esc_attr($name) . '">';
foreach ($options as $option => $label) {
echo '<option value="' . esc_attr($option) . '" ' . selected((string) ($value[$part] ?? ''), $option, false) . '>' . esc_html($label) . '</option>';
}
echo '</select>';
}
echo '</div>';
}
private static function group(SchemaWidget $widget, array $field, array $rows, string $key): void {
$token = '__index_' . substr(md5($key), 0, 8) . '__';
echo '<div class="aksrt-widget-repeater" data-index-token="' . esc_attr($token) . '" data-max-items="' . esc_attr((string) ($field['max_items'] ?? 50)) . '"><div class="aksrt-widget-repeater__rows">';
foreach ($rows as $index => $row) {
self::group_row($widget, $field, is_array($row) ? $row : [], $key, (string) $index);
}
echo '</div><template class="aksrt-widget-repeater__template">';
self::group_row($widget, $field, [], $key, $token);
echo '</template><button type="button" class="button aksrt-widget-repeater__add">' . esc_html__('添加项目', 'aksrt') . '</button></div>';
}
private static function group_row(SchemaWidget $widget, array $field, array $row, string $key, string $index): void {
echo '<div class="aksrt-widget-repeater__row"><div class="aksrt-widget-repeater__bar"><span class="dashicons dashicons-move" aria-hidden="true"></span><strong>' . esc_html__('项目', 'aksrt') . '</strong><button type="button" class="button-link-delete aksrt-widget-repeater__remove">' . esc_html__('删除', 'aksrt') . '</button></div><div class="aksrt-widget-repeater__fields">';
foreach ((array) ($field['fields'] ?? []) as $child) {
$child_id = (string) ($child['id'] ?? '');
self::field($widget, $child, $row[$child_id] ?? ($child['default'] ?? ''), $key . '[' . $index . ']');
}
echo '</div></div>';
}
private static function nested(SchemaWidget $widget, array $field, array $value, string $key): void {
echo '<details class="aksrt-widget-nested"><summary>' . esc_html((string) ($field['label'] ?? '更多设置')) . '</summary><div class="aksrt-widget-nested__body">';
foreach ((array) ($field['fields'] ?? []) as $child) {
$child_id = (string) ($child['id'] ?? '');
self::field($widget, $child, $value[$child_id] ?? ($child['default'] ?? ''), $key);
}
echo '</div></details>';
}
private static function plain_input(SchemaWidget $widget, string $key, string $value, string $type, string $placeholder): void {
$name = $widget->get_field_name($key);
$id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
echo '<input class="widefat" type="' . esc_attr($type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '">';
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Applies the common widget contract before dispatching business markup.
*/
final class FrontendRenderer {
public static function render(WidgetDefinition $definition, array $args, array $instance, SchemaWidget $widget): void {
if (!self::is_visible($instance)) {
return;
}
$context = [
'widget_id' => (string) $widget->id,
'id_base' => $definition->id_base(),
];
$content = RendererRegistry::render($definition->renderer(), $instance, $context);
if ($content === '' && $definition->slug() !== 'layout_gap') {
return;
}
AssetManager::enqueue_for($definition);
$classes = ['aksrt-widget', 'aksrt-widget--' . $definition->slug(), 'aksrt-widget--' . $definition->size()];
if (($instance['show_type'] ?? 'all') === 'only_pc') {
$classes[] = 'aksrt-widget--only-pc';
} elseif (($instance['show_type'] ?? 'all') === 'only_sm') {
$classes[] = 'aksrt-widget--only-mobile';
}
if (!empty($instance['sidebar_affix'])) {
$classes[] = 'aksrt-widget--affix';
}
if (!empty($instance['animation_in'])) {
$classes[] = 'aksrt-widget--animate';
$classes[] = 'aksrt-widget--animate-' . sanitize_html_class((string) $instance['animation_in']);
}
if (!empty($instance['animation_repeat'])) {
$classes[] = 'aksrt-widget--animate-repeat';
}
$style = self::style($instance);
echo (string) ($args['before_widget'] ?? '<section>'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="' . esc_attr(implode(' ', $classes)) . '" data-aksrt-widget="' . esc_attr($definition->slug()) . '" data-widget-id="' . esc_attr((string) $widget->id) . '"' . ($style !== '' ? ' style="' . esc_attr($style) . '"' : '') . '>';
echo self::title($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="aksrt-widget__content">' . $content . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo (string) ($args['after_widget'] ?? '</section>'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private static function is_visible(array $instance): bool {
$type = (string) ($instance['show_id_type'] ?? '');
$ids = RendererHelpers::ids((string) ($instance['show_ids'] ?? ''));
if ($type === '' || !$ids) {
return true;
}
$current = function_exists('is_singular') && is_singular() && function_exists('get_queried_object_id') ? (int) get_queried_object_id() : 0;
$matched = in_array($current, $ids, true);
return $type === 'show' ? $matched : !$matched;
}
private static function title(array $instance): string {
$title = trim((string) ($instance['title'] ?? ''));
if ($title === '') {
return '';
}
$safe_title = esc_html($title);
$highlight = trim((string) ($instance['title_highlight'] ?? ''));
if ($highlight !== '') {
$safe_highlight = esc_html($highlight);
$safe_title = str_replace($safe_highlight, '<mark class="' . esc_attr((string) ($instance['title_highlight_color'] ?? 'focus-color')) . '">' . $safe_highlight . '</mark>', $safe_title);
}
$html = '<header class="aksrt-widget__header' . (($instance['title_style'] ?? '') === 'big' ? ' aksrt-widget__header--big' : '') . '"><div><h2 class="aksrt-widget__title">' . nl2br($safe_title) . '</h2>';
if (!empty($instance['subtitle'])) {
$html .= '<p class="aksrt-widget__subtitle">' . nl2br(esc_html((string) $instance['subtitle'])) . '</p>';
}
$html .= '</div><nav class="aksrt-widget__title-links" aria-label="' . esc_attr__('模块链接', 'aksrt') . '">';
foreach (['title_link', 'title_link2', 'title_link3'] as $index => $key) {
$link = RendererHelpers::link((array) ($instance[$key] ?? []));
if ($link['url'] !== '' && $link['text'] !== '') {
$color = (string) ($instance['title_link_style']['color_' . ($index + 1)] ?? '');
$html .= '<a class="' . esc_attr($color) . '" href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . esc_html($link['text']) . '</a>';
}
}
return $html . '</nav></header>';
}
private static function style(array $instance): string {
$properties = [];
foreach (['layout_pin_pc' => 'space', 'layout_pin_m' => 'space-m'] as $key => $prefix) {
foreach (['top', 'right', 'bottom', 'left'] as $edge) {
$value = $instance[$key][$edge] ?? '';
if ($value !== '' && is_numeric($value)) {
$properties[] = '--aksrt-widget-' . $prefix . '-' . $edge . ':' . (float) $value . 'px';
}
}
}
foreach (['light', 'dark'] as $mode) {
$background = (array) ($instance['layout_bg'][$mode] ?? []);
if (!empty($background['color'])) {
$properties[] = '--aksrt-widget-bg-' . $mode . ':' . (string) $background['color'];
}
if (!empty($background['image'])) {
$properties[] = '--aksrt-widget-bg-image-' . $mode . ':url(' . esc_url_raw((string) $background['image']) . ')';
}
}
return implode(';', $properties);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Builds post queries exclusively from normalized, saved widget settings.
*/
final class PostQuery {
private const PAY_TYPES = ['reading', 'download', 'gallery', 'video', 'vip'];
public static function build(array $instance, int $page = 1, array $segment = []): \WP_Query {
return new \WP_Query(self::arguments($instance, $page, $segment));
}
/** @return array<string,mixed> */
public static function arguments(array $instance, int $page = 1, array $segment = []): array {
$config = array_merge($instance, $segment);
$count = max(1, min(50, (int) ($config['count'] ?? 8)));
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $count,
'paged' => max(1, $page),
'ignore_sticky_posts' => true,
'no_found_rows' => false,
];
$categories = RendererHelpers::ids((string) ($config['category_ids'] ?? ''));
if (!empty($config['category_id'])) {
$categories = [(int) $config['category_id']];
}
if ($categories) {
$args['category__in'] = $categories;
}
$tags = RendererHelpers::ids((string) ($config['tag_ids'] ?? ''));
if ($tags) {
$args['tag__in'] = $tags;
}
$days = max(0, (int) ($config['limit_day'] ?? 0));
if ($days > 0) {
$args['date_query'] = [['after' => gmdate('Y-m-d', time() - ($days * DAY_IN_SECONDS)), 'inclusive' => true]];
}
self::apply_order($args, (string) ($config['orderby'] ?? 'date'), (string) ($config['order'] ?? 'DESC'));
self::apply_pay_type($args, (string) ($config['pay_type'] ?? ''));
return $args;
}
public static function segment(array $instance, string $segment): array {
$tabs = array_values((array) ($instance['tabs'] ?? []));
if (!$tabs) {
return [];
}
$index = ctype_digit($segment) ? (int) $segment : 0;
return isset($tabs[$index]) && is_array($tabs[$index]) ? $tabs[$index] : $tabs[0];
}
private static function apply_order(array &$args, string $orderby, string $order): void {
$args['order'] = strtoupper($order) === 'ASC' ? 'ASC' : 'DESC';
$meta = [
'views' => class_exists('AKSRT\\Core\\DomainModel') ? \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY : 'aksrt_post_views',
'likes' => '_aksrt_like_count',
'favorites' => '_aksrt_favorite_count',
'downloads' => class_exists('AKSRT\\Core\\DomainModel') ? \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY : '_aksrt_res_download_count',
];
if (isset($meta[$orderby])) {
$args['meta_key'] = $meta[$orderby];
$args['orderby'] = 'meta_value_num';
return;
}
if ($orderby === 'comments') {
$args['orderby'] = 'comment_count';
} elseif (in_array($orderby, ['date', 'modified', 'rand'], true)) {
$args['orderby'] = $orderby;
} else {
$args['orderby'] = 'date';
}
}
private static function apply_pay_type(array &$args, string $pay_type): void {
if (!in_array($pay_type, self::PAY_TYPES, true)) {
return;
}
$type_key = class_exists('AKSRT\\Core\\DomainModel') ? \AKSRT\Core\DomainModel::PAY_TYPE_KEY : '_aksrt_pay_type';
$meta_key = class_exists('AKSRT\\Core\\DomainModel') ? \AKSRT\Core\DomainModel::PAY_META_KEY : '_aksrt_pay';
$args['meta_query'] = [[
'relation' => 'OR',
['key' => $type_key, 'value' => $pay_type, 'compare' => '='],
['key' => $meta_key, 'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"', 'compare' => 'LIKE'],
]];
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Shared list/card/mini markup and pagination payloads.
*/
final class PostRenderer {
public static function payload(array $instance, string $slug, int $page = 1, string $segment = ''): array {
$segment_config = $slug === 'tab_posts' ? PostQuery::segment($instance, $segment) : [];
$query = PostQuery::build($instance, $page, $segment_config);
$html = self::items($query, $instance, $slug, $page);
$max_pages = max(1, (int) $query->max_num_pages);
$total = (int) $query->found_posts;
return [
'html' => $html,
'paginate_html' => self::pagination($instance, $page, $max_pages),
'has_more' => $page < $max_pages,
'page' => $page,
'max_pages' => $max_pages,
'total' => $total,
];
}
public static function items(\WP_Query $query, array $instance, string $slug, int $page = 1): string {
if (!$query->have_posts()) {
return RendererHelpers::empty_state(__('暂无文章', 'aksrt'));
}
$style = (string) ($instance['style'] ?? 'card');
$mode = in_array($style, ['list', 'card', 'mini'], true) ? $style : 'card';
$config = [
'mode' => $mode,
'context' => 'widget',
'columns' => $mode === 'card' ? 3 : 1,
'show_thumb' => $slug !== 'hot_posts' || !empty($instance['show_thumbnail']),
'show_meta' => $mode !== 'mini',
'show_excerpt' => $mode !== 'mini',
'show_author' => false,
'show_comments' => true,
'show_likes' => true,
'show_tags' => false,
'show_badges' => true,
'show_number' => $slug === 'hot_posts',
'number_offset' => ($page - 1) * max(1, (int) ($instance['count'] ?? 8)),
'wrapper_class' => 'aksrt-widget-posts aksrt-widget-posts--' . $slug,
];
if ($slug === 'oneline_posts') {
return self::oneline($query, $config, !empty($instance['autoplay']));
}
if (function_exists('aksrt_render_post_list')) {
return aksrt_render_post_list($query, $config, false);
}
$html = '<div class="aksrt-widget-posts aksrt-widget-posts--fallback">';
while ($query->have_posts()) {
$query->the_post();
$html .= '<article><a href="' . esc_url(get_permalink()) . '">' . get_the_post_thumbnail(get_the_ID(), 'medium') . '<strong>' . esc_html(get_the_title()) . '</strong></a></article>';
}
wp_reset_postdata();
return $html . '</div>';
}
private static function oneline(\WP_Query $query, array $config, bool $autoplay): string {
$slides = '';
while ($query->have_posts()) {
$query->the_post();
$item = function_exists('aksrt_render_post')
? aksrt_render_post(array_merge($config, ['skip_wrapper' => true]))
: '<a href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a>';
$slides .= '<div class="swiper-slide">' . $item . '</div>';
}
wp_reset_postdata();
return '<div class="swiper aksrt-widget-swiper aksrt-widget-oneline" data-aksrt-swiper="scroll" data-autoplay="' . ($autoplay ? '1' : '0') . '"><div class="swiper-wrapper">' . $slides . '</div><button class="swiper-button-prev" type="button" aria-label="' . esc_attr__('上一组', 'aksrt') . '"></button><button class="swiper-button-next" type="button" aria-label="' . esc_attr__('下一组', 'aksrt') . '"></button></div>';
}
private static function pagination(array $instance, int $page, int $max_pages): string {
$mode = (string) ($instance['paginate'] ?? 'none');
if ($max_pages <= 1 || $mode === 'none') {
return '';
}
if ($mode === 'numbered') {
$start = max(1, min($page - 2, $max_pages - 4));
$end = min($max_pages, max(5, $page + 2));
$html = '<nav class="aksrt-widget-pagination" aria-label="' . esc_attr__('文章分页', 'aksrt') . '">';
for ($number = $start; $number <= $end; $number++) {
$html .= '<button type="button" data-widget-page="' . $number . '"' . ($number === $page ? ' aria-current="page"' : '') . '>' . $number . '</button>';
}
return $html . '</nav>';
}
$label = $mode === 'infinite' ? __('继续加载', 'aksrt') : __('加载更多', 'aksrt');
return '<button class="aksrt-widget-load-more" type="button" data-next-page="' . (int) ($page + 1) . '" data-mode="' . esc_attr($mode) . '">' . esc_html($label) . '</button>';
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Small HTML primitives shared by the renderer groups.
*/
final class RendererHelpers {
public static function link(array $link): array {
$url = esc_url((string) ($link['url'] ?? ''));
$text = sanitize_text_field((string) ($link['text'] ?? ''));
$target = ($link['target'] ?? '') === '_blank' ? '_blank' : '';
return ['url' => $url, 'text' => $text, 'target' => $target];
}
public static function link_attrs(array $link): string {
$link = self::link($link);
return $link['target'] === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
}
public static function image(string $url, string $alt = '', string $class = ''): string {
if ($url === '') {
return '';
}
return '<img' . ($class !== '' ? ' class="' . esc_attr($class) . '"' : '')
. ' src="' . esc_url($url) . '" alt="' . esc_attr($alt) . '" loading="lazy">';
}
public static function lines(string $value): array {
$lines = preg_split('/\r\n|\r|\n/', $value) ?: [];
return array_values(array_filter(array_map('trim', $lines), static fn(string $line): bool => $line !== ''));
}
public static function empty_state(string $message): string {
if (function_exists('aksrt_get_widget_null')) {
return aksrt_get_widget_null($message);
}
return '<div class="aksrt-widget__empty" role="status">' . esc_html($message) . '</div>';
}
public static function card_media(array $item, string $class = ''): string {
$title = (string) ($item['title'] ?? '');
$link = self::link((array) ($item['link'] ?? []));
$media = '';
if (!empty($item['image_dark'])) {
$media .= self::image((string) $item['image_dark'], $title, 'aksrt-media-card__image aksrt-media-card__image--dark');
}
$media .= self::image((string) ($item['image'] ?? ''), $title, 'aksrt-media-card__image');
if (!empty($item['video'])) {
$media .= '<video class="aksrt-media-card__video" src="' . esc_url((string) $item['video']) . '" muted loop playsinline preload="metadata"></video>';
}
$body = '<span class="aksrt-media-card__body"><strong>' . esc_html($title) . '</strong>';
if (!empty($item['description'])) {
$body .= '<small>' . esc_html((string) $item['description']) . '</small>';
}
$body .= '</span>';
$tag = $link['url'] !== '' ? 'a' : 'div';
$attrs = $link['url'] !== '' ? ' href="' . $link['url'] . '"' . self::link_attrs($link) : '';
return '<' . $tag . ' class="aksrt-media-card ' . esc_attr($class) . '"' . $attrs . '><span class="aksrt-media-card__media">' . $media . '</span>' . $body . '</' . $tag . '>';
}
public static function ids(string $value): array {
return array_values(array_filter(array_map('absint', preg_split('/\s*,\s*/', $value) ?: [])));
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace AKSRT\WidgetSystem;
use AKSRT\WidgetSystem\Renderers\ContentRenderer;
use AKSRT\WidgetSystem\Renderers\GeneralRenderer;
use AKSRT\WidgetSystem\Renderers\KnowledgeProductRenderer;
use AKSRT\WidgetSystem\Renderers\SliderRenderer;
use AKSRT\WidgetSystem\Renderers\UserRenderer;
defined('ABSPATH') || exit;
/**
* One explicit dispatch table for every modern widget renderer.
*/
final class RendererRegistry {
/** @return array<string, class-string> */
public static function all(): array {
return [
'search' => GeneralRenderer::class, 'posts_toc' => GeneralRenderer::class,
'quote' => GeneralRenderer::class, 'icon_cards' => GeneralRenderer::class,
'video' => GeneralRenderer::class, 'embed' => GeneralRenderer::class,
'media_cards' => GeneralRenderer::class, 'icon_cover_cards' => GeneralRenderer::class,
'partners' => GeneralRenderer::class, 'combo_info' => GeneralRenderer::class,
'text_title' => GeneralRenderer::class, 'buttons' => GeneralRenderer::class,
'layout_gap' => GeneralRenderer::class, 'tag_cloud' => GeneralRenderer::class,
'recent_comments' => GeneralRenderer::class, 'links' => GeneralRenderer::class,
'notice' => GeneralRenderer::class,
'term_cards' => ContentRenderer::class, 'term_aggregation' => ContentRenderer::class,
'hot_posts' => ContentRenderer::class, 'post_pay' => ContentRenderer::class,
'posts' => ContentRenderer::class, 'tab_posts' => ContentRenderer::class,
'oneline_posts' => ContentRenderer::class,
'pricing_plans' => KnowledgeProductRenderer::class, 'reviews' => KnowledgeProductRenderer::class,
'faq' => KnowledgeProductRenderer::class, 'stats' => KnowledgeProductRenderer::class,
'highlights' => KnowledgeProductRenderer::class, 'feature_intro' => KnowledgeProductRenderer::class,
'tilt_cases' => KnowledgeProductRenderer::class,
'slider' => SliderRenderer::class, 'slider_media_combo' => SliderRenderer::class,
'user_card' => UserRenderer::class, 'author_card' => UserRenderer::class,
'user_ranking' => UserRenderer::class, 'user_list' => UserRenderer::class,
];
}
public static function render(string $slug, array $instance, array $context = []): string {
$renderer = self::all()[$slug] ?? null;
if ($renderer === null || !is_callable([$renderer, 'render'])) {
return '';
}
return (string) $renderer::render($slug, $instance, $context);
}
public static function supports_items(string $slug): bool {
return in_array($slug, ['posts', 'tab_posts', 'oneline_posts', 'hot_posts'], true);
}
}
@@ -0,0 +1,136 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\PostRenderer;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render article discovery and paid-content widgets. */
final class ContentRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
switch ($slug) {
case 'term_cards': return self::term_cards($instance);
case 'term_aggregation': return self::term_aggregation($instance);
case 'post_pay': return self::post_pay($instance);
case 'tab_posts': return self::tab_posts($instance, $context);
case 'posts':
case 'hot_posts':
case 'oneline_posts': return self::posts($slug, $instance, $context);
}
return '';
}
private static function terms(array $instance): array {
$args = [
'taxonomy' => (string) $instance['taxonomy'],
'number' => (int) $instance['count'],
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
];
$ids = RendererHelpers::ids((string) $instance['term_ids']);
if ($ids) {
$args['include'] = $ids;
$args['orderby'] = 'include';
}
$terms = get_terms($args);
return is_wp_error($terms) ? [] : (array) $terms;
}
private static function term_cards(array $instance): string {
$terms = self::terms($instance);
if (!$terms) {
return RendererHelpers::empty_state(__('暂无分类', 'aksrt'));
}
$html = '<div class="aksrt-widget-grid aksrt-term-cards" style="--aksrt-cols:' . (int) $instance['pc_row'] . ';--aksrt-cols-m:' . (int) $instance['m_row'] . '">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
continue;
}
$image = (string) get_term_meta((int) $term->term_id, 'aksrt_cover_url', true);
if ($image === '') {
$image = (string) get_term_meta((int) $term->term_id, 'cover_url', true);
}
$html .= '<a class="aksrt-term-card" href="' . esc_url($link) . '"><span class="aksrt-term-card__media">' . RendererHelpers::image($image, $term->name) . '</span><span class="aksrt-term-card__body"><strong>' . esc_html($term->name) . '</strong><small>' . esc_html(wp_trim_words((string) $term->description, 18)) . '</small><em>' . sprintf(esc_html__('%s 篇内容', 'aksrt'), number_format_i18n((int) $term->count)) . '</em></span></a>';
}
return $html . '</div>';
}
private static function term_aggregation(array $instance): string {
$terms = self::terms($instance);
if (!$terms) {
return RendererHelpers::empty_state(__('暂无聚合内容', 'aksrt'));
}
$taxonomy = (string) $instance['taxonomy'];
$term_ids = array_map(static fn($term): int => (int) $term->term_id, $terms);
$query = new \WP_Query([
'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => min(100, count($terms) * 6),
'ignore_sticky_posts' => true, 'no_found_rows' => true,
'tax_query' => [['taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids]],
]);
$by_term = [];
foreach ($query->posts as $post) {
foreach (wp_get_post_terms((int) $post->ID, $taxonomy, ['fields' => 'ids']) as $term_id) {
if (in_array((int) $term_id, $term_ids, true) && count($by_term[$term_id] ?? []) < 4) {
$by_term[$term_id][] = $post;
}
}
}
$html = '<div class="aksrt-term-aggregation">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
continue;
}
$html .= '<section class="aksrt-term-aggregation__card"><header><a href="' . esc_url($link) . '"><strong>' . esc_html($term->name) . '</strong><span>' . esc_html__('查看全部', 'aksrt') . '</span></a></header><ul>';
foreach ($by_term[$term->term_id] ?? [] as $post) {
$html .= '<li><a href="' . esc_url(get_permalink($post)) . '">' . get_the_post_thumbnail($post, 'thumbnail') . '<span>' . esc_html(get_the_title($post)) . '</span></a></li>';
}
$html .= '</ul></section>';
}
return $html . '</div>';
}
private static function posts(string $slug, array $instance, array $context): string {
$widget_id = (string) ($context['widget_id'] ?? '');
if (($instance['load_mode'] ?? 'sync') === 'ajax') {
return '<div class="aksrt-widget-items is-loading" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1"><div class="aksrt-widget-skeleton" aria-label="' . esc_attr__('正在加载', 'aksrt') . '"></div></div>';
}
$payload = PostRenderer::payload($instance, $slug, 1);
return '<div class="aksrt-widget-items" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1">' . $payload['html'] . $payload['paginate_html'] . '</div>';
}
private static function tab_posts(array $instance, array $context): string {
$tabs = array_values((array) $instance['tabs']);
if (!$tabs) {
return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt'));
}
$buttons = '';
foreach ($tabs as $index => $tab) {
$buttons .= '<button type="button" role="tab" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" data-segment="' . $index . '">' . esc_html((string) ($tab['title'] ?? sprintf(__('栏目 %d', 'aksrt'), $index + 1))) . '</button>';
}
$payload = PostRenderer::payload($instance, 'tab_posts', 1, '0');
return '<div class="aksrt-widget-tabs" data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '"><div class="aksrt-widget-tabs__nav" role="tablist">' . $buttons . '</div><div class="aksrt-widget-tabs__panel" role="tabpanel" data-widget-items data-segment="0">' . $payload['html'] . $payload['paginate_html'] . '</div></div>';
}
private static function post_pay(array $instance): string {
$post = get_post();
if (!$post instanceof \WP_Post || !function_exists('aksrt_get_pay_meta')) {
return '';
}
$meta = aksrt_get_pay_meta($post);
if (($meta['pay_type'] ?? 'no') === 'no') {
return RendererHelpers::empty_state(__('当前文章未配置付费内容', 'aksrt'));
}
if (function_exists('aksrt_has_paid_access') && aksrt_has_paid_access($post)) {
$html = '<div class="aksrt-post-pay is-authorized ' . esc_attr((string) $instance['theme']) . '"><i class="fas fa-check-circle" aria-hidden="true"></i><strong>' . esc_html__('内容已解锁', 'aksrt') . '</strong><p>' . esc_html__('你已拥有当前知识内容的访问权限。', 'aksrt') . '</p>';
$html .= function_exists('aksrt_get_pay_attributes_html') ? aksrt_get_pay_attributes_html($post, 'aksrt-post-pay__attrs') : '';
$html .= function_exists('aksrt_get_demo_link_html') ? aksrt_get_demo_link_html($post, 'aksrt-widget-button') : '';
return $html . '</div>';
}
return function_exists('aksrt_get_paywall_html') ? aksrt_get_paywall_html($post, true) : RendererHelpers::empty_state(__('请登录或购买后查看', 'aksrt'));
}
}
@@ -0,0 +1,196 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render the 17 general and decorative widgets. */
final class GeneralRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
$method = 'render_' . $slug;
return is_callable([self::class, $method]) ? (string) self::$method($instance, $context) : '';
}
private static function render_search(array $i): string {
$action = esc_url(home_url('/'));
$placeholder = esc_attr((string) $i['placeholder']);
$style = sanitize_html_class((string) $i['search_style']);
return '<form class="aksrt-widget-search aksrt-widget-search--' . $style . '" role="search" method="get" action="' . $action . '"><label><span class="screen-reader-text">' . esc_html__('搜索:', 'aksrt') . '</span><input type="search" name="s" placeholder="' . $placeholder . '" autocomplete="off"></label><button type="submit" aria-label="' . esc_attr__('搜索', 'aksrt') . '"><i class="fas fa-search" aria-hidden="true"></i><span>' . esc_html__('搜索', 'aksrt') . '</span></button></form>';
}
private static function render_posts_toc(array $i): string {
if (!is_singular('post')) {
return '';
}
return '<nav class="aksrt-widget-toc' . (!empty($i['collapsed']) ? ' is-collapsed' : '') . '" data-min-headings="' . (int) $i['min_headings'] . '" data-scroll-offset="' . (int) $i['offset'] . '" aria-label="' . esc_attr__('文章目录', 'aksrt') . '"><button class="aksrt-widget-toc__toggle" type="button" aria-expanded="' . (empty($i['collapsed']) ? 'true' : 'false') . '"><span>' . esc_html__('文章目录', 'aksrt') . '</span><i class="fas fa-chevron-down" aria-hidden="true"></i></button><ol class="aksrt-widget-toc__list"></ol><p class="aksrt-widget-toc__empty">' . esc_html__('正在生成目录…', 'aksrt') . '</p></nav>';
}
private static function render_quote(array $i): string {
$quotes = RendererHelpers::lines((string) $i['quotes']);
$text = $quotes ? $quotes[array_rand($quotes)] : __('保持好奇,持续创造。', 'aksrt');
return '<figure class="aksrt-widget-quote" data-source="' . esc_attr((string) $i['source']) . '" data-interval="' . (int) $i['interval'] . '"><i class="fas fa-quote-left" aria-hidden="true"></i><blockquote>' . esc_html($text) . '</blockquote><button type="button" aria-label="' . esc_attr__('换一句', 'aksrt') . '"><i class="fas fa-sync-alt" aria-hidden="true"></i></button></figure>';
}
private static function render_icon_cards(array $i): string {
return self::icon_cards((array) $i['cards'], false, (int) $i['pc_row'], (int) $i['m_row']);
}
private static function render_icon_cover_cards(array $i): string {
return self::icon_cards((array) $i['cards'], true, (int) $i['pc_row'], (int) $i['m_row']);
}
private static function icon_cards(array $cards, bool $cover, int $pc, int $mobile): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-icon-cards' . ($cover ? ' aksrt-widget-icon-cards--cover' : '') . '" style="--aksrt-cols:' . $pc . ';--aksrt-cols-m:' . $mobile . '">';
foreach ($cards as $card) {
if (!is_array($card)) {
continue;
}
$args = ['icon' => $card['icon'] ?? '', 'customize_icon' => $card['custom_icon'] ?? '', 'icon_image' => $card['custom_icon'] ?? '', 'icon_color' => $card['color'] ?? '', 'icon_class' => $card['color'] ?? '', 'title' => $card['title'] ?? '', 'desc' => $card['description'] ?? '', 'link' => $card['link'] ?? []];
if ($cover && function_exists('aksrt_icon_cover_card')) {
$html .= aksrt_icon_cover_card($args);
} elseif (!$cover && function_exists('aksrt_icon_card')) {
$html .= aksrt_icon_card($args);
} else {
$link = RendererHelpers::link((array) ($card['link'] ?? []));
$tag = $link['url'] ? 'a' : 'div';
$html .= '<' . $tag . ' class="aksrt-widget-icon-card ' . esc_attr((string) ($card['color'] ?? '')) . '"' . ($link['url'] ? ' href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) : '') . '><i class="' . esc_attr((string) ($card['icon'] ?? 'fa fa-star')) . '" aria-hidden="true"></i><span><strong>' . esc_html((string) ($card['title'] ?? '')) . '</strong><small>' . esc_html((string) ($card['description'] ?? '')) . '</small></span></' . $tag . '>';
}
}
return $html . '</div>';
}
private static function render_video(array $i): string {
$url = esc_url((string) $i['url']);
if ($url === '') {
return RendererHelpers::empty_state(__('请配置视频地址', 'aksrt'));
}
$ratio = (string) $i['ratio'];
return '<div class="aksrt-widget-video aksrt-widget-video--' . esc_attr(str_replace(':', '-', $ratio)) . '" data-video-url="' . $url . '" data-video-poster="' . esc_url((string) $i['poster']) . '" data-autoplay="' . (!empty($i['autoplay']) ? '1' : '0') . '" data-muted="' . (!empty($i['muted']) ? '1' : '0') . '"><video controls playsinline preload="metadata"' . (!empty($i['autoplay']) ? ' autoplay' : '') . (!empty($i['muted']) ? ' muted' : '') . (!empty($i['poster']) ? ' poster="' . esc_url((string) $i['poster']) . '"' : '') . '><source src="' . $url . '"></video><p class="aksrt-widget-media-error" hidden>' . esc_html__('视频加载失败,请稍后重试。', 'aksrt') . '</p></div>';
}
private static function render_embed(array $i): string {
if (empty($i['url'])) {
return RendererHelpers::empty_state(__('请配置嵌入地址', 'aksrt'));
}
return '<div class="aksrt-widget-embed" style="--aksrt-embed-ratio:' . esc_attr((string) (float) $i['ratio']) . '%"><iframe src="' . esc_url((string) $i['url']) . '" loading="lazy" referrerpolicy="strict-origin-when-cross-origin"' . (!empty($i['allow_fullscreen']) ? ' allowfullscreen' : '') . ' title="' . esc_attr__('嵌入内容', 'aksrt') . '"></iframe></div>';
}
private static function render_media_cards(array $i): string {
return self::media_grid((array) $i['cards']);
}
private static function media_grid(array $items, string $class = ''): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-media-cards ' . esc_attr($class) . '">';
foreach ($items as $item) {
if (is_array($item)) {
$html .= RendererHelpers::card_media($item);
}
}
return $html . '</div>';
}
private static function render_partners(array $i): string {
$items = '';
foreach ((array) $i['items'] as $item) {
if (is_array($item)) {
$items .= RendererHelpers::card_media($item, 'aksrt-partner');
}
}
return '<div class="aksrt-widget-partners" style="--aksrt-partner-rows:' . (int) $i['rows_pc'] . ';--aksrt-partner-rows-m:' . (int) $i['rows_mobile'] . ';--aksrt-partner-speed:' . (int) $i['speed'] . 's"><div class="aksrt-widget-partners__track">' . $items . $items . '</div></div>';
}
private static function render_combo_info(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-combo">';
foreach ((array) $i['items'] as $item) {
if (!is_array($item)) {
continue;
}
$tags = '';
foreach (preg_split('/\s*[,,]\s*/', (string) ($item['tags'] ?? '')) ?: [] as $tag) {
if ($tag !== '') {
$tags .= '<span>' . esc_html($tag) . '</span>';
}
}
$html .= '<article class="aksrt-combo-card">' . RendererHelpers::card_media($item) . ($tags ? '<div class="aksrt-combo-card__tags">' . $tags . '</div>' : '') . '</article>';
}
return $html . '</div>';
}
private static function render_text_title(array $i): string {
return '<div class="aksrt-widget-text-title is-' . esc_attr((string) $i['alignment']) . '"><h2>' . nl2br(esc_html((string) $i['heading'])) . '</h2><p>' . nl2br(esc_html((string) $i['description'])) . '</p></div>';
}
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>';
}
return $html . '</div>';
}
private static function render_layout_gap(array $i): string {
return '<div class="aksrt-widget-gap" aria-hidden="true" style="--aksrt-gap:' . (float) $i['pc_gap'] . 'px;--aksrt-gap-m:' . (float) $i['m_gap'] . 'px"></div>';
}
private static function render_tag_cloud(array $i): string {
$args = ['taxonomy' => (string) $i['taxonomy'], 'number' => (int) $i['number'], 'hide_empty' => true];
if ($i['orderby'] !== 'rand') {
$args['orderby'] = (string) $i['orderby'];
$args['order'] = 'DESC';
}
$terms = get_terms($args);
if (is_wp_error($terms) || !$terms) {
return RendererHelpers::empty_state(__('暂无标签', 'aksrt'));
}
if ($i['orderby'] === 'rand') {
shuffle($terms);
}
$html = '<div class="aksrt-widget-tag-cloud">';
foreach (array_slice($terms, 0, (int) $i['number']) as $term) {
$url = get_term_link($term);
if (!is_wp_error($url)) {
$html .= '<a href="' . esc_url($url) . '">' . esc_html($term->name) . (!empty($i['show_count']) ? '<small>' . (int) $term->count . '</small>' : '') . '</a>';
}
}
return $html . '</div>';
}
private static function render_recent_comments(array $i): string {
$comments = get_comments(['status' => 'approve', 'number' => (int) $i['count'], 'user__not_in' => RendererHelpers::ids((string) $i['exclude_users']), 'post__not_in' => RendererHelpers::ids((string) $i['exclude_posts'])]);
if (!$comments) {
return RendererHelpers::empty_state(__('暂无评论', 'aksrt'));
}
$html = '<ul class="aksrt-widget-comments">';
foreach ($comments as $comment) {
$html .= '<li><a class="aksrt-widget-comments__avatar" href="' . esc_url(get_comment_link($comment)) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($comment, 48) : get_avatar($comment, 48)) . '</a><div><strong>' . esc_html(get_comment_author($comment)) . '</strong><p>' . esc_html(wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 18)) . '</p>' . (!empty($i['show_source']) ? '<a class="aksrt-widget-comments__source" href="' . esc_url(get_comment_link($comment)) . '">' . esc_html(get_the_title((int) $comment->comment_post_ID)) . '</a>' : '') . '</div></li>';
}
return $html . '</ul>';
}
private static function render_links(array $i): string {
$html = '<div class="aksrt-widget-links aksrt-widget-links--' . esc_attr((string) $i['style']) . '">';
foreach (array_slice((array) $i['links'], 0, (int) $i['limit']) as $item) {
$link = RendererHelpers::link((array) ($item['link'] ?? []));
if ($link['url']) {
$html .= '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '><strong>' . esc_html((string) ($item['title'] ?? $link['text'])) . '</strong><small>' . esc_html((string) ($item['description'] ?? '')) . '</small></a>';
}
}
return $html . '</div>';
}
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>';
}
return '<div class="aksrt-widget-notice"><i class="fas fa-bullhorn" aria-hidden="true"></i><div class="swiper aksrt-widget-swiper" data-aksrt-swiper="notice" data-interval="' . ((int) $i['interval'] * 1000) . '"><div class="swiper-wrapper">' . $slides . '</div></div></div>';
}
}
@@ -0,0 +1,119 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render the seven knowledge-product presentation widgets. */
final class KnowledgeProductRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
switch ($slug) {
case 'pricing_plans': return self::pricing($instance);
case 'reviews': return self::reviews($instance);
case 'faq': return self::faq($instance);
case 'stats': return self::stats($instance);
case 'highlights': return self::highlights($instance);
case 'feature_intro': return self::feature_intro($instance);
case 'tilt_cases': return self::tilt($instance);
}
return '';
}
private static function pricing(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-pricing" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['plans'] as $plan) {
if (!is_array($plan)) {
continue;
}
$url = '';
$label = __('立即了解', 'aksrt');
if (($plan['cta_type'] ?? 'link') === 'post' && !empty($plan['post_id'])) {
$url = (string) get_permalink((int) $plan['post_id']);
$label = __('查看付费内容', 'aksrt');
} elseif (($plan['cta_type'] ?? 'link') === 'vip') {
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : home_url('/');
$url = add_query_arg('level', (int) ($plan['vip_level'] ?? 1), $url);
$label = __('开通会员', 'aksrt');
} else {
$link = RendererHelpers::link((array) ($plan['link'] ?? []));
$url = $link['url'];
$label = $link['text'] ?: $label;
}
$html .= '<article class="aksrt-pricing-card' . (!empty($plan['featured']) ? ' is-featured' : '') . '">';
if (!empty($plan['featured'])) {
$html .= '<span class="aksrt-pricing-card__badge">' . esc_html__('推荐', 'aksrt') . '</span>';
}
$html .= '<h3>' . esc_html((string) ($plan['title'] ?? '')) . '</h3><div class="aksrt-pricing-card__price">' . esc_html((string) ($plan['price'] ?? '')) . '</div><p>' . esc_html((string) ($plan['description'] ?? '')) . '</p><ul>';
foreach (RendererHelpers::lines((string) ($plan['features'] ?? '')) as $feature) {
$html .= '<li><i class="fas fa-check" aria-hidden="true"></i>' . esc_html($feature) . '</li>';
}
$html .= '</ul>' . ($url !== '' ? '<a class="aksrt-widget-button" href="' . esc_url($url) . '">' . esc_html($label) . '</a>' : '') . '</article>';
}
return $html . '</div>';
}
private static function reviews(array $i): string {
$slides = '';
foreach ((array) $i['items'] as $item) {
if (!is_array($item)) {
continue;
}
$rating = max(1, min(5, (int) ($item['rating'] ?? 5)));
$slides .= '<div class="swiper-slide"><article class="aksrt-review-card"><div class="aksrt-review-card__rating" aria-label="' . esc_attr(sprintf(__('%d 星评价', 'aksrt'), $rating)) . '">' . str_repeat('<i class="fas fa-star" aria-hidden="true"></i>', $rating) . '</div><blockquote>' . esc_html((string) ($item['description'] ?? '')) . '</blockquote><footer>' . RendererHelpers::image((string) ($item['image'] ?? ''), '', 'aksrt-review-card__avatar') . '<span><strong>' . esc_html((string) ($item['title'] ?? '')) . '</strong><small>' . esc_html((string) ($item['value'] ?? '')) . '</small></span></footer></article></div>';
}
return '<div class="swiper aksrt-widget-swiper aksrt-reviews" data-aksrt-swiper="cards"><div class="swiper-wrapper">' . $slides . '</div><div class="swiper-pagination"></div></div>';
}
private static function faq(array $i): string {
$html = '<div class="aksrt-faq">';
if (!empty($i['search'])) {
$html .= '<label class="aksrt-faq__search"><i class="fas fa-search" aria-hidden="true"></i><span class="screen-reader-text">' . esc_html__('搜索常见问题', 'aksrt') . '</span><input type="search" placeholder="' . esc_attr__('搜索问题', 'aksrt') . '"></label>';
}
$html .= '<div class="aksrt-faq__items">';
foreach ((array) $i['items'] as $index => $item) {
$id = 'aksrt-faq-' . $index . '-' . wp_rand(1000, 9999);
$html .= '<section class="aksrt-faq__item" data-search="' . esc_attr(wp_strip_all_tags((string) ($item['question'] ?? '') . ' ' . (string) ($item['answer'] ?? ''))) . '"><button type="button" aria-expanded="false" aria-controls="' . esc_attr($id) . '"><span>' . esc_html((string) ($item['question'] ?? '')) . '</span><i class="fas fa-plus" aria-hidden="true"></i></button><div id="' . esc_attr($id) . '" hidden>' . wp_kses_post((string) ($item['answer'] ?? '')) . '</div></section>';
}
return $html . '</div></div>';
}
private static function stats(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-stats" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $item) {
$html .= '<article class="aksrt-stat"><strong data-count-up="' . esc_attr((string) ($item['value'] ?? '')) . '">' . esc_html((string) ($item['value'] ?? '')) . '</strong><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p></article>';
}
return $html . '</div>';
}
private static function highlights(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-highlights" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $index => $item) {
$html .= RendererHelpers::card_media((array) $item, 'aksrt-highlight aksrt-highlight--' . (($index % 5 === 0) ? 'large' : 'normal'));
}
return $html . '</div>';
}
private static function feature_intro(array $i): string {
$tabs = $panels = '';
foreach ((array) $i['items'] as $index => $item) {
$tabs .= '<button type="button" role="tab" data-feature-index="' . $index . '" aria-selected="' . ($index === 0 ? 'true' : 'false') . '">' . esc_html((string) ($item['title'] ?? '')) . '</button>';
$features = '';
foreach (RendererHelpers::lines((string) ($item['features'] ?? '')) as $feature) {
$features .= '<li>' . esc_html($feature) . '</li>';
}
$panels .= '<article role="tabpanel" data-feature-panel="' . $index . '"' . ($index ? ' hidden' : '') . '><div class="aksrt-feature-intro__media">' . RendererHelpers::card_media((array) $item) . '</div><div class="aksrt-feature-intro__copy"><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p><ul>' . $features . '</ul></div></article>';
}
return '<div class="aksrt-feature-intro"><div class="aksrt-feature-intro__tabs" role="tablist">' . $tabs . '</div><div class="aksrt-feature-intro__panels">' . $panels . '</div></div>';
}
private static function tilt(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-tilt-cases" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $item) {
$html .= '<div class="aksrt-tilt-case" data-aksrt-tilt data-tilt-max="9" data-tilt-speed="500">' . RendererHelpers::card_media((array) $item) . '</div>';
}
return $html . '</div>';
}
}
@@ -0,0 +1,55 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render full sliders and the slider/media combination. */
final class SliderRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
$slides = '';
foreach ((array) $instance['slides'] as $slide) {
if (!is_array($slide) || (empty($slide['background']) && empty($slide['background_video']))) {
continue;
}
$link = RendererHelpers::link((array) ($slide['link'] ?? []));
$media = !empty($slide['background_video'])
? '<video src="' . esc_url((string) $slide['background_video']) . '" muted loop playsinline preload="metadata"></video>'
: RendererHelpers::image((string) $slide['background'], (string) ($slide['title'] ?? ''), 'swiper-lazy');
$layers = '';
foreach ((array) ($slide['layers'] ?? []) as $layer) {
if (!empty($layer['image'])) {
$layers .= '<img class="aksrt-slide-layer" src="' . esc_url((string) $layer['image']) . '" alt="" data-swiper-parallax="' . (int) ($layer['parallax'] ?? 0) . '" data-swiper-parallax-scale="' . esc_attr((string) ((float) ($layer['scale'] ?? 100) / 100)) . '">';
}
}
$copy = '<div class="aksrt-slide-copy is-' . esc_attr((string) ($slide['text_align'] ?? 'left-bottom')) . '"><h3>' . esc_html((string) ($slide['title'] ?? '')) . '</h3><p>' . esc_html((string) ($slide['description'] ?? '')) . '</p></div>';
$body = $media . $layers . $copy;
if ($link['url']) {
$body = '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . $body . '</a>';
}
$slides .= '<div class="swiper-slide' . (!empty($slide['hide_mobile']) ? ' aksrt-slide--hide-mobile' : '') . '">' . $body . '</div>';
}
if ($slides === '') {
return RendererHelpers::empty_state(__('请配置幻灯片', 'aksrt'));
}
$slider = '<div class="swiper aksrt-widget-swiper aksrt-slider" data-aksrt-swiper="slider" data-direction="' . esc_attr((string) $instance['direction']) . '" data-effect="' . esc_attr((string) $instance['effect']) . '" data-loop="' . (!empty($instance['loop']) ? '1' : '0') . '" data-autoplay="' . (!empty($instance['autoplay']) ? '1' : '0') . '" data-interval="' . ((int) $instance['interval'] * 1000) . '" data-speed="' . (int) $instance['speed'] . '" data-space="' . (int) $instance['spacebetween'] . '" style="--aksrt-slider-height:' . (int) $instance['pc_height'] . 'px;--aksrt-slider-height-m:' . (int) $instance['m_height'] . 'px"><div class="swiper-wrapper">' . $slides . '</div>';
if (!empty($instance['button'])) {
$slider .= '<button type="button" class="swiper-button-prev" aria-label="' . esc_attr__('上一张', 'aksrt') . '"></button><button type="button" class="swiper-button-next" aria-label="' . esc_attr__('下一张', 'aksrt') . '"></button>';
}
if (!empty($instance['pagination'])) {
$slider .= '<div class="swiper-pagination"></div>';
}
$slider .= '</div>';
if ($slug !== 'slider_media_combo') {
return $slider;
}
$cards = '';
foreach ((array) ($instance['side_cards'] ?? []) as $card) {
$cards .= RendererHelpers::card_media((array) $card);
}
return '<div class="aksrt-slider-combo"><div class="aksrt-slider-combo__main">' . $slider . '</div><aside class="aksrt-slider-combo__side">' . $cards . '</aside></div>';
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render login, author, ranking and user-list widgets. */
final class UserRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
switch ($slug) {
case 'user_card': return self::user_card($instance);
case 'author_card': return self::author_card($instance);
case 'user_ranking': return self::ranking($instance);
case 'user_list': return self::user_list($instance);
}
return '';
}
private static function user_card(array $i): string {
$user_id = get_current_user_id();
if ($user_id <= 0) {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
$register = function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url();
$oauth_html = '';
if (class_exists('AKSRT\\Auth\\OAuth')) {
$oauth = \AKSRT\Auth\OAuth::get_instance();
if (is_callable([$oauth, 'render_social_buttons'])) {
$oauth_html = (string) $oauth->render_social_buttons(__('社交账号登录', 'aksrt'));
} elseif (is_callable([$oauth, 'get_login_buttons'])) {
$oauth_html = (string) $oauth->get_login_buttons();
}
}
return '<div class="aksrt-user-card is-guest"><div class="aksrt-user-card__cover"></div><div class="aksrt-user-card__body">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html(0, 88) : get_avatar(0, 88)) . '<h3>' . esc_html__('Hi,请登录', 'aksrt') . '</h3><p>' . esc_html__('登录后管理课程、会员、订单与创作内容。', 'aksrt') . '</p><div class="aksrt-user-card__actions"><a class="aksrt-widget-button signin-loader" href="' . esc_url($login) . '">' . esc_html__('登录', 'aksrt') . '</a><a class="aksrt-widget-button is-ghost signup-loader" href="' . esc_url($register) . '">' . esc_html__('注册', 'aksrt') . '</a></div>' . ($oauth_html !== '' ? '<div class="aksrt-user-card__oauth">' . $oauth_html . '</div>' : '') . '</div></div>';
}
$user = get_userdata($user_id);
return $user instanceof \WP_User ? self::profile_card($user, $i, true) : '';
}
private static function author_card(array $i): string {
$post = get_post();
if (!$post instanceof \WP_Post) {
return '';
}
$user = get_userdata((int) $post->post_author);
return $user instanceof \WP_User ? self::profile_card($user, $i, false) : '';
}
private static function profile_card(\WP_User $user, array $i, bool $self_card): string {
$user_id = (int) $user->ID;
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
$cover = (string) get_user_meta($user_id, 'aksrt_background_image', true);
$followers = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id) : 0;
$following = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_following_count($user_id) : 0;
$html = '<article class="aksrt-user-card' . (!$self_card ? ' is-author' : '') . (empty($i['show_cover']) ? ' no-cover' : '') . '">';
if (!empty($i['show_cover'])) {
$html .= '<div class="aksrt-user-card__cover"' . ($cover ? ' style="background-image:url(' . esc_url($cover) . ')"' : '') . '></div>';
}
$html .= '<div class="aksrt-user-card__body"><a class="aksrt-user-card__avatar" href="' . esc_url($home) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($user_id, 96) : get_avatar($user_id, 96)) . '</a><h3><a href="' . esc_url($home) . '">' . esc_html($user->display_name) . '</a>' . (function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($user_id) : '') . '</h3>';
if (function_exists('aksrt_render_user_identity_chips')) {
$html .= '<div class="aksrt-user-card__identity">' . aksrt_render_user_identity_chips($user_id, 'widget') . '</div>';
}
$html .= '<p>' . esc_html(wp_trim_words((string) $user->description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
if (!empty($i['show_buttons'])) {
$html .= '<div class="aksrt-user-card__actions">' . self::action((string) $i['primary_action'], $user, true) . self::action((string) $i['secondary_action'], $user, false) . '</div>';
}
if (!$self_card && get_current_user_id() > 0 && get_current_user_id() !== $user_id) {
$active = class_exists('AKSRT\\User\\Follow') && \AKSRT\User\Follow::get_instance()->is_following(get_current_user_id(), $user_id);
$html .= '<button class="aksrt-user-card__follow' . ($active ? ' is-following' : '') . '" type="button" data-aksrt-follow="' . $user_id . '">' . esc_html($active ? __('已关注', 'aksrt') : __('关注', 'aksrt')) . '</button>';
}
$html .= '</div>';
if (!empty($i['show_posts']) && function_exists('aksrt_render_user_card_posts')) {
$html .= '<div class="aksrt-user-card__posts">' . aksrt_render_user_card_posts(['user_id' => $user_id, 'count' => 4, 'post_style' => 'mini', 'show_empty' => false]) . '</div>';
}
return $html . '</article>';
}
private static function action(string $action, \WP_User $user, bool $primary): string {
$labels = ['home' => __('个人主页', 'aksrt'), 'submit' => __('发布文章', 'aksrt'), 'vip' => __('会员中心', 'aksrt'), 'orders' => __('订单中心', 'aksrt'), 'recharge' => __('余额充值', 'aksrt'), 'signin' => __('签到', 'aksrt')];
if ($action === 'signin' && function_exists('aksrt_header_user_checkin_btn') && get_current_user_id() === (int) $user->ID) {
return '<span class="aksrt-user-card__checkin">' . aksrt_header_user_checkin_btn((int) $user->ID, true) . '</span>';
}
if ($action === 'home') {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url((int) $user->ID);
} elseif ($action === 'submit') {
$url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : home_url('/submit/');
} else {
$tab = $action === 'signin' ? 'credits' : $action;
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url($tab) : home_url('/user/');
}
if ($url === '') {
return '';
}
return '<a class="aksrt-widget-button' . (!$primary ? ' is-ghost' : '') . '" href="' . esc_url($url) . '">' . esc_html($labels[$action] ?? $labels['home']) . '</a>';
}
private static function ranking(array $i): string {
$users = get_users(['number' => 200, 'orderby' => 'registered', 'order' => 'DESC']);
$metric = (string) $i['orderby'];
usort($users, static fn(\WP_User $a, \WP_User $b): int => self::metric($b, $metric) <=> self::metric($a, $metric));
$users = array_slice($users, 0, (int) $i['number']);
if (!$users) {
return RendererHelpers::empty_state(__('暂无用户数据', 'aksrt'));
}
$html = '<ol class="aksrt-user-ranking">';
foreach ($users as $index => $user) {
$html .= '<li><span class="aksrt-user-ranking__index' . (!empty($i['show_badge']) && $index < 3 ? ' is-top' : '') . '">' . ($index + 1) . '</span>' . self::user_tile($user, self::metric($user, $metric), self::metric_label($metric)) . '</li>';
}
return $html . '</ol>';
}
private static function user_list(array $i): string {
$args = ['number' => 200];
if (!empty($i['include'])) {
$args['include'] = RendererHelpers::ids((string) $i['include']);
}
if (!empty($i['exclude'])) {
$args['exclude'] = RendererHelpers::ids((string) $i['exclude']);
}
$users = get_users($args);
$orderby = (string) $i['orderby'];
usort($users, static function (\WP_User $a, \WP_User $b) use ($orderby): int {
if ($orderby === 'display_name') {
return strcasecmp($a->display_name, $b->display_name);
}
if ($orderby === 'registered') {
return strcmp($b->user_registered, $a->user_registered);
}
return self::metric($b, $orderby) <=> self::metric($a, $orderby);
});
$html = '<div class="aksrt-widget-grid aksrt-user-list">';
foreach (array_slice($users, 0, (int) $i['number']) as $user) {
$html .= '<article>' . self::user_tile($user, self::metric($user, $orderby), self::metric_label($orderby)) . '</article>';
}
return $html . '</div>';
}
private static function user_tile(\WP_User $user, int $value, string $label): string {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url((int) $user->ID);
return '<a class="aksrt-user-tile" href="' . esc_url($url) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($user->ID, 64) : get_avatar($user->ID, 64)) . '<span><strong>' . esc_html($user->display_name) . '</strong><small>' . esc_html($label) . ' ' . number_format_i18n($value) . '</small></span></a>';
}
private static function metric(\WP_User $user, string $metric): int {
$id = (int) $user->ID;
if ($metric === 'points') {
return function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($id) : 0;
}
if ($metric === 'followers') {
return class_exists('AKSRT\\User\\Follow') ? (int) \AKSRT\User\Follow::get_instance()->get_follower_count($id) : 0;
}
if ($metric === 'post_count') {
return (int) count_user_posts($id, 'post', true);
}
$key = $metric === 'checkin_continuous_day' ? 'aksrt_checkin_continuous_day' : 'aksrt_checkin_all_day';
return (int) get_user_meta($id, $key, true);
}
private static function metric_label(string $metric): string {
$labels = ['points' => __('积分', 'aksrt'), 'followers' => __('粉丝', 'aksrt'), 'post_count' => __('文章', 'aksrt'), 'registered' => __('新加入', 'aksrt'), 'display_name' => __('用户', 'aksrt'), 'checkin_all_day' => __('累计签到', 'aksrt'), 'checkin_continuous_day' => __('连续签到', 'aksrt')];
return $labels[$metric] ?? $labels['points'];
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/** Public read-only endpoints for saved widget instances and quotes. */
final class RestController extends \WP_REST_Controller {
public function __construct() {
$this->namespace = 'aksrt/v1';
$this->rest_base = 'widgets';
}
public function register_routes(): void {
register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<widget>[a-z][a-z0-9_]*)/items', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'get_items'],
'permission_callback' => '__return_true',
'args' => [
'widget' => ['type' => 'string', 'required' => true, 'pattern' => '^[a-z][a-z0-9_]*$', 'validate_callback' => [$this, 'validate_widget']],
'instance' => ['type' => 'string', 'required' => true, 'pattern' => '^aksrt_widget_[a-z0-9_]+-[1-9][0-9]*$'],
'page' => ['type' => 'integer', 'default' => 1, 'minimum' => 1, 'maximum' => 1000],
'segment' => ['type' => 'string', 'default' => '', 'pattern' => '^[0-9]{0,2}$'],
],
]);
register_rest_route($this->namespace, '/' . $this->rest_base . '/quote', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'get_quote'],
'permission_callback' => '__return_true',
'args' => [],
]);
}
public function validate_widget($value): bool {
return is_string($value) && WidgetRegistry::get($value) !== null;
}
/** @return \WP_REST_Response|\WP_Error */
public function get_items(\WP_REST_Request $request) {
$unknown = array_diff(array_keys($request->get_params()), ['widget', 'instance', 'page', 'segment']);
if ($unknown) {
return new \WP_Error('aksrt_widget_unknown_parameter', __('请求包含不支持的参数。', 'aksrt'), ['status' => 400, 'parameters' => array_values($unknown)]);
}
$slug = (string) $request->get_param('widget');
if (!RendererRegistry::supports_items($slug)) {
return new \WP_Error('aksrt_widget_items_unsupported', __('该小工具不提供动态列表。', 'aksrt'), ['status' => 404]);
}
$instance_id = (string) $request->get_param('instance');
$instance = $this->load_instance($slug, $instance_id);
if (is_wp_error($instance)) {
return $instance;
}
$page = max(1, (int) $request->get_param('page'));
$segment = (string) $request->get_param('segment');
$payload = PostRenderer::payload($instance, $slug, $page, $segment);
return rest_ensure_response(['success' => true, 'code' => 'ok'] + $payload);
}
/** @return \WP_REST_Response|\WP_Error */
public function get_quote(\WP_REST_Request $request) {
if ($request->get_params()) {
return new \WP_Error('aksrt_widget_unknown_parameter', __('一言接口不接受查询参数。', 'aksrt'), ['status' => 400]);
}
$cached = get_transient('aksrt_widget_quote');
if (is_array($cached) && !empty($cached['text'])) {
return rest_ensure_response($cached);
}
$fallbacks = [__('保持好奇,持续创造。', 'aksrt'), __('知识因为分享而更有价值。', 'aksrt'), __('把复杂的事,做得清楚而简单。', 'aksrt')];
$data = ['text' => $fallbacks[array_rand($fallbacks)], 'author' => 'AKSRT', 'source' => 'local'];
$response = wp_remote_get('https://v1.hitokoto.cn/?encode=json', ['timeout' => 3, 'redirection' => 2, 'user-agent' => 'AKSRT Widget/1.0']);
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
$remote = json_decode((string) wp_remote_retrieve_body($response), true);
if (is_array($remote) && !empty($remote['hitokoto'])) {
$data = ['text' => sanitize_text_field((string) $remote['hitokoto']), 'author' => sanitize_text_field((string) ($remote['from_who'] ?? $remote['from'] ?? '')), 'source' => 'hitokoto'];
}
}
set_transient('aksrt_widget_quote', $data, 10 * MINUTE_IN_SECONDS);
return rest_ensure_response($data);
}
/** @return array<string,mixed>|\WP_Error */
private function load_instance(string $slug, string $instance_id) {
$definition = WidgetRegistry::get($slug);
if ($definition === null || !preg_match('/^' . preg_quote($definition->id_base(), '/') . '-([1-9][0-9]*)$/', $instance_id, $matches)) {
return new \WP_Error('aksrt_widget_invalid_instance', __('小工具实例无效。', 'aksrt'), ['status' => 400]);
}
$sidebars = wp_get_sidebars_widgets();
$registered = false;
foreach ($sidebars as $widgets) {
if (is_array($widgets) && in_array($instance_id, $widgets, true)) {
$registered = true;
break;
}
}
if (!$registered) {
return new \WP_Error('aksrt_widget_instance_not_registered', __('小工具实例未挂载。', 'aksrt'), ['status' => 404]);
}
$widget = new SchemaWidget($definition);
$settings = $widget->get_settings();
$number = (int) $matches[1];
if (!is_array($settings) || !isset($settings[$number]) || !is_array($settings[$number])) {
return new \WP_Error('aksrt_widget_instance_not_found', __('未找到小工具配置。', 'aksrt'), ['status' => 404]);
}
return $definition->normalize($settings[$number]);
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* One reusable WP_Widget implementation backed by a WidgetDefinition.
*/
final class SchemaWidget extends \WP_Widget {
private WidgetDefinition $definition;
public function __construct(WidgetDefinition $definition) {
$this->definition = $definition;
parent::__construct(
$definition->id_base(),
$definition->name(),
[
'classname' => 'aksrt-schema-widget aksrt-schema-widget--' . $definition->slug(),
'description' => $definition->description(),
'customize_selective_refresh' => true,
],
['width' => 520]
);
}
public function definition(): WidgetDefinition {
return $this->definition;
}
public function widget($args, $instance): void {
$renderer = 'AKSRT\\WidgetSystem\\FrontendRenderer';
if (class_exists($renderer) && is_callable([$renderer, 'render'])) {
$renderer::render($this->definition, (array) $args, $this->definition->normalize((array) $instance), $this);
}
}
public function form($instance): void {
$instance = array_merge($this->definition->defaults(), is_array($instance) ? $instance : []);
if ($this->should_lazy_load()) {
$this->render_lazy_placeholder();
return;
}
FormRenderer::render($this, $this->definition->fields(), $instance);
}
public function render_full_form(array $instance): void {
FormRenderer::render($this, $this->definition->fields(), array_merge($this->definition->defaults(), $instance));
}
public function update($new_instance, $old_instance): array {
return $this->definition->normalize(is_array($new_instance) ? $new_instance : []);
}
private function should_lazy_load(): bool {
if (wp_doing_ajax() && isset($_REQUEST['action']) && $_REQUEST['action'] === AdminFormController::AJAX_ACTION) {
return false;
}
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
return $screen && in_array($screen->id, ['widgets', 'customize'], true);
}
private function render_lazy_placeholder(): void {
printf(
'<div class="aksrt-widget-form-lazy" data-id-base="%1$s" data-widget-number="%2$s"><p class="aksrt-widget-form-lazy__status">%3$s</p><button type="button" class="button aksrt-widget-form-lazy__load">%4$s</button></div>',
esc_attr($this->id_base),
esc_attr((string) $this->number),
esc_html__('配置将在展开或添加模块后加载。', 'aksrt'),
esc_html__('加载配置', 'aksrt')
);
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Canonical catalog of the 37 modern AKSRT widgets.
*/
final class WidgetCatalog {
public static function definitions(): array {
$groups = [
'general' => [
['search', '搜索框', '显示多种样式的站内搜索框', 'sidebar'],
['posts_toc', '文章目录树', '显示当前文章的标题目录', 'sidebar'],
['quote', '一言', '定时切换本地或在线句子', 'sidebar'],
['icon_cards', '图标卡片(竖版)', '图标在文字上方的特色卡片', 'wide'],
['video', '视频', '支持常规视频与流媒体格式', 'any'],
['embed', '超级嵌入', '按比例嵌入外部在线内容', 'any'],
['media_cards', '图文视频封面卡片', '显示图片、视频和文字封面卡片', 'wide'],
['icon_cover_cards', '图标卡片(横版)', '图标在文字左侧的特色卡片', 'wide'],
['partners', '横向滚动合作伙伴', '多行自动滚动的合作伙伴或案例列表', 'wide'],
['combo_info', '复合信息卡片', '组合图标、标签、简介和按钮的信息卡', 'wide'],
['text_title', '文字标题', '独立的标题与简介模块', 'wide'],
['buttons', '按钮组', '单个或多个跳转按钮', 'wide'],
['layout_gap', '布局额外间距', '增加 PC 与移动端模块间距', 'wide'],
['tag_cloud', '标签云', '彩色显示标签或分类', 'sidebar'],
['recent_comments', '最近评论', '显示网站最新评论', 'sidebar'],
['links', '链接列表', '友情链接和网址导航列表', 'wide'],
['notice', '上下滚动公告', '自动滚动的公告信息列表', 'wide'],
],
'content' => [
['term_cards', '分类图文卡片', '将分类或标签显示为图文卡片', 'wide'],
['term_aggregation', '分类与标签聚合卡片', '聚合分类、标签与文章内容', 'wide'],
['hot_posts', '热榜文章', '按浏览、评论、收藏等维度排行', 'sidebar'],
['post_pay', '付费购买', '显示当前文章的知识付费购买模块', 'sidebar'],
['posts', '文章列表', '多条件筛选和多样式文章列表', 'wide'],
['tab_posts', '多栏目文章', '通过多个栏目切换文章列表', 'wide'],
['oneline_posts', '横向滚动文章', '单行横向滚动文章列表', 'wide'],
],
'knowledge_product' => [
['pricing_plans', '知识产品方案卡片', '课程与会员方案价格、权益和购买入口', 'wide'],
['reviews', '知识产品评价', '横向滑动的学员或会员评价', 'wide'],
['faq', '知识产品常见问题', '支持搜索的常见问题与帮助卡片', 'wide'],
['stats', '知识产品数据展示', '横排数字统计展示模块', 'wide'],
['highlights', '知识产品亮点卡片', '多尺寸知识产品亮点聚合', 'wide'],
['feature_intro', '知识产品图文亮点', '可切换媒体的左右图文介绍', 'wide'],
['tilt_cases', '知识产品 3D 展示', '带 3D 交互的知识产品案例展示', 'wide'],
],
'slider' => [
['slider', '幻灯片', '支持图片、视频、文字和视差图层', 'wide'],
['slider_media_combo', '幻灯片加图文视频卡片', '幻灯片与图文视频卡片组合布局', 'wide'],
],
'user' => [
['user_card', '用户个人信息', '根据登录状态显示登录入口或用户信息', 'sidebar'],
['author_card', '文章作者信息', '显示当前文章作者与关注入口', 'sidebar'],
['user_ranking', '用户排行榜', '按签到、积分或粉丝生成排行', 'sidebar'],
['user_list', '用户列表', '筛选并显示注册用户列表', 'wide'],
],
];
$definitions = [];
foreach ($groups as $group => $items) {
foreach ($items as [$slug, $name, $description, $size]) {
$definitions[] = [
'slug' => $slug,
'name' => 'AKSRT ' . $name,
'description' => $description,
'group' => $group,
'renderer' => $slug,
'size' => $size,
'common' => [
'title' => !in_array($slug, ['post_pay', 'layout_gap'], true),
'affix' => $size === 'sidebar',
],
'fields' => WidgetSchemas::for($slug),
'assets' => self::assets($slug),
];
}
}
return $definitions;
}
private static function assets(string $slug): array {
if (in_array($slug, ['slider', 'slider_media_combo', 'reviews', 'oneline_posts'], true)) {
return ['swiper'];
}
if ($slug === 'video') {
return ['dplayer', 'hls', 'flv', 'dash'];
}
if ($slug === 'tilt_cases') {
return ['tilt'];
}
return [];
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Immutable description of one schema-driven widget.
*/
final class WidgetDefinition {
private string $slug;
private string $name;
private string $description;
private string $group;
private string $renderer;
private string $size;
private array $fields;
private array $common;
private array $assets;
public function __construct(array $definition) {
$slug = (string) ($definition['slug'] ?? '');
if (!preg_match('/^[a-z][a-z0-9_]*$/', $slug)) {
throw new \InvalidArgumentException('Invalid widget slug: ' . $slug);
}
$name = trim((string) ($definition['name'] ?? ''));
if ($name === '') {
throw new \InvalidArgumentException('Widget name is required for ' . $slug);
}
$this->slug = $slug;
$this->name = $name;
$this->description = trim((string) ($definition['description'] ?? ''));
$this->group = (string) ($definition['group'] ?? 'general');
$this->renderer = (string) ($definition['renderer'] ?? $slug);
$this->size = in_array(($definition['size'] ?? 'any'), ['any', 'wide', 'sidebar'], true)
? (string) $definition['size']
: 'any';
$this->fields = array_values((array) ($definition['fields'] ?? []));
$this->common = array_merge(
[
'title' => true,
'affix' => false,
'visibility' => true,
'animation' => true,
'layout' => true,
],
(array) ($definition['common'] ?? [])
);
$this->assets = array_values(array_unique(array_map('strval', (array) ($definition['assets'] ?? []))));
}
public function slug(): string {
return $this->slug;
}
public function id_base(): string {
return 'aksrt_widget_' . $this->slug;
}
public function name(): string {
return $this->name;
}
public function description(): string {
return $this->description;
}
public function group(): string {
return $this->group;
}
public function renderer(): string {
return $this->renderer;
}
public function size(): string {
return $this->size;
}
public function assets(): array {
return $this->assets;
}
public function fields(): array {
return array_merge(CommonFields::for_definition($this->common), $this->fields);
}
public function defaults(): array {
return FieldSchema::defaults($this->fields());
}
public function normalize(array $input): array {
return FieldSchema::normalize($this->fields(), $input);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Lazy, immutable registry for all modern widgets.
*/
final class WidgetRegistry {
/** @var array<string, WidgetDefinition>|null */
private static ?array $definitions = null;
/** @return array<string, WidgetDefinition> */
public static function all(): array {
if (self::$definitions === null) {
self::$definitions = [];
foreach (WidgetCatalog::definitions() as $raw) {
$definition = new WidgetDefinition($raw);
if (isset(self::$definitions[$definition->slug()])) {
throw new \LogicException('Duplicate widget slug: ' . $definition->slug());
}
self::$definitions[$definition->slug()] = $definition;
}
}
return self::$definitions;
}
public static function get(string $slug): ?WidgetDefinition {
return self::all()[$slug] ?? null;
}
public static function get_by_id_base(string $id_base): ?WidgetDefinition {
$prefix = 'aksrt_widget_';
if (!str_starts_with($id_base, $prefix)) {
return null;
}
return self::get(substr($id_base, strlen($prefix)));
}
public static function count(): int {
return count(self::all());
}
public static function reset(): void {
self::$definitions = null;
}
}
+309
View File
@@ -0,0 +1,309 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Business field schemas. Rendering behavior is implemented in phase 2.
*/
final class WidgetSchemas {
public static function for(string $slug): array {
if (in_array($slug, ['term_cards', 'term_aggregation', 'hot_posts', 'posts', 'tab_posts', 'oneline_posts'], true)) {
return self::content($slug);
}
if ($slug === 'post_pay') {
return [
['id' => 'theme', 'type' => 'palette', 'label' => '色彩主题', 'default' => 'jb-red', 'options' => CommonFields::palette()],
];
}
if (in_array($slug, ['pricing_plans', 'reviews', 'faq', 'stats', 'highlights', 'feature_intro', 'tilt_cases'], true)) {
return self::knowledge_product($slug);
}
if (in_array($slug, ['slider', 'slider_media_combo'], true)) {
return self::slider($slug);
}
if (in_array($slug, ['user_card', 'author_card', 'user_ranking', 'user_list'], true)) {
return self::user($slug);
}
return self::general($slug);
}
private static function general(string $slug): array {
switch ($slug) {
case 'search':
return [
['id' => 'placeholder', 'type' => 'text', 'label' => '提示文字', 'default' => '输入关键词搜索'],
['id' => 'search_style', 'type' => 'radio', 'label' => '显示样式', 'default' => 'button', 'options' => ['button' => '按钮式', 'compact' => '紧凑式', 'large' => '大尺寸']],
];
case 'posts_toc':
return [
['id' => 'min_headings', 'type' => 'spinner', 'label' => '最少标题数量', 'default' => 3, 'min' => 1, 'max' => 20],
['id' => 'collapsed', 'type' => 'switcher', 'label' => '默认收起', 'default' => false],
['id' => 'offset', 'type' => 'spinner', 'label' => '滚动偏移', 'default' => 70, 'min' => 0, 'max' => 300],
];
case 'quote':
return [
['id' => 'source', 'type' => 'select', 'label' => '内容来源', 'default' => 'local', 'options' => ['local' => '本地句子', 'api' => '在线一言']],
['id' => 'quotes', 'type' => 'textarea', 'label' => '本地句子', 'description' => '每行一句', 'default' => "保持好奇,持续创造。\n知识因为分享而更有价值。"],
['id' => 'interval', 'type' => 'spinner', 'label' => '自动更新间隔(秒)', 'default' => 30, 'min' => 5, 'max' => 300],
];
case 'video':
return [
['id' => 'url', 'type' => 'upload', 'label' => '视频地址', 'default' => ''],
['id' => 'poster', 'type' => 'upload', 'label' => '视频封面', 'default' => ''],
['id' => 'autoplay', 'type' => 'switcher', 'label' => '自动播放', 'default' => false],
['id' => 'muted', 'type' => 'switcher', 'label' => '静音', 'default' => false],
['id' => 'ratio', 'type' => 'select', 'label' => '长宽比例', 'default' => '16:9', 'options' => ['16:9' => '16:9', '4:3' => '4:3', '1:1' => '1:1', 'auto' => '自适应']],
];
case 'embed':
return [
['id' => 'url', 'type' => 'upload', 'label' => '嵌入地址', 'default' => ''],
['id' => 'ratio', 'type' => 'slider', 'label' => '高度比例(%)', 'default' => 56.25, 'min' => 20, 'max' => 200, 'step' => 0.25],
['id' => 'allow_fullscreen', 'type' => 'switcher', 'label' => '允许全屏', 'default' => true],
];
case 'layout_gap':
return [
['id' => 'pc_gap', 'type' => 'slider', 'label' => 'PC 端间隔', 'default' => 20, 'min' => 0, 'max' => 300],
['id' => 'm_gap', 'type' => 'slider', 'label' => '移动端间隔', 'default' => 10, 'min' => 0, 'max' => 200],
];
case 'tag_cloud':
return [
['id' => 'taxonomy', 'type' => 'select', 'label' => '分类法', 'default' => 'post_tag', 'options' => ['post_tag' => '标签', 'category' => '分类']],
['id' => 'number', 'type' => 'spinner', 'label' => '数量', 'default' => 20, 'min' => 1, 'max' => 100],
['id' => 'orderby', 'type' => 'select', 'label' => '排序', 'default' => 'count', 'options' => ['count' => '内容数量', 'name' => '名称', 'term_id' => 'ID', 'rand' => '随机']],
['id' => 'show_count', 'type' => 'switcher', 'label' => '显示数量', 'default' => false],
];
case 'recent_comments':
return [
['id' => 'count', 'type' => 'spinner', 'label' => '显示数量', 'default' => 6, 'min' => 1, 'max' => 50],
['id' => 'exclude_users', 'type' => 'text', 'label' => '排除用户 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'exclude_posts', 'type' => 'text', 'label' => '排除文章 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'show_source', 'type' => 'switcher', 'label' => '显示评论来源', 'default' => true],
];
case 'links':
return [
['id' => 'style', 'type' => 'button_set', 'label' => '样式', 'default' => 'card', 'options' => ['card' => '卡片', 'bigcard' => '大卡片', 'image' => '图片', 'simple' => '简约']],
['id' => 'limit', 'type' => 'spinner', 'label' => '最大数量', 'default' => 12, 'min' => 1, 'max' => 100],
['id' => 'links', 'type' => 'group', 'label' => '链接列表', 'max_items' => 100, 'fields' => self::card_fields(true)],
];
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)],
];
case 'text_title':
return [
['id' => 'heading', 'type' => 'textarea', 'label' => '标题', 'default' => ''],
['id' => 'description', 'type' => 'textarea', 'label' => '简介', 'default' => ''],
['id' => 'alignment', 'type' => 'button_set', 'label' => '对齐', 'default' => 'center', 'options' => ['left' => '左', 'center' => '中', 'right' => '右']],
];
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()],
];
case 'partners':
return [
['id' => 'rows_pc', 'type' => 'spinner', 'label' => 'PC 行数', 'default' => 2, 'min' => 1, 'max' => 5],
['id' => 'rows_mobile', 'type' => 'spinner', 'label' => '移动端行数', 'default' => 1, 'min' => 1, 'max' => 4],
['id' => 'speed', 'type' => 'spinner', 'label' => '滚动速度', 'default' => 30, 'min' => 5, 'max' => 120],
['id' => 'items', 'type' => 'group', 'label' => '合作伙伴', 'max_items' => 60, 'fields' => self::media_card_fields()],
];
case 'combo_info':
return [['id' => 'items', 'type' => 'group', 'label' => '信息卡片', 'max_items' => 30, 'fields' => array_merge(self::media_card_fields(), [['id' => 'tags', 'type' => 'text', 'label' => '标签', 'default' => '']])]];
case 'icon_cards':
case 'icon_cover_cards':
return [
['id' => 'pc_row', 'type' => 'spinner', 'label' => 'PC 单行数量', 'default' => 4, 'min' => 1, 'max' => 12],
['id' => 'm_row', 'type' => 'spinner', 'label' => '移动端单行数量', 'default' => 2, 'min' => 1, 'max' => 6],
['id' => 'cards', 'type' => 'group', 'label' => '卡片', 'max_items' => 40, 'fields' => self::icon_card_fields()],
];
case 'media_cards':
default:
return [['id' => 'cards', 'type' => 'group', 'label' => '卡片', 'max_items' => 40, 'fields' => self::media_card_fields()]];
}
}
private static function content(string $slug): array {
if ($slug === 'term_cards' || $slug === 'term_aggregation') {
return [
['id' => 'taxonomy', 'type' => 'select', 'label' => '分类法', 'default' => 'category', 'options' => ['category' => '分类', 'post_tag' => '标签']],
['id' => 'term_ids', 'type' => 'text', 'label' => '分类/标签 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'count', 'type' => 'spinner', 'label' => '数量', 'default' => 8, 'min' => 1, 'max' => 50],
['id' => 'pc_row', 'type' => 'spinner', 'label' => 'PC 单行数量', 'default' => 4, 'min' => 1, 'max' => 12],
['id' => 'm_row', 'type' => 'spinner', 'label' => '移动端单行数量', 'default' => 2, 'min' => 1, 'max' => 6],
];
}
$fields = self::post_query_fields();
if ($slug === 'hot_posts') {
$fields[] = ['id' => 'show_thumbnail', 'type' => 'switcher', 'label' => '显示缩略图', 'default' => true];
}
if ($slug === 'oneline_posts') {
$fields[] = ['id' => 'autoplay', 'type' => 'switcher', 'label' => '自动滚动', 'default' => true];
}
if ($slug === 'tab_posts') {
$fields[] = [
'id' => 'tabs', 'type' => 'group', 'label' => '栏目', 'max_items' => 10,
'fields' => [
['id' => 'title', 'type' => 'text', 'label' => '栏目标题', 'default' => ''],
['id' => 'category_id', 'type' => 'number', 'label' => '分类 ID', 'default' => 0, 'min' => 0],
['id' => 'tag_ids', 'type' => 'text', 'label' => '标签 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'pay_type', 'type' => 'select', 'label' => '付费类型', 'default' => '', 'options' => self::pay_types()],
],
];
}
return $fields;
}
private static function post_query_fields(): array {
return [
['id' => 'category_ids', 'type' => 'text', 'label' => '分类 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'tag_ids', 'type' => 'text', 'label' => '标签 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'pay_type', 'type' => 'select', 'label' => '付费内容类型', 'default' => '', 'options' => self::pay_types()],
['id' => 'limit_day', 'type' => 'spinner', 'label' => '最近天数', 'default' => 0, 'min' => 0, 'max' => 3650],
['id' => 'orderby', 'type' => 'select', 'label' => '排序', 'default' => 'date', 'options' => ['date' => '发布时间', 'modified' => '更新时间', 'views' => '浏览量', 'comments' => '评论数', 'likes' => '点赞数', 'favorites' => '收藏数', 'downloads' => '下载量', 'rand' => '随机']],
['id' => 'order', 'type' => 'radio', 'label' => '顺序', 'default' => 'DESC', 'options' => ['DESC' => '降序', 'ASC' => '升序']],
['id' => 'count', 'type' => 'spinner', 'label' => '数量', 'default' => 8, 'min' => 1, 'max' => 50],
['id' => 'style', 'type' => 'button_set', 'label' => '列表样式', 'default' => 'card', 'options' => ['list' => '列表', 'card' => '卡片', 'mini' => 'Mini']],
['id' => 'paginate', 'type' => 'select', 'label' => '翻页方式', 'default' => 'none', 'options' => ['none' => '不翻页', 'numbered' => '数字分页', 'load_more' => '加载更多', 'infinite' => '无限加载']],
['id' => 'load_mode', 'type' => 'select', 'label' => '加载方式', 'default' => 'sync', 'options' => ['sync' => '同步', 'ajax' => 'AJAX 延迟加载']],
];
}
private static function knowledge_product(string $slug): array {
$base = [
['id' => 'pc_row', 'type' => 'spinner', 'label' => 'PC 单行数量', 'default' => 3, 'min' => 1, 'max' => 6],
['id' => 'm_row', 'type' => 'spinner', 'label' => '移动端单行数量', 'default' => 1, 'min' => 1, 'max' => 3],
];
if ($slug === 'pricing_plans') {
$base[] = ['id' => 'plans', 'type' => 'group', 'label' => '课程/会员方案', 'max_items' => 12, 'fields' => [
['id' => 'title', 'type' => 'text', 'label' => '方案名称', 'default' => ''],
['id' => 'price', 'type' => 'text', 'label' => '价格', 'default' => ''],
['id' => 'description', 'type' => 'textarea', 'label' => '简介', 'default' => ''],
['id' => 'features', 'type' => 'textarea', 'label' => '权益(每行一项)', 'default' => ''],
['id' => 'cta_type', 'type' => 'select', 'label' => '按钮类型', 'default' => 'link', 'options' => ['link' => '链接', 'post' => '付费文章', 'vip' => '会员方案']],
['id' => 'post_id', 'type' => 'number', 'label' => '文章 ID', 'default' => 0, 'min' => 0],
['id' => 'vip_level', 'type' => 'number', 'label' => '会员等级', 'default' => 1, 'min' => 1, 'max' => 20],
['id' => 'link', 'type' => 'link', 'label' => '按钮链接', 'default' => []],
['id' => 'featured', 'type' => 'switcher', 'label' => '推荐方案', 'default' => false],
]];
return $base;
}
if ($slug === 'faq') {
$base[] = ['id' => 'search', 'type' => 'switcher', 'label' => '显示搜索', 'default' => true];
$base[] = ['id' => 'items', 'type' => 'group', 'label' => '问答', 'max_items' => 60, 'fields' => [['id' => 'question', 'type' => 'text', 'label' => '问题', 'default' => ''], ['id' => 'answer', 'type' => 'textarea', 'label' => '回答', 'default' => '', 'allow_html' => true]]];
return $base;
}
$base[] = ['id' => 'items', 'type' => 'group', 'label' => '内容项', 'max_items' => 40, 'fields' => array_merge(self::media_card_fields(), [
['id' => 'value', 'type' => 'text', 'label' => '数值/徽标', 'default' => ''],
['id' => 'features', 'type' => 'textarea', 'label' => '特性', 'default' => ''],
['id' => 'rating', 'type' => 'slider', 'label' => '评分', 'default' => 5, 'min' => 1, 'max' => 5],
])];
return $base;
}
private static function slider(string $slug): array {
$fields = [
['id' => 'direction', 'type' => 'radio', 'label' => '方向', 'default' => 'horizontal', 'options' => ['horizontal' => '横向', 'vertical' => '纵向']],
['id' => 'effect', 'type' => 'select', 'label' => '切换效果', 'default' => 'slide', 'options' => ['slide' => '滑动', 'fade' => '淡入', 'cube' => '立方体', 'coverflow' => '封面流']],
['id' => 'pc_height', 'type' => 'spinner', 'label' => 'PC 高度', 'default' => 400, 'min' => 120, 'max' => 1000],
['id' => 'm_height', 'type' => 'spinner', 'label' => '移动端高度', 'default' => 240, 'min' => 100, 'max' => 800],
['id' => 'spacebetween', 'type' => 'spinner', 'label' => '间距', 'default' => 15, 'min' => 0, 'max' => 100],
['id' => 'speed', 'type' => 'spinner', 'label' => '切换速度(毫秒)', 'default' => 500, 'min' => 100, 'max' => 5000],
['id' => 'interval', 'type' => 'spinner', 'label' => '自动播放间隔(秒)', 'default' => 4, 'min' => 1, 'max' => 60],
['id' => 'autoplay', 'type' => 'switcher', 'label' => '自动播放', 'default' => true],
['id' => 'loop', 'type' => 'switcher', 'label' => '循环', 'default' => true],
['id' => 'button', 'type' => 'switcher', 'label' => '显示切换按钮', 'default' => true],
['id' => 'pagination', 'type' => 'switcher', 'label' => '显示分页器', 'default' => true],
['id' => 'slides', 'type' => 'group', 'label' => '幻灯片', 'max_items' => 30, 'fields' => [
['id' => 'background', 'type' => 'upload', 'label' => '背景图片', 'default' => ''],
['id' => 'background_video', 'type' => 'upload', 'label' => '背景视频', 'default' => ''],
['id' => 'hide_mobile', 'type' => 'switcher', 'label' => '移动端隐藏', 'default' => false],
['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []],
['id' => 'title', 'type' => 'text', 'label' => '标题', 'default' => ''],
['id' => 'description', 'type' => 'textarea', 'label' => '说明', 'default' => ''],
['id' => 'text_align', 'type' => 'select', 'label' => '文字位置', 'default' => 'left-bottom', 'options' => ['left-top' => '左上', 'left-center' => '左中', 'left-bottom' => '左下', 'center' => '居中', 'right-bottom' => '右下']],
['id' => 'layers', 'type' => 'group', 'label' => '图片图层', 'max_items' => 12, 'fields' => [['id' => 'image', 'type' => 'upload', 'label' => '图片', 'default' => ''], ['id' => 'parallax', 'type' => 'spinner', 'label' => '视差', 'default' => 0, 'min' => -300, 'max' => 300], ['id' => 'scale', 'type' => 'slider', 'label' => '缩放', 'default' => 100, 'min' => 10, 'max' => 300]]],
]],
];
if ($slug === 'slider_media_combo') {
$fields[] = ['id' => 'side_cards', 'type' => 'group', 'label' => '右侧图文视频卡片', 'max_items' => 6, 'fields' => self::media_card_fields()];
}
return $fields;
}
private static function user(string $slug): array {
if ($slug === 'user_ranking') {
return [
['id' => 'orderby', 'type' => 'select', 'label' => '榜单类型', 'default' => 'points', 'options' => ['checkin_all_day' => '累计签到', 'checkin_continuous_day' => '连续签到', 'points' => '积分', 'followers' => '粉丝']],
['id' => 'number', 'type' => 'spinner', 'label' => '显示数量', 'default' => 8, 'min' => 1, 'max' => 50],
['id' => 'show_badge', 'type' => 'switcher', 'label' => '显示前三徽标', 'default' => true],
];
}
if ($slug === 'user_list') {
return [
['id' => 'number', 'type' => 'spinner', 'label' => '显示数量', 'default' => 12, 'min' => 1, 'max' => 100],
['id' => 'include', 'type' => 'text', 'label' => '包含用户 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'exclude', 'type' => 'text', 'label' => '排除用户 ID', 'default' => '', 'sanitize' => 'ids'],
['id' => 'orderby', 'type' => 'select', 'label' => '排序', 'default' => 'registered', 'options' => ['registered' => '注册时间', 'display_name' => '名称', 'post_count' => '文章数', 'points' => '积分', 'followers' => '粉丝']],
];
}
return [
['id' => 'show_cover', 'type' => 'switcher', 'label' => '显示封面', 'default' => true],
['id' => 'show_posts', 'type' => 'switcher', 'label' => '显示文章', 'default' => $slug === 'author_card'],
['id' => 'show_buttons', 'type' => 'switcher', 'label' => '显示快捷按钮', 'default' => true],
['id' => 'primary_action', 'type' => 'select', 'label' => '主按钮', 'default' => 'submit', 'options' => self::user_actions()],
['id' => 'secondary_action', 'type' => 'select', 'label' => '次按钮', 'default' => 'home', 'options' => self::user_actions()],
];
}
private static function icon_card_fields(): array {
return [
['id' => 'title', 'type' => 'text', 'label' => '标题', 'default' => ''],
['id' => 'description', 'type' => 'textarea', 'label' => '说明', 'default' => ''],
['id' => 'icon', 'type' => 'icon', 'label' => '图标', 'default' => 'fa fa-star'],
['id' => 'custom_icon', 'type' => 'upload', 'label' => '自定义图标', 'default' => ''],
['id' => 'color', 'type' => 'palette', 'label' => '颜色', 'default' => 'jb-blue', 'options' => CommonFields::palette()],
['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []],
];
}
private static function media_card_fields(): array {
return [
['id' => 'title', 'type' => 'text', 'label' => '标题', 'default' => ''],
['id' => 'description', 'type' => 'textarea', 'label' => '说明', 'default' => ''],
['id' => 'image', 'type' => 'upload', 'label' => '图片', 'default' => ''],
['id' => 'image_dark', 'type' => 'upload', 'label' => '深色图片', 'default' => ''],
['id' => 'video', 'type' => 'upload', 'label' => '视频', 'default' => ''],
['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []],
];
}
private static function card_fields(bool $link = false): array {
$fields = [['id' => 'title', 'type' => 'text', 'label' => '标题', 'default' => ''], ['id' => 'description', 'type' => 'textarea', 'label' => '说明', 'default' => '']];
if ($link) {
$fields[] = ['id' => 'link', 'type' => 'link', 'label' => '链接', 'default' => []];
}
return $fields;
}
private static function button_fields(): 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' => []],
];
}
private static function pay_types(): array {
return ['' => '全部', 'reading' => '付费阅读', 'download' => '付费下载', 'gallery' => '付费图库', 'video' => '付费视频', 'vip' => 'VIP 专属'];
}
private static function user_actions(): array {
return ['home' => '个人主页', 'submit' => '前台投稿', 'vip' => '会员中心', 'orders' => '订单中心', 'recharge' => '余额充值', 'signin' => '签到'];
}
}