feat(widgets): rebuild admin widgets page to match reference theme
- Replace transfer modal/admin-post flow with reference-style AJAX export/import/template dialogs (admin-transfer.js, TransferController) - Customizer "移出模块" action, reference lazy form loading with t-notice - Common fields use reference labels, dependencies, compact/inline rows, top/bottom-only extra spacing; form header with description/reminder - Remove placement hint, template preview and legacy transfer code - Fix widget audit (current_datetime stub, honor catalog common flags) and regenerate stale golden fixtures for posts_toc, quote, posts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea80cd96cc
commit
a579a5b00a
14 files changed
+698
-446
No files matched your search
@@ -4,156 +4,120 @@ namespace AKSRT\WidgetSystem;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Admin-only import, export, template application and preview. */
|
||||
/** Admin AJAX endpoints for per-sidebar export, JSON import and template import. */
|
||||
final class TransferController {
|
||||
private const NONCE = 'aksrt_widgets_transfer';
|
||||
private static bool $controls_rendered = false;
|
||||
public const NONCE = 'aksrt_widgets_transfer';
|
||||
|
||||
public static function register_hooks(): void {
|
||||
add_action('admin_notices', [self::class, 'render_panel']);
|
||||
add_action('customize_controls_print_footer_scripts', [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']);
|
||||
add_action('wp_ajax_aksrt_widgets_export_sidebar', [self::class, 'ajax_export']);
|
||||
add_action('wp_ajax_aksrt_widgets_import_sidebar', [self::class, 'ajax_import']);
|
||||
add_action('wp_ajax_aksrt_widgets_list_templates', [self::class, 'ajax_list_templates']);
|
||||
add_action('wp_ajax_aksrt_widgets_import_template', [self::class, 'ajax_import_template']);
|
||||
}
|
||||
|
||||
public static function render_panel(): void {
|
||||
if (self::$controls_rendered) {
|
||||
return;
|
||||
}
|
||||
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||
if (!$screen || !in_array($screen->id, ['widgets', 'customize'], true) || !current_user_can('edit_theme_options')) {
|
||||
return;
|
||||
}
|
||||
self::$controls_rendered = true;
|
||||
$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-modal" id="aksrt-widget-transfer-modal" aria-hidden="true" hidden>
|
||||
<button type="button" class="aksrt-widget-transfer-modal__backdrop" data-aksrt-transfer-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"></button>
|
||||
<section class="aksrt-widget-transfer-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-widget-transfer-title">
|
||||
<button type="button" class="aksrt-widget-transfer-modal__close" data-aksrt-transfer-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">×</button>
|
||||
<h2 id="aksrt-widget-transfer-title"><?php esc_html_e('小工具区域管理', 'aksrt'); ?></h2>
|
||||
<p class="description aksrt-widget-transfer-modal__area"></p>
|
||||
<nav class="aksrt-widget-transfer-modal__tabs" aria-label="<?php esc_attr_e('小工具操作', 'aksrt'); ?>">
|
||||
<button type="button" class="button is-active" data-transfer-tab="export"><?php esc_html_e('导出', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" data-transfer-tab="import"><?php esc_html_e('导入', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" data-transfer-tab="template"><?php esc_html_e('内置模板', 'aksrt'); ?></button>
|
||||
</nav>
|
||||
<div class="aksrt-widget-transfer-modal__panel" data-transfer-panel="export">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="aksrt_widgets_export">
|
||||
<?php wp_nonce_field(self::NONCE); ?>
|
||||
<input type="hidden" name="sidebar" value="">
|
||||
<p><?php esc_html_e('导出当前小工具区域的配置文件。', 'aksrt'); ?></p>
|
||||
<?php submit_button(__('下载配置', 'aksrt'), 'primary', 'submit', false); ?>
|
||||
</form>
|
||||
</div>
|
||||
<div class="aksrt-widget-transfer-modal__panel" data-transfer-panel="import" hidden>
|
||||
<form method="post" enctype="multipart/form-data" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<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(); ?>
|
||||
<input type="hidden" name="target_sidebar" value="">
|
||||
<?php submit_button(__('导入配置', 'aksrt'), 'primary', 'submit', false); ?>
|
||||
</form>
|
||||
</div>
|
||||
<div class="aksrt-widget-transfer-modal__panel" data-transfer-panel="template" hidden>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="aksrt_widgets_template">
|
||||
<?php wp_nonce_field(self::NONCE); ?>
|
||||
<select name="template" class="aksrt-widget-template-select" aria-label="<?php esc_attr_e('选择内置模板', 'aksrt'); ?>">
|
||||
<?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(); ?>
|
||||
<input type="hidden" name="target_sidebar" value="">
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function handle_export(): void {
|
||||
public static function ajax_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;
|
||||
$sidebar = self::target_sidebar();
|
||||
$json = wp_json_encode(WidgetTransfer::export($sidebar), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
wp_send_json_success(['json_text' => (string) $json, 'sidebar_id' => $sidebar]);
|
||||
}
|
||||
|
||||
public static function handle_import(): void {
|
||||
public static function ajax_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'));
|
||||
$sidebar = self::target_sidebar();
|
||||
$raw = isset($_POST['json']) ? trim((string) wp_unslash($_POST['json'])) : '';
|
||||
if ($raw === '') {
|
||||
wp_send_json_error(['message' => __('请粘贴 JSON 内容。', 'aksrt')]);
|
||||
}
|
||||
$payload = json_decode((string) file_get_contents((string) $file['tmp_name']), true);
|
||||
if (strlen($raw) > 2 * MB_IN_BYTES) {
|
||||
wp_send_json_error(['message' => __('JSON 内容超过 2MB。', 'aksrt')]);
|
||||
}
|
||||
$payload = json_decode($raw, true);
|
||||
if (!is_array($payload)) {
|
||||
wp_die(esc_html__('导入文件不是有效 JSON。', 'aksrt'));
|
||||
wp_send_json_error(['message' => __('JSON 解析失败,请检查格式。', 'aksrt')]);
|
||||
}
|
||||
self::run_import($payload);
|
||||
$mode = self::import_mode();
|
||||
$result = self::run_import($payload, $sidebar, $mode);
|
||||
$message = $mode === 'append'
|
||||
/* translators: 1: sidebar name, 2: imported widget count */
|
||||
? __('已向侧栏「%1$s」末尾追加 %2$d 个小工具。请刷新页面后查看效果。', 'aksrt')
|
||||
/* translators: 1: sidebar name, 2: imported widget count */
|
||||
: __('已覆盖侧栏「%1$s」并导入 %2$d 个小工具。请刷新页面后查看效果。', 'aksrt');
|
||||
self::send_result($sidebar, $mode, $result, $message);
|
||||
}
|
||||
|
||||
public static function handle_template(): void {
|
||||
public static function ajax_list_templates(): void {
|
||||
self::authorize();
|
||||
$template = isset($_POST['template']) ? sanitize_key(wp_unslash($_POST['template'])) : '';
|
||||
$list = [];
|
||||
foreach (WidgetTransfer::templates() as $id => $template) {
|
||||
$list[] = [
|
||||
'id' => (string) $id,
|
||||
'title' => (string) $template['title'],
|
||||
'desc' => (string) ($template['desc'] ?? ''),
|
||||
'remind' => (string) ($template['remind'] ?? ''),
|
||||
'modules' => WidgetTransfer::template_modules((string) $id),
|
||||
];
|
||||
}
|
||||
wp_send_json_success(['templates' => $list]);
|
||||
}
|
||||
|
||||
public static function ajax_import_template(): void {
|
||||
self::authorize();
|
||||
$sidebar = self::target_sidebar();
|
||||
$template = isset($_POST['template_id']) ? sanitize_key(wp_unslash($_POST['template_id'])) : '';
|
||||
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);
|
||||
wp_send_json_error(['message' => $error->getMessage()]);
|
||||
}
|
||||
$mode = self::import_mode();
|
||||
$result = self::run_import($payload, $sidebar, $mode);
|
||||
$message = $mode === 'append'
|
||||
/* translators: 1: sidebar name, 2: imported widget count */
|
||||
? __('已从模板向侧栏「%1$s」末尾追加 %2$d 个小工具。请刷新页面后查看效果。', 'aksrt')
|
||||
/* translators: 1: sidebar name, 2: imported widget count */
|
||||
: __('已用模板覆盖侧栏「%1$s」并导入 %2$d 个小工具。请刷新页面后查看效果。', 'aksrt');
|
||||
self::send_result($sidebar, $mode, $result, $message, ['template_id' => $template]);
|
||||
}
|
||||
|
||||
private static function authorize(): void {
|
||||
check_ajax_referer(self::NONCE, 'nonce');
|
||||
if (!current_user_can('edit_theme_options')) {
|
||||
wp_die(esc_html__('权限不足。', 'aksrt'), '', ['response' => 403]);
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')], 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'])) : '';
|
||||
private static function target_sidebar(): string {
|
||||
$sidebar = isset($_POST['sidebar_id']) ? sanitize_key(wp_unslash($_POST['sidebar_id'])) : '';
|
||||
if ($sidebar === '' || !isset($GLOBALS['wp_registered_sidebars'][$sidebar])) {
|
||||
wp_send_json_error(['message' => __('无效的侧栏。', 'aksrt')]);
|
||||
}
|
||||
return $sidebar;
|
||||
}
|
||||
|
||||
private static function import_mode(): string {
|
||||
$mode = isset($_POST['import_mode']) ? sanitize_key(wp_unslash($_POST['import_mode'])) : 'append';
|
||||
return $mode === 'replace' ? 'replace' : 'append';
|
||||
}
|
||||
|
||||
/** @return array{sidebars:int,widgets:int} */
|
||||
private static function run_import(array $payload, string $sidebar, string $mode): array {
|
||||
try {
|
||||
WidgetTransfer::import($payload, $mode, $target);
|
||||
return WidgetTransfer::import($payload, $mode, $sidebar);
|
||||
} catch (\InvalidArgumentException $error) {
|
||||
wp_die(esc_html($error->getMessage()), '', ['response' => 400]);
|
||||
wp_send_json_error(['message' => $error->getMessage()]);
|
||||
}
|
||||
wp_safe_redirect(add_query_arg('aksrt_widgets_notice', 'updated', admin_url('widgets.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
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>';
|
||||
/** @param array{sidebars:int,widgets:int} $result */
|
||||
private static function send_result(string $sidebar, string $mode, array $result, string $message, array $extra = []): void {
|
||||
$name = (string) ($GLOBALS['wp_registered_sidebars'][$sidebar]['name'] ?? $sidebar);
|
||||
wp_send_json_success(array_merge([
|
||||
'imported' => (int) $result['widgets'],
|
||||
'sidebar_id' => $sidebar,
|
||||
'import_mode' => $mode,
|
||||
'warnings' => [],
|
||||
'message' => sprintf($message, $name, (int) $result['widgets']),
|
||||
], $extra));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user