feat(widgets): complete phase 3 atomic integration

This commit is contained in:
2026-08-17 18:21:47 +08:00
parent 7c3f4221fe
commit d5dfeb7102
89 changed files with 843 additions and 12886 deletions
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/** Admin-only import, export, template application and preview. */
final class TransferController {
private const NONCE = 'aksrt_widgets_transfer';
public static function register_hooks(): void {
add_action('admin_notices', [self::class, 'render_panel']);
add_action('admin_post_aksrt_widgets_export', [self::class, 'handle_export']);
add_action('admin_post_aksrt_widgets_import', [self::class, 'handle_import']);
add_action('admin_post_aksrt_widgets_template', [self::class, 'handle_template']);
add_action('wp_ajax_aksrt_widgets_template_preview', [self::class, 'ajax_preview']);
}
public static function render_panel(): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->id !== 'widgets' || !current_user_can('edit_theme_options')) {
return;
}
$sidebars = $GLOBALS['wp_registered_sidebars'] ?? [];
$templates = WidgetTransfer::templates();
$notice = isset($_GET['aksrt_widgets_notice']) ? sanitize_key(wp_unslash($_GET['aksrt_widgets_notice'])) : '';
if ($notice !== '') {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('小工具配置已更新。', 'aksrt') . '</p></div>';
}
?>
<div class="aksrt-widget-transfer postbox">
<div class="postbox-header"><h2><?php esc_html_e('AKSRT 小工具模板与导入导出', 'aksrt'); ?></h2></div>
<div class="inside aksrt-widget-transfer__grid">
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<h3><?php esc_html_e('导出', 'aksrt'); ?></h3>
<input type="hidden" name="action" value="aksrt_widgets_export">
<?php wp_nonce_field(self::NONCE); ?>
<?php self::sidebar_select($sidebars, 'sidebar', true); ?>
<?php submit_button(__('下载 aksrt.widgets.v1', 'aksrt'), 'secondary', 'submit', false); ?>
</form>
<form method="post" enctype="multipart/form-data" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<h3><?php esc_html_e('导入', 'aksrt'); ?></h3>
<input type="hidden" name="action" value="aksrt_widgets_import">
<?php wp_nonce_field(self::NONCE); ?>
<input type="file" name="widgets_file" accept="application/json,.json" required>
<?php self::mode_select(); ?>
<?php self::sidebar_select($sidebars, 'target_sidebar', true, __('沿用文件中的区域', 'aksrt')); ?>
<?php submit_button(__('导入配置', 'aksrt'), 'secondary', 'submit', false); ?>
</form>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<h3><?php esc_html_e('内置模板', 'aksrt'); ?></h3>
<input type="hidden" name="action" value="aksrt_widgets_template">
<?php wp_nonce_field(self::NONCE); ?>
<select name="template" class="aksrt-widget-template-select">
<?php foreach ($templates as $key => $template) : ?>
<option value="<?php echo esc_attr($key); ?>"><?php echo esc_html((string) $template['label']); ?></option>
<?php endforeach; ?>
</select>
<?php self::mode_select(); ?>
<?php self::sidebar_select($sidebars, 'target_sidebar', true, __('使用模板默认区域', 'aksrt')); ?>
<div class="aksrt-widget-transfer__actions">
<button type="button" class="button aksrt-widget-template-preview"><?php esc_html_e('预览模块', 'aksrt'); ?></button>
<?php submit_button(__('应用模板', 'aksrt'), 'primary', 'submit', false); ?>
</div>
</form>
<div class="aksrt-widget-template-preview-output" hidden></div>
</div>
</div>
<?php
}
public static function handle_export(): void {
self::authorize();
$sidebar = isset($_POST['sidebar']) ? sanitize_key(wp_unslash($_POST['sidebar'])) : '';
$payload = WidgetTransfer::export($sidebar);
nocache_headers();
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: attachment; filename="aksrt-widgets-' . gmdate('Ymd-His') . '.json"');
echo wp_json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
public static function handle_import(): void {
self::authorize();
$file = $_FILES['widgets_file'] ?? null;
if (!is_array($file) || !empty($file['error']) || empty($file['tmp_name']) || (int) ($file['size'] ?? 0) > 2 * MB_IN_BYTES) {
wp_die(esc_html__('导入文件无效或超过 2MB。', 'aksrt'));
}
$payload = json_decode((string) file_get_contents((string) $file['tmp_name']), true);
if (!is_array($payload)) {
wp_die(esc_html__('导入文件不是有效 JSON。', 'aksrt'));
}
self::run_import($payload);
}
public static function handle_template(): void {
self::authorize();
$template = isset($_POST['template']) ? sanitize_key(wp_unslash($_POST['template'])) : '';
try {
$payload = WidgetTransfer::template_payload($template);
} catch (\InvalidArgumentException $error) {
wp_die(esc_html($error->getMessage()), '', ['response' => 400]);
}
self::run_import($payload);
}
public static function ajax_preview(): void {
check_ajax_referer(self::NONCE, 'nonce');
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => __('权限不足。', 'aksrt')], 403);
}
$template = isset($_POST['template']) ? sanitize_key(wp_unslash($_POST['template'])) : '';
try {
wp_send_json_success(['html' => WidgetTransfer::preview($template)]);
} catch (\InvalidArgumentException $error) {
wp_send_json_error(['message' => $error->getMessage()], 400);
}
}
private static function authorize(): void {
if (!current_user_can('edit_theme_options')) {
wp_die(esc_html__('权限不足。', 'aksrt'), '', ['response' => 403]);
}
check_admin_referer(self::NONCE);
}
private static function run_import(array $payload): void {
$mode = isset($_POST['mode']) ? sanitize_key(wp_unslash($_POST['mode'])) : 'append';
$target = isset($_POST['target_sidebar']) ? sanitize_key(wp_unslash($_POST['target_sidebar'])) : '';
try {
WidgetTransfer::import($payload, $mode, $target);
} catch (\InvalidArgumentException $error) {
wp_die(esc_html($error->getMessage()), '', ['response' => 400]);
}
wp_safe_redirect(add_query_arg('aksrt_widgets_notice', 'updated', admin_url('widgets.php')));
exit;
}
private static function sidebar_select(array $sidebars, string $name, bool $allow_empty, string $empty_label = ''): void {
echo '<label><span class="screen-reader-text">' . esc_html__('小工具区域', 'aksrt') . '</span><select name="' . esc_attr($name) . '">';
if ($allow_empty) {
echo '<option value="">' . esc_html($empty_label !== '' ? $empty_label : __('全部区域', 'aksrt')) . '</option>';
}
foreach ($sidebars as $id => $sidebar) {
echo '<option value="' . esc_attr((string) $id) . '">' . esc_html((string) ($sidebar['name'] ?? $id)) . '</option>';
}
echo '</select></label>';
}
private static function mode_select(): void {
echo '<select name="mode"><option value="append">' . esc_html__('追加', 'aksrt') . '</option><option value="replace">' . esc_html__('替换区域', 'aksrt') . '</option></select>';
}
}