Files
wordpress-theme-aksrt/src/WidgetSystem/FormRenderer.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

329 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Jio\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="jio-widget-form" data-jio-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="' . esc_attr(self::field_class($field, $type)) . '" data-field-id="' . esc_attr($id) . '"';
if ($dependency !== '') {
echo ' data-dependency="' . esc_attr($dependency) . '"';
}
echo '>';
if (!empty($field['label'])) {
echo '<label class="jio-widget-field__label" for="' . esc_attr($field_id) . '">' . esc_html((string) $field['label']) . '</label>';
}
if (!empty($field['subtitle'])) {
echo '<p class="jio-widget-field__subtitle">' . esc_html((string) $field['subtitle']) . '</p>';
}
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':
case 'image_select':
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 'terms':
self::terms($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':
$rows = max(1, (int) ($field['attributes']['rows'] ?? 4));
echo '<textarea class="widefat" rows="' . $rows . '" 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, (string) ($field['library'] ?? ''));
break;
case 'color':
echo '<input class="widefat jio-widget-color" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '">';
break;
case 'icon':
echo jio_icon_picker_field($field_name, $field_id, (string) $value);
break;
case 'link':
self::link($widget, $key, is_array($value) ? $value : []);
break;
case 'spacing':
self::spacing($widget, $key, is_array($value) ? $value : [], (array) ($field['edges'] ?? []));
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'] ?? '');
$class = self::field_class($field, $type);
if ($type === 'submessage') {
$style = in_array($field['style'] ?? '', ['success', 'info', 'warning', 'danger'], true) ? $field['style'] : 'normal';
$class .= ' jio-widget-field--submessage-' . $style;
}
echo '<div class="' . esc_attr($class) . '">' . wp_kses_post($content) . '</div>';
}
/** Reference CSF row classes: type modifier plus schema `class` (e.g. compact) and `inline`. */
private static function field_class(array $field, string $type): string {
$classes = ['jio-widget-field', 'jio-widget-field--' . $type];
foreach (preg_split('/\s+/', trim((string) ($field['class'] ?? ''))) ?: [] as $extra) {
if ($extra !== '') {
$classes[] = 'is-' . sanitize_html_class($extra);
}
}
if (!empty($field['inline'])) {
$classes[] = 'is-inline';
}
return implode(' ', $classes);
}
private static function switcher(string $id, string $name, bool $value): void {
echo '<input type="hidden" name="' . esc_attr($name) . '" value="0">';
echo '<label class="jio-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="jio-widget-choices jio-widget-choices--' . esc_attr($type) . '">';
foreach ($options as $key => $label) {
$option_id = $id . '-' . sanitize_html_class((string) $key);
$image = is_array($label) ? (string) ($label['image'] ?? $label['url'] ?? '') : (string) $label;
$text = is_array($label) ? (string) ($label['label'] ?? $label['title'] ?? $key) : (string) $label;
if ($type === 'image_select' && $text === $image) $text = (string) $key;
if ($type === 'image_select' && filter_var($image, FILTER_VALIDATE_URL)) {
$body = '<img src="' . esc_url($image) . '" alt="' . esc_attr($text) . '"><small>' . esc_html($text) . '</small>';
} elseif ($type === 'palette') {
$body = '<span class="jio-widget-palette-choice"><i aria-hidden="true" style="background:' . esc_attr(self::palette_color((string) $key)) . '"></i><small>' . esc_html($text) . '</small></span>';
} else {
$body = esc_html($text);
}
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>' . $body . '</span></label>';
}
echo '</div>';
}
private static function palette_color(string $key): string {
$colors = [
'' => '#fff', 'c-red' => 'rgba(255,84,115,.4)', 'c-red-2' => 'rgba(194,41,46,.4)',
'c-yellow' => 'rgba(255,111,6,.4)', 'c-yellow-2' => 'rgba(179,103,8,.4)', 'c-cyan' => 'rgba(8,196,193,.4)',
'c-blue' => 'rgba(41,151,247,.4)', 'c-blue-2' => 'rgba(77,130,249,.4)', 'c-green' => 'rgba(18,185,40,.4)',
'c-green-2' => 'rgba(72,135,24,.4)', 'c-purple' => 'rgba(213,72,245,.4)', 'c-purple-2' => 'rgba(154,72,245,.4)',
'b-red' => '#f74b3d', 'b-yellow' => '#f3920a', 'b-cyan' => '#08c4c1', 'b-blue' => '#0a8cf3',
'b-green' => '#1fd05a', 'b-purple' => '#c133f5', 'b-black' => '#121517',
'jb-red' => 'linear-gradient(135deg,#ffbeb4 10%,#f61a1a 100%)', 'jb-pink' => 'linear-gradient(135deg,#ff5e7f 30%,#ff967e 100%)',
'jb-yellow' => 'linear-gradient(135deg,#ffd6b2 10%,#ff651c 100%)', 'jb-cyan' => 'linear-gradient(140deg,#039ab3 10%,#58dbcf 90%)',
'jb-blue' => 'linear-gradient(135deg,#b6e6ff 10%,#198aff 100%)', 'jb-green' => 'linear-gradient(135deg,#ccffcd 10%,#52bb51 100%)',
'jb-purple' => 'linear-gradient(135deg,#fec2ff 10%,#d000de 100%)', 'jb-vip1' => 'linear-gradient(25deg,#eab869 10%,#fbecd4 60%,#ffe0ae 100%)',
'jb-vip2' => 'linear-gradient(317deg,#4d4c4c 30%,#878787 70%,#5f5c5c 100%)',
'focus-color' => 'linear-gradient(135deg,#74d5f6,#9866ff)', 'key-color' => '#333', 'muted-color' => '#777',
'muted-2-color' => '#999', 'muted-3-color' => '#b1b1b1',
];
return $colors[$key] ?? '#dcdcde';
}
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';
$unit = trim((string) ($field['unit'] ?? ''));
echo '<div class="jio-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>' . ($unit !== '' ? '<span class="jio-widget-number__unit">' . esc_html($unit) . '</span>' : '') . '</div>';
}
private static function terms(string $id, string $name, array $field, $value): void {
$taxonomy = (string) ($field['taxonomy'] ?? 'category');
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 500, 'orderby' => 'name']);
if (is_wp_error($terms)) {
$terms = [];
}
$selected = array_values(array_filter(array_map('absint', (array) $value)));
$by_id = [];
foreach ($terms as $term) {
$by_id[(int) $term->term_id] = $term;
}
echo '<div class="jio-widget-terms" data-jio-terms="' . esc_attr($taxonomy) . '">';
echo '<input type="hidden" class="jio-widget-terms__value" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr(implode(',', $selected)) . '">';
echo '<div class="jio-widget-terms__selected">';
foreach ($selected as $term_id) {
if (!isset($by_id[$term_id])) {
continue;
}
echo '<span class="jio-widget-terms__chip" data-term-id="' . esc_attr((string) $term_id) . '"><span class="dashicons dashicons-move" aria-hidden="true"></span>' . esc_html((string) $by_id[$term_id]->name) . '<button type="button" class="button-link-delete jio-widget-terms__remove" aria-label="' . esc_attr__('移除', 'jio') . '">×</button></span>';
}
echo '</div>';
echo '<input type="search" class="widefat jio-widget-terms__search" placeholder="' . esc_attr__('输入关键词筛选', 'jio') . '" aria-label="' . esc_attr__('筛选可选分类', 'jio') . '">';
echo '<div class="jio-widget-terms__options">';
foreach ($terms as $term) {
$term_id = (int) $term->term_id;
echo '<label class="jio-widget-terms__option" data-term-name="' . esc_attr((string) $term->name) . '"><input type="checkbox" value="' . esc_attr((string) $term_id) . '" ' . checked(in_array($term_id, $selected, true), true, false) . '> ' . esc_html((string) $term->name) . '</label>';
}
echo '</div></div>';
}
private static function upload(string $id, string $name, string $value, string $library = ''): void {
$library = in_array($library, ['image', 'video'], true) ? $library : '';
echo '<div class="jio-widget-upload" data-media-library="' . esc_attr($library) . '">';
// 参考主题:视频上传字段不展示预览图(视频 URL 作为 img src 会得到破图)。
if ($value !== '' && $library !== 'video') {
echo '<img src="' . esc_url($value) . '" alt="">';
}
echo '<input class="widefat" type="url" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '">';
echo '<button type="button" class="button jio-widget-upload__choose">' . esc_html($library === 'video' ? __('选择视频', 'jio') : __('选择图片', 'jio')) . '</button> <button type="button" class="button-link-delete jio-widget-upload__remove">' . esc_html__('移除', 'jio') . '</button></div>';
}
private static function link(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="jio-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__('新窗口打开', 'jio') . '</label></div>';
}
private static function spacing(SchemaWidget $widget, string $key, array $value, array $edges = []): void {
$all = ['top' => 'dashicons-arrow-up-alt', 'right' => 'dashicons-arrow-right-alt', 'bottom' => 'dashicons-arrow-down-alt', 'left' => 'dashicons-arrow-left-alt'];
$visible = $edges ? array_intersect_key($all, array_flip($edges)) : $all;
echo '<div class="jio-widget-spacing jio-widget-spacing--' . count($visible) . '">';
foreach ($visible as $edge => $icon) {
echo '<label><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></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="jio-widget-background">';
self::plain_input($widget, $key . '[color]', (string) ($value['color'] ?? ''), 'text', '背景颜色', 'widefat jio-widget-color');
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) . '__';
$button_title = (string) ($field['button_title'] ?? __('添加项目', 'jio'));
echo '<div class="jio-widget-repeater" data-index-token="' . esc_attr($token) . '" data-item-label="' . esc_attr((string) ($field['item_label'] ?? $field['label'] ?? __('项目', 'jio'))) . '" data-min-items="' . esc_attr((string) ($field['min_items'] ?? 0)) . '" data-max-items="' . esc_attr((string) ($field['max_items'] ?? 50)) . '"><div class="jio-widget-repeater__rows">';
foreach ($rows as $index => $row) {
self::group_row($widget, $field, is_array($row) ? $row : [], $key, (string) $index);
}
echo '</div><template class="jio-widget-repeater__template">';
self::group_row($widget, $field, [], $key, $token);
echo '</template><button type="button" class="button jio-widget-repeater__add">' . esc_html($button_title) . '</button></div>';
}
private static function group_row(SchemaWidget $widget, array $field, array $row, string $key, string $index): void {
$prefix = (string) ($field['item_label'] ?? $field['label'] ?? __('项目', 'jio'));
$number = ctype_digit($index) ? ((int) $index + 1) : 0;
$title = $prefix . $number;
if (!empty($row['title'])) $title .= ':' . strip_tags((string) $row['title']);
echo '<details class="jio-widget-repeater__row" data-row-index="' . esc_attr(ctype_digit($index) ? $index : '0') . '"><summary class="jio-widget-repeater__bar"><span class="dashicons dashicons-move" aria-hidden="true"></span><strong class="jio-widget-repeater__title" data-title-prefix="' . esc_attr($prefix) . '">' . esc_html($title) . '</strong><button type="button" class="button-link-delete jio-widget-repeater__remove">' . esc_html__('删除', 'jio') . '</button></summary><div class="jio-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></details>';
}
private static function nested(SchemaWidget $widget, array $field, array $value, string $key): void {
echo '<details class="jio-widget-nested"><summary>' . esc_html((string) ($field['label'] ?? '更多设置')) . '</summary><div class="jio-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, string $class = 'widefat'): void {
$name = $widget->get_field_name($key);
$id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
echo '<input class="' . esc_attr($class) . '" type="' . esc_attr($type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '">';
}
}