feat(widgets): complete phase 3 atomic integration
This commit is contained in:
@@ -19,18 +19,21 @@ final class AdminFormController {
|
||||
|
||||
public static function enqueue(): void {
|
||||
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||
if (!$screen || !in_array($screen->id, ['widgets', 'customize'], true)) {
|
||||
$is_customizer = current_filter() === 'customize_controls_enqueue_scripts';
|
||||
if (!$is_customizer && (!$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_style('aksrt-widget-preview', AKSRT_URI . '/assets/widgets/frontend.css', [], 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),
|
||||
'transferNonce' => wp_create_nonce('aksrt_widgets_transfer'),
|
||||
'i18n' => [
|
||||
'loading' => __('正在加载模块配置…', 'aksrt'),
|
||||
'error' => __('加载失败,请重试。', 'aksrt'),
|
||||
|
||||
@@ -9,6 +9,15 @@ final class AssetManager {
|
||||
private static bool $registered = false;
|
||||
private static bool $configured = false;
|
||||
|
||||
/** Enqueue before wp_head whenever at least one new widget is assigned. */
|
||||
public static function enqueue_active(): void {
|
||||
foreach (WidgetRegistry::all() as $definition) {
|
||||
if (is_active_widget(false, false, $definition->id_base(), true)) {
|
||||
self::enqueue_for($definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function enqueue_for(WidgetDefinition $definition): void {
|
||||
self::register();
|
||||
foreach ($definition->assets() as $asset) {
|
||||
@@ -42,17 +51,17 @@ final class AssetManager {
|
||||
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');
|
||||
wp_register_style('aksrt-swiper', $uri . '/assets/css/frontend/swiper-bundle.min.css', [], '11.2.10');
|
||||
}
|
||||
if (!wp_script_is('aksrt-swiper', 'registered')) {
|
||||
wp_register_script('aksrt-swiper', $uri . '/assets/js/swiper-bundle.min.js', [], '11.0', true);
|
||||
wp_register_script('aksrt-swiper', $uri . '/assets/js/swiper-bundle.min.js', [], '11.2.10', 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);
|
||||
$third_party = $uri . '/assets/widgets/third-party/';
|
||||
wp_register_script('aksrt-widget-hls', $third_party . 'hls.min.js', [], 'reference-pinned', true);
|
||||
wp_register_script('aksrt-widget-flv', $third_party . 'flv.min.js', [], 'reference-pinned', true);
|
||||
wp_register_script('aksrt-widget-dash', $third_party . 'dash.all.min.js', [], 'reference-pinned', true);
|
||||
wp_register_script('aksrt-widget-dplayer', $third_party . 'DPlayer.min.js', ['aksrt-widget-hls', 'aksrt-widget-flv', 'aksrt-widget-dash'], 'reference-pinned', true);
|
||||
wp_register_script('aksrt-widget-tilt', $third_party . 'vanilla-tilt.min.js', [], 'reference-pinned', true);
|
||||
self::$registered = true;
|
||||
}
|
||||
|
||||
@@ -61,7 +70,6 @@ final class AssetManager {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace AKSRT\WidgetSystem;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Single bootstrap for the schema-driven widget runtime.
|
||||
*/
|
||||
final class Runtime {
|
||||
private static bool $registered = false;
|
||||
|
||||
public static function init(): void {
|
||||
if (self::$registered) {
|
||||
return;
|
||||
}
|
||||
self::$registered = true;
|
||||
|
||||
add_action('widgets_init', [self::class, 'unregister_core_widgets'], 1);
|
||||
add_action('widgets_init', [WidgetAreas::class, 'register'], 5);
|
||||
add_action('widgets_init', [self::class, 'register_widgets'], 10);
|
||||
add_action('save_post_page', [WidgetAreas::class, 'flush_page_cache']);
|
||||
add_action('rest_api_init', static function (): void {
|
||||
(new RestController())->register_routes();
|
||||
});
|
||||
add_action('wp_enqueue_scripts', [AssetManager::class, 'enqueue_active'], 20);
|
||||
add_action('in_widget_form', [self::class, 'render_placement_hint'], 20, 1);
|
||||
|
||||
AdminFormController::register_hooks();
|
||||
TransferController::register_hooks();
|
||||
}
|
||||
|
||||
public static function unregister_core_widgets(): void {
|
||||
foreach ([
|
||||
'WP_Widget_RSS',
|
||||
'WP_Widget_Recent_Posts',
|
||||
'WP_Widget_Recent_Comments',
|
||||
'WP_Widget_Pages',
|
||||
'WP_Widget_Search',
|
||||
'WP_Widget_Calendar',
|
||||
] as $class) {
|
||||
unregister_widget($class);
|
||||
}
|
||||
}
|
||||
|
||||
public static function register_widgets(): void {
|
||||
foreach (WidgetRegistry::all() as $definition) {
|
||||
register_widget(new SchemaWidget($definition));
|
||||
}
|
||||
}
|
||||
|
||||
public static function render_placement_hint(\WP_Widget $widget): void {
|
||||
if (!$widget instanceof SchemaWidget) {
|
||||
return;
|
||||
}
|
||||
$definition = $widget->definition();
|
||||
$messages = [
|
||||
'wide' => __('宽版模块:建议放入全宽或主内容区域;放入侧栏时会显示尺寸警告。', 'aksrt'),
|
||||
'sidebar' => __('侧栏模块:建议放入侧栏区域;放入全宽区域时会显示尺寸警告。', 'aksrt'),
|
||||
'any' => __('自适应模块:可放入主内容、全宽或侧栏区域。', 'aksrt'),
|
||||
];
|
||||
printf(
|
||||
'<p class="aksrt-widget-placement-hint" data-widget-size="%1$s"><span class="dashicons dashicons-info-outline" aria-hidden="true"></span>%2$s</p>',
|
||||
esc_attr($definition->size()),
|
||||
esc_html($messages[$definition->size()] ?? $messages['any'])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ final class SchemaWidget extends \WP_Widget {
|
||||
if (wp_doing_ajax() && isset($_REQUEST['action']) && $_REQUEST['action'] === AdminFormController::AJAX_ACTION) {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('is_customize_preview') && is_customize_preview()) {
|
||||
return true;
|
||||
}
|
||||
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||
return $screen && in_array($screen->id, ['widgets', 'customize'], true);
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace AKSRT\WidgetSystem;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Canonical 40 static areas plus five opt-in areas for each page. */
|
||||
final class WidgetAreas {
|
||||
private const PAGE_CACHE_KEY = 'aksrt_widget_pages_v1';
|
||||
|
||||
/** @return array<string, array{0:string,1:string,2:string}> */
|
||||
public static function definitions(): array {
|
||||
$areas = [
|
||||
'all_top_fluid' => ['所有页面 - 顶部全宽', '显示在所有知识内容页面顶部。', 'fluid'],
|
||||
'all_bottom_fluid' => ['所有页面 - 底部全宽', '显示在所有知识内容页面底部。', 'fluid'],
|
||||
'all_sidebar_top' => ['所有页面 - 侧栏顶部', '显示在页面侧栏最上方。', 'sidebar'],
|
||||
'all_sidebar_bottom' => ['所有页面 - 侧栏底部', '显示在页面侧栏最下方。', 'sidebar'],
|
||||
];
|
||||
$page_names = [
|
||||
'home' => '首页',
|
||||
'single' => '文章页',
|
||||
'cat' => '分类页',
|
||||
'tag' => '标签页',
|
||||
'search' => '搜索页',
|
||||
];
|
||||
$positions = [
|
||||
'top_fluid' => ['顶部全宽', 'fluid'],
|
||||
'top_content' => ['主内容上面', 'content'],
|
||||
'bottom_content' => ['主内容下面', 'content'],
|
||||
'bottom_fluid' => ['底部全宽', 'fluid'],
|
||||
'sidebar' => ['侧边栏', 'sidebar'],
|
||||
];
|
||||
foreach ($page_names as $prefix => $page_name) {
|
||||
foreach ($positions as $suffix => [$position, $kind]) {
|
||||
$areas[$prefix . '_' . $suffix] = [
|
||||
$page_name . ' - ' . $position,
|
||||
sprintf('显示在%s的%s位置。', $page_name, $position),
|
||||
$kind,
|
||||
];
|
||||
}
|
||||
}
|
||||
foreach (['author' => '作者页', 'msg' => '消息中心'] as $prefix => $page_name) {
|
||||
$areas[$prefix . '_top_fluid'] = [$page_name . ' - 顶部全宽', '显示在' . $page_name . '内容顶部。', 'fluid'];
|
||||
$areas[$prefix . '_bottom_fluid'] = [$page_name . ' - 底部全宽', '显示在' . $page_name . '内容底部。', 'fluid'];
|
||||
}
|
||||
$areas['user_top_fluid'] = ['用户中心 - 顶部全宽', '显示在用户中心内容顶部。', 'fluid'];
|
||||
$areas['user_top_content'] = ['用户中心 - 内容区上面', '显示在用户中心主内容上面。', 'content'];
|
||||
$areas['user_bottom_fluid'] = ['用户中心 - 底部全宽', '显示在用户中心内容底部。', 'fluid'];
|
||||
$areas['newposts_sidebar_top'] = ['前台投稿 - 侧栏顶部', '显示在前台投稿侧栏顶部,并在移动端保留。', 'sidebar'];
|
||||
$areas['newposts_sidebar_bottom'] = ['前台投稿 - 侧栏底部', '显示在前台投稿侧栏底部,并在移动端保留。', 'sidebar'];
|
||||
$areas['all_footer'] = ['页脚内部', '显示在站点页脚内部。', 'content'];
|
||||
$areas['mobile_nav_fluid'] = ['移动菜单 - 底部', '显示在移动端抽屉菜单底部。', 'sidebar'];
|
||||
return $areas;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public static function static_ids(): array {
|
||||
return array_keys(self::definitions());
|
||||
}
|
||||
|
||||
public static function register(): void {
|
||||
foreach (self::definitions() as $id => [$name, $description, $kind]) {
|
||||
self::register_area($id, $name, $description, $kind);
|
||||
}
|
||||
self::register_page_areas();
|
||||
}
|
||||
|
||||
public static function render(string $id, string $class = ''): bool {
|
||||
if (!is_active_sidebar($id)) {
|
||||
return false;
|
||||
}
|
||||
$class = trim('aksrt-widget-area aksrt-widget-area--' . sanitize_html_class($id) . ' ' . $class);
|
||||
echo '<div class="' . esc_attr($class) . '" data-widget-area="' . esc_attr($id) . '">';
|
||||
dynamic_sidebar($id);
|
||||
echo '</div>';
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function flush_page_cache(): void {
|
||||
wp_cache_delete(self::PAGE_CACHE_KEY, 'aksrt');
|
||||
}
|
||||
|
||||
private static function register_page_areas(): void {
|
||||
$page_ids = wp_cache_get(self::PAGE_CACHE_KEY, 'aksrt');
|
||||
if (!is_array($page_ids)) {
|
||||
$page_ids = get_posts([
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'no_found_rows' => true,
|
||||
'update_post_meta_cache' => false,
|
||||
'update_post_term_cache' => false,
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => 'widgets_register',
|
||||
'value' => '1',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$page_ids = array_map('absint', $page_ids);
|
||||
wp_cache_set(self::PAGE_CACHE_KEY, $page_ids, 'aksrt');
|
||||
}
|
||||
|
||||
$positions = [
|
||||
'sidebar' => ['侧边栏', 'sidebar'],
|
||||
'top_fluid' => ['顶部全宽', 'fluid'],
|
||||
'top_content' => ['主内容上面', 'content'],
|
||||
'bottom_content' => ['主内容下面', 'content'],
|
||||
'bottom_fluid' => ['底部全宽', 'fluid'],
|
||||
];
|
||||
foreach ($page_ids as $page_id) {
|
||||
$enabled = get_post_meta($page_id, 'widgets_register_container', true);
|
||||
if (!is_array($enabled)) {
|
||||
continue;
|
||||
}
|
||||
$title = wp_html_excerpt(get_the_title($page_id), 12, '…');
|
||||
foreach (array_values(array_unique(array_map('sanitize_key', $enabled))) as $position) {
|
||||
if (!isset($positions[$position])) {
|
||||
continue;
|
||||
}
|
||||
[$label, $kind] = $positions[$position];
|
||||
self::register_area(
|
||||
'page_' . $position . '_' . $page_id,
|
||||
sprintf('[页面:%1$s] - %2$s', $title, $label),
|
||||
sprintf('显示在页面“%1$s”的%2$s位置。', $title, $label),
|
||||
$kind
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function register_area(string $id, string $name, string $description, string $kind): void {
|
||||
register_sidebar([
|
||||
'id' => $id,
|
||||
'name' => __($name, 'aksrt'),
|
||||
'description' => __($description, 'aksrt'),
|
||||
'before_widget' => '<section id="%1$s" class="aksrt-widget-shell aksrt-widget-shell--' . esc_attr($kind) . ' %2$s">',
|
||||
'after_widget' => '</section>',
|
||||
'before_title' => '<h3 class="aksrt-widget-shell__title">',
|
||||
'after_title' => '</h3>',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace AKSRT\WidgetSystem;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Import/export contract for schema widgets. */
|
||||
final class WidgetTransfer {
|
||||
public const FORMAT = 'aksrt.widgets.v1';
|
||||
|
||||
/** @return array<string, array<string,mixed>> */
|
||||
public static function templates(): array {
|
||||
return [
|
||||
'article_home' => [
|
||||
'label' => __('文章首页', 'aksrt'),
|
||||
'description' => __('公告、幻灯片、多栏目文章与热榜侧栏。', 'aksrt'),
|
||||
'sidebars' => [
|
||||
'home_top_content' => [
|
||||
['widget' => 'notice', 'settings' => ['title' => '站点公告']],
|
||||
['widget' => 'slider', 'settings' => ['title' => '精选内容']],
|
||||
['widget' => 'tab_posts', 'settings' => ['title' => '热门栏目']],
|
||||
],
|
||||
'home_sidebar' => [['widget' => 'hot_posts', 'settings' => ['title' => '热门文章']]],
|
||||
],
|
||||
],
|
||||
'article_list' => [
|
||||
'label' => __('文章列表', 'aksrt'),
|
||||
'description' => __('标题、分类卡片与可分页文章列表。', 'aksrt'),
|
||||
'sidebars' => [
|
||||
'home_top_content' => [
|
||||
['widget' => 'text_title', 'settings' => ['heading' => '最新知识内容', 'description' => '持续更新的课程、文章与资料']],
|
||||
['widget' => 'term_cards', 'settings' => ['title' => '内容分类']],
|
||||
['widget' => 'posts', 'settings' => ['title' => '最新发布', 'paginate' => 'number']],
|
||||
],
|
||||
],
|
||||
],
|
||||
'links_navigation' => [
|
||||
'label' => __('链接导航', 'aksrt'),
|
||||
'description' => __('搜索、分类入口与链接列表。', 'aksrt'),
|
||||
'sidebars' => [
|
||||
'home_top_content' => [
|
||||
['widget' => 'icon_cover_cards', 'settings' => ['title' => '常用入口']],
|
||||
['widget' => 'links', 'settings' => ['title' => '链接导航']],
|
||||
],
|
||||
'home_sidebar' => [['widget' => 'search', 'settings' => ['title' => '快速查找']]],
|
||||
],
|
||||
],
|
||||
'article_sidebar' => [
|
||||
'label' => __('文章侧栏', 'aksrt'),
|
||||
'description' => __('目录、作者、付费授权与热榜文章。', 'aksrt'),
|
||||
'sidebars' => [
|
||||
'single_sidebar' => [
|
||||
['widget' => 'posts_toc', 'settings' => ['title' => '文章目录']],
|
||||
['widget' => 'author_card', 'settings' => ['title' => '作者']],
|
||||
['widget' => 'post_pay', 'settings' => []],
|
||||
['widget' => 'hot_posts', 'settings' => ['title' => '热门文章']],
|
||||
],
|
||||
],
|
||||
],
|
||||
'knowledge_landing' => [
|
||||
'label' => __('知识付费落地页', 'aksrt'),
|
||||
'description' => __('亮点、方案、评价与常见问题。', 'aksrt'),
|
||||
'sidebars' => [
|
||||
'home_top_content' => [
|
||||
['widget' => 'feature_intro', 'settings' => ['title' => '系统化学习路径']],
|
||||
['widget' => 'highlights', 'settings' => ['title' => '内容亮点']],
|
||||
['widget' => 'pricing_plans', 'settings' => ['title' => '选择学习方案']],
|
||||
['widget' => 'reviews', 'settings' => ['title' => '学员评价']],
|
||||
['widget' => 'faq', 'settings' => ['title' => '常见问题']],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function template_payload(string $template): array {
|
||||
$templates = self::templates();
|
||||
if (!isset($templates[$template])) {
|
||||
throw new \InvalidArgumentException(__('未知的小工具模板。', 'aksrt'));
|
||||
}
|
||||
return [
|
||||
'format' => self::FORMAT,
|
||||
'template' => $template,
|
||||
'sidebars' => $templates[$template]['sidebars'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function export(string $sidebar = ''): array {
|
||||
$assignments = wp_get_sidebars_widgets();
|
||||
$sidebars = [];
|
||||
foreach ($assignments as $sidebar_id => $widget_ids) {
|
||||
if ($sidebar !== '' && $sidebar_id !== $sidebar) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($widget_ids)) {
|
||||
continue;
|
||||
}
|
||||
$items = [];
|
||||
foreach ($widget_ids as $widget_id) {
|
||||
$item = self::export_widget((string) $widget_id);
|
||||
if ($item !== null) {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
if ($items || $sidebar !== '') {
|
||||
$sidebars[$sidebar_id] = $items;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'format' => self::FORMAT,
|
||||
'exported_at' => gmdate('c'),
|
||||
'sidebars' => $sidebars,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{sidebars:int,widgets:int} */
|
||||
public static function import(array $payload, string $mode = 'append', string $target_sidebar = ''): array {
|
||||
if (($payload['format'] ?? '') !== self::FORMAT || !isset($payload['sidebars']) || !is_array($payload['sidebars'])) {
|
||||
throw new \InvalidArgumentException(__('仅接受 aksrt.widgets.v1 格式。', 'aksrt'));
|
||||
}
|
||||
$mode = $mode === 'replace' ? 'replace' : 'append';
|
||||
$registered = array_keys($GLOBALS['wp_registered_sidebars'] ?? []);
|
||||
$assignments = wp_get_sidebars_widgets();
|
||||
$sources = array_slice($payload['sidebars'], 0, 100, true);
|
||||
if ($target_sidebar !== '') {
|
||||
$combined = [];
|
||||
foreach ($sources as $items) {
|
||||
if (is_array($items)) {
|
||||
$combined = array_merge($combined, $items);
|
||||
}
|
||||
}
|
||||
$sources = [$target_sidebar => $combined];
|
||||
}
|
||||
$changed = 0;
|
||||
$created = 0;
|
||||
|
||||
foreach ($sources as $source_sidebar => $items) {
|
||||
$sidebar_id = $target_sidebar !== '' ? sanitize_key($target_sidebar) : sanitize_key((string) $source_sidebar);
|
||||
if (!in_array($sidebar_id, $registered, true) || !is_array($items)) {
|
||||
continue;
|
||||
}
|
||||
$current = isset($assignments[$sidebar_id]) && is_array($assignments[$sidebar_id]) ? $assignments[$sidebar_id] : [];
|
||||
if ($mode === 'replace') {
|
||||
$current = [];
|
||||
}
|
||||
foreach (array_slice($items, 0, 100) as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$widget_id = self::create_widget($item);
|
||||
if ($widget_id !== '') {
|
||||
$current[] = $widget_id;
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
$assignments[$sidebar_id] = array_values(array_unique($current));
|
||||
$changed++;
|
||||
}
|
||||
wp_set_sidebars_widgets($assignments);
|
||||
return ['sidebars' => $changed, 'widgets' => $created];
|
||||
}
|
||||
|
||||
public static function preview(string $template): string {
|
||||
$payload = self::template_payload($template);
|
||||
$html = '';
|
||||
foreach ($payload['sidebars'] as $sidebar_id => $items) {
|
||||
$html .= '<section class="aksrt-widget-template-preview__area"><h4>' . esc_html((string) $sidebar_id) . '</h4>';
|
||||
foreach ($items as $item) {
|
||||
$definition = WidgetRegistry::get((string) ($item['widget'] ?? ''));
|
||||
if ($definition === null) {
|
||||
continue;
|
||||
}
|
||||
$settings = $definition->normalize((array) ($item['settings'] ?? []));
|
||||
$html .= '<article class="aksrt-widget-template-preview__module aksrt-widget aksrt-widget--' . esc_attr($definition->slug()) . '"><strong>' . esc_html($definition->name()) . '</strong><div class="aksrt-widget__content">';
|
||||
$html .= RendererRegistry::render($definition->renderer(), $settings, ['widget_id' => 'preview-0']);
|
||||
$html .= '</div></article>';
|
||||
}
|
||||
$html .= '</section>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function export_widget(string $widget_id): ?array {
|
||||
if (!preg_match('/^(aksrt_widget_[a-z0-9_]+)-([1-9][0-9]*)$/', $widget_id, $matches)) {
|
||||
return null;
|
||||
}
|
||||
$definition = WidgetRegistry::get_by_id_base($matches[1]);
|
||||
if ($definition === null) {
|
||||
return null;
|
||||
}
|
||||
$settings = get_option('widget_' . $definition->id_base(), []);
|
||||
$number = (int) $matches[2];
|
||||
if (!is_array($settings) || !isset($settings[$number]) || !is_array($settings[$number])) {
|
||||
return null;
|
||||
}
|
||||
return ['widget' => $definition->slug(), 'settings' => $definition->normalize($settings[$number])];
|
||||
}
|
||||
|
||||
private static function create_widget(array $item): string {
|
||||
$definition = WidgetRegistry::get(sanitize_key((string) ($item['widget'] ?? '')));
|
||||
if ($definition === null) {
|
||||
return '';
|
||||
}
|
||||
$option_name = 'widget_' . $definition->id_base();
|
||||
$settings = get_option($option_name, []);
|
||||
$settings = is_array($settings) ? $settings : [];
|
||||
$numbers = array_filter(array_map('intval', array_keys($settings)), static fn(int $number): bool => $number > 0);
|
||||
$number = $numbers ? max($numbers) + 1 : 1;
|
||||
$settings[$number] = $definition->normalize((array) ($item['settings'] ?? []));
|
||||
$settings['_multiwidget'] = 1;
|
||||
update_option($option_name, $settings);
|
||||
return $definition->id_base() . '-' . $number;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user