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
+214
View File
@@ -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;
}
}