feat: rewrite schema-driven widget system

This commit is contained in:
2026-08-17 17:29:54 +08:00
parent 6c6c462df8
commit ad99ab53ce
28 changed files with 3489 additions and 0 deletions
@@ -0,0 +1,136 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\PostRenderer;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render article discovery and paid-content widgets. */
final class ContentRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
switch ($slug) {
case 'term_cards': return self::term_cards($instance);
case 'term_aggregation': return self::term_aggregation($instance);
case 'post_pay': return self::post_pay($instance);
case 'tab_posts': return self::tab_posts($instance, $context);
case 'posts':
case 'hot_posts':
case 'oneline_posts': return self::posts($slug, $instance, $context);
}
return '';
}
private static function terms(array $instance): array {
$args = [
'taxonomy' => (string) $instance['taxonomy'],
'number' => (int) $instance['count'],
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
];
$ids = RendererHelpers::ids((string) $instance['term_ids']);
if ($ids) {
$args['include'] = $ids;
$args['orderby'] = 'include';
}
$terms = get_terms($args);
return is_wp_error($terms) ? [] : (array) $terms;
}
private static function term_cards(array $instance): string {
$terms = self::terms($instance);
if (!$terms) {
return RendererHelpers::empty_state(__('暂无分类', 'aksrt'));
}
$html = '<div class="aksrt-widget-grid aksrt-term-cards" style="--aksrt-cols:' . (int) $instance['pc_row'] . ';--aksrt-cols-m:' . (int) $instance['m_row'] . '">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
continue;
}
$image = (string) get_term_meta((int) $term->term_id, 'aksrt_cover_url', true);
if ($image === '') {
$image = (string) get_term_meta((int) $term->term_id, 'cover_url', true);
}
$html .= '<a class="aksrt-term-card" href="' . esc_url($link) . '"><span class="aksrt-term-card__media">' . RendererHelpers::image($image, $term->name) . '</span><span class="aksrt-term-card__body"><strong>' . esc_html($term->name) . '</strong><small>' . esc_html(wp_trim_words((string) $term->description, 18)) . '</small><em>' . sprintf(esc_html__('%s 篇内容', 'aksrt'), number_format_i18n((int) $term->count)) . '</em></span></a>';
}
return $html . '</div>';
}
private static function term_aggregation(array $instance): string {
$terms = self::terms($instance);
if (!$terms) {
return RendererHelpers::empty_state(__('暂无聚合内容', 'aksrt'));
}
$taxonomy = (string) $instance['taxonomy'];
$term_ids = array_map(static fn($term): int => (int) $term->term_id, $terms);
$query = new \WP_Query([
'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => min(100, count($terms) * 6),
'ignore_sticky_posts' => true, 'no_found_rows' => true,
'tax_query' => [['taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids]],
]);
$by_term = [];
foreach ($query->posts as $post) {
foreach (wp_get_post_terms((int) $post->ID, $taxonomy, ['fields' => 'ids']) as $term_id) {
if (in_array((int) $term_id, $term_ids, true) && count($by_term[$term_id] ?? []) < 4) {
$by_term[$term_id][] = $post;
}
}
}
$html = '<div class="aksrt-term-aggregation">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
continue;
}
$html .= '<section class="aksrt-term-aggregation__card"><header><a href="' . esc_url($link) . '"><strong>' . esc_html($term->name) . '</strong><span>' . esc_html__('查看全部', 'aksrt') . '</span></a></header><ul>';
foreach ($by_term[$term->term_id] ?? [] as $post) {
$html .= '<li><a href="' . esc_url(get_permalink($post)) . '">' . get_the_post_thumbnail($post, 'thumbnail') . '<span>' . esc_html(get_the_title($post)) . '</span></a></li>';
}
$html .= '</ul></section>';
}
return $html . '</div>';
}
private static function posts(string $slug, array $instance, array $context): string {
$widget_id = (string) ($context['widget_id'] ?? '');
if (($instance['load_mode'] ?? 'sync') === 'ajax') {
return '<div class="aksrt-widget-items is-loading" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1"><div class="aksrt-widget-skeleton" aria-label="' . esc_attr__('正在加载', 'aksrt') . '"></div></div>';
}
$payload = PostRenderer::payload($instance, $slug, 1);
return '<div class="aksrt-widget-items" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1">' . $payload['html'] . $payload['paginate_html'] . '</div>';
}
private static function tab_posts(array $instance, array $context): string {
$tabs = array_values((array) $instance['tabs']);
if (!$tabs) {
return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt'));
}
$buttons = '';
foreach ($tabs as $index => $tab) {
$buttons .= '<button type="button" role="tab" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" data-segment="' . $index . '">' . esc_html((string) ($tab['title'] ?? sprintf(__('栏目 %d', 'aksrt'), $index + 1))) . '</button>';
}
$payload = PostRenderer::payload($instance, 'tab_posts', 1, '0');
return '<div class="aksrt-widget-tabs" data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '"><div class="aksrt-widget-tabs__nav" role="tablist">' . $buttons . '</div><div class="aksrt-widget-tabs__panel" role="tabpanel" data-widget-items data-segment="0">' . $payload['html'] . $payload['paginate_html'] . '</div></div>';
}
private static function post_pay(array $instance): string {
$post = get_post();
if (!$post instanceof \WP_Post || !function_exists('aksrt_get_pay_meta')) {
return '';
}
$meta = aksrt_get_pay_meta($post);
if (($meta['pay_type'] ?? 'no') === 'no') {
return RendererHelpers::empty_state(__('当前文章未配置付费内容', 'aksrt'));
}
if (function_exists('aksrt_has_paid_access') && aksrt_has_paid_access($post)) {
$html = '<div class="aksrt-post-pay is-authorized ' . esc_attr((string) $instance['theme']) . '"><i class="fas fa-check-circle" aria-hidden="true"></i><strong>' . esc_html__('内容已解锁', 'aksrt') . '</strong><p>' . esc_html__('你已拥有当前知识内容的访问权限。', 'aksrt') . '</p>';
$html .= function_exists('aksrt_get_pay_attributes_html') ? aksrt_get_pay_attributes_html($post, 'aksrt-post-pay__attrs') : '';
$html .= function_exists('aksrt_get_demo_link_html') ? aksrt_get_demo_link_html($post, 'aksrt-widget-button') : '';
return $html . '</div>';
}
return function_exists('aksrt_get_paywall_html') ? aksrt_get_paywall_html($post, true) : RendererHelpers::empty_state(__('请登录或购买后查看', 'aksrt'));
}
}
@@ -0,0 +1,196 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render the 17 general and decorative widgets. */
final class GeneralRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
$method = 'render_' . $slug;
return is_callable([self::class, $method]) ? (string) self::$method($instance, $context) : '';
}
private static function render_search(array $i): string {
$action = esc_url(home_url('/'));
$placeholder = esc_attr((string) $i['placeholder']);
$style = sanitize_html_class((string) $i['search_style']);
return '<form class="aksrt-widget-search aksrt-widget-search--' . $style . '" role="search" method="get" action="' . $action . '"><label><span class="screen-reader-text">' . esc_html__('搜索:', 'aksrt') . '</span><input type="search" name="s" placeholder="' . $placeholder . '" autocomplete="off"></label><button type="submit" aria-label="' . esc_attr__('搜索', 'aksrt') . '"><i class="fas fa-search" aria-hidden="true"></i><span>' . esc_html__('搜索', 'aksrt') . '</span></button></form>';
}
private static function render_posts_toc(array $i): string {
if (!is_singular('post')) {
return '';
}
return '<nav class="aksrt-widget-toc' . (!empty($i['collapsed']) ? ' is-collapsed' : '') . '" data-min-headings="' . (int) $i['min_headings'] . '" data-scroll-offset="' . (int) $i['offset'] . '" aria-label="' . esc_attr__('文章目录', 'aksrt') . '"><button class="aksrt-widget-toc__toggle" type="button" aria-expanded="' . (empty($i['collapsed']) ? 'true' : 'false') . '"><span>' . esc_html__('文章目录', 'aksrt') . '</span><i class="fas fa-chevron-down" aria-hidden="true"></i></button><ol class="aksrt-widget-toc__list"></ol><p class="aksrt-widget-toc__empty">' . esc_html__('正在生成目录…', 'aksrt') . '</p></nav>';
}
private static function render_quote(array $i): string {
$quotes = RendererHelpers::lines((string) $i['quotes']);
$text = $quotes ? $quotes[array_rand($quotes)] : __('保持好奇,持续创造。', 'aksrt');
return '<figure class="aksrt-widget-quote" data-source="' . esc_attr((string) $i['source']) . '" data-interval="' . (int) $i['interval'] . '"><i class="fas fa-quote-left" aria-hidden="true"></i><blockquote>' . esc_html($text) . '</blockquote><button type="button" aria-label="' . esc_attr__('换一句', 'aksrt') . '"><i class="fas fa-sync-alt" aria-hidden="true"></i></button></figure>';
}
private static function render_icon_cards(array $i): string {
return self::icon_cards((array) $i['cards'], false, (int) $i['pc_row'], (int) $i['m_row']);
}
private static function render_icon_cover_cards(array $i): string {
return self::icon_cards((array) $i['cards'], true, (int) $i['pc_row'], (int) $i['m_row']);
}
private static function icon_cards(array $cards, bool $cover, int $pc, int $mobile): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-icon-cards' . ($cover ? ' aksrt-widget-icon-cards--cover' : '') . '" style="--aksrt-cols:' . $pc . ';--aksrt-cols-m:' . $mobile . '">';
foreach ($cards as $card) {
if (!is_array($card)) {
continue;
}
$args = ['icon' => $card['icon'] ?? '', 'customize_icon' => $card['custom_icon'] ?? '', 'icon_image' => $card['custom_icon'] ?? '', 'icon_color' => $card['color'] ?? '', 'icon_class' => $card['color'] ?? '', 'title' => $card['title'] ?? '', 'desc' => $card['description'] ?? '', 'link' => $card['link'] ?? []];
if ($cover && function_exists('aksrt_icon_cover_card')) {
$html .= aksrt_icon_cover_card($args);
} elseif (!$cover && function_exists('aksrt_icon_card')) {
$html .= aksrt_icon_card($args);
} else {
$link = RendererHelpers::link((array) ($card['link'] ?? []));
$tag = $link['url'] ? 'a' : 'div';
$html .= '<' . $tag . ' class="aksrt-widget-icon-card ' . esc_attr((string) ($card['color'] ?? '')) . '"' . ($link['url'] ? ' href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) : '') . '><i class="' . esc_attr((string) ($card['icon'] ?? 'fa fa-star')) . '" aria-hidden="true"></i><span><strong>' . esc_html((string) ($card['title'] ?? '')) . '</strong><small>' . esc_html((string) ($card['description'] ?? '')) . '</small></span></' . $tag . '>';
}
}
return $html . '</div>';
}
private static function render_video(array $i): string {
$url = esc_url((string) $i['url']);
if ($url === '') {
return RendererHelpers::empty_state(__('请配置视频地址', 'aksrt'));
}
$ratio = (string) $i['ratio'];
return '<div class="aksrt-widget-video aksrt-widget-video--' . esc_attr(str_replace(':', '-', $ratio)) . '" data-video-url="' . $url . '" data-video-poster="' . esc_url((string) $i['poster']) . '" data-autoplay="' . (!empty($i['autoplay']) ? '1' : '0') . '" data-muted="' . (!empty($i['muted']) ? '1' : '0') . '"><video controls playsinline preload="metadata"' . (!empty($i['autoplay']) ? ' autoplay' : '') . (!empty($i['muted']) ? ' muted' : '') . (!empty($i['poster']) ? ' poster="' . esc_url((string) $i['poster']) . '"' : '') . '><source src="' . $url . '"></video><p class="aksrt-widget-media-error" hidden>' . esc_html__('视频加载失败,请稍后重试。', 'aksrt') . '</p></div>';
}
private static function render_embed(array $i): string {
if (empty($i['url'])) {
return RendererHelpers::empty_state(__('请配置嵌入地址', 'aksrt'));
}
return '<div class="aksrt-widget-embed" style="--aksrt-embed-ratio:' . esc_attr((string) (float) $i['ratio']) . '%"><iframe src="' . esc_url((string) $i['url']) . '" loading="lazy" referrerpolicy="strict-origin-when-cross-origin"' . (!empty($i['allow_fullscreen']) ? ' allowfullscreen' : '') . ' title="' . esc_attr__('嵌入内容', 'aksrt') . '"></iframe></div>';
}
private static function render_media_cards(array $i): string {
return self::media_grid((array) $i['cards']);
}
private static function media_grid(array $items, string $class = ''): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-media-cards ' . esc_attr($class) . '">';
foreach ($items as $item) {
if (is_array($item)) {
$html .= RendererHelpers::card_media($item);
}
}
return $html . '</div>';
}
private static function render_partners(array $i): string {
$items = '';
foreach ((array) $i['items'] as $item) {
if (is_array($item)) {
$items .= RendererHelpers::card_media($item, 'aksrt-partner');
}
}
return '<div class="aksrt-widget-partners" style="--aksrt-partner-rows:' . (int) $i['rows_pc'] . ';--aksrt-partner-rows-m:' . (int) $i['rows_mobile'] . ';--aksrt-partner-speed:' . (int) $i['speed'] . 's"><div class="aksrt-widget-partners__track">' . $items . $items . '</div></div>';
}
private static function render_combo_info(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-widget-combo">';
foreach ((array) $i['items'] as $item) {
if (!is_array($item)) {
continue;
}
$tags = '';
foreach (preg_split('/\s*[,,]\s*/', (string) ($item['tags'] ?? '')) ?: [] as $tag) {
if ($tag !== '') {
$tags .= '<span>' . esc_html($tag) . '</span>';
}
}
$html .= '<article class="aksrt-combo-card">' . RendererHelpers::card_media($item) . ($tags ? '<div class="aksrt-combo-card__tags">' . $tags . '</div>' : '') . '</article>';
}
return $html . '</div>';
}
private static function render_text_title(array $i): string {
return '<div class="aksrt-widget-text-title is-' . esc_attr((string) $i['alignment']) . '"><h2>' . nl2br(esc_html((string) $i['heading'])) . '</h2><p>' . nl2br(esc_html((string) $i['description'])) . '</p></div>';
}
private static function render_buttons(array $i): string {
$html = '<div class="aksrt-widget-buttons is-' . esc_attr((string) $i['alignment']) . '" style="--aksrt-button-gap:' . (float) $i['gap'] . 'px">';
foreach ((array) $i['buttons'] as $button) {
$link = RendererHelpers::link((array) ($button['link'] ?? []));
if ($link['url'] === '') {
continue;
}
$html .= '<a class="aksrt-widget-button ' . esc_attr((string) ($button['color'] ?? 'jb-blue')) . '" href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . (!empty($button['icon']) ? '<i class="' . esc_attr((string) $button['icon']) . '" aria-hidden="true"></i>' : '') . '<span>' . esc_html((string) ($button['text'] ?? $link['text'])) . '</span></a>';
}
return $html . '</div>';
}
private static function render_layout_gap(array $i): string {
return '<div class="aksrt-widget-gap" aria-hidden="true" style="--aksrt-gap:' . (float) $i['pc_gap'] . 'px;--aksrt-gap-m:' . (float) $i['m_gap'] . 'px"></div>';
}
private static function render_tag_cloud(array $i): string {
$args = ['taxonomy' => (string) $i['taxonomy'], 'number' => (int) $i['number'], 'hide_empty' => true];
if ($i['orderby'] !== 'rand') {
$args['orderby'] = (string) $i['orderby'];
$args['order'] = 'DESC';
}
$terms = get_terms($args);
if (is_wp_error($terms) || !$terms) {
return RendererHelpers::empty_state(__('暂无标签', 'aksrt'));
}
if ($i['orderby'] === 'rand') {
shuffle($terms);
}
$html = '<div class="aksrt-widget-tag-cloud">';
foreach (array_slice($terms, 0, (int) $i['number']) as $term) {
$url = get_term_link($term);
if (!is_wp_error($url)) {
$html .= '<a href="' . esc_url($url) . '">' . esc_html($term->name) . (!empty($i['show_count']) ? '<small>' . (int) $term->count . '</small>' : '') . '</a>';
}
}
return $html . '</div>';
}
private static function render_recent_comments(array $i): string {
$comments = get_comments(['status' => 'approve', 'number' => (int) $i['count'], 'user__not_in' => RendererHelpers::ids((string) $i['exclude_users']), 'post__not_in' => RendererHelpers::ids((string) $i['exclude_posts'])]);
if (!$comments) {
return RendererHelpers::empty_state(__('暂无评论', 'aksrt'));
}
$html = '<ul class="aksrt-widget-comments">';
foreach ($comments as $comment) {
$html .= '<li><a class="aksrt-widget-comments__avatar" href="' . esc_url(get_comment_link($comment)) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($comment, 48) : get_avatar($comment, 48)) . '</a><div><strong>' . esc_html(get_comment_author($comment)) . '</strong><p>' . esc_html(wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 18)) . '</p>' . (!empty($i['show_source']) ? '<a class="aksrt-widget-comments__source" href="' . esc_url(get_comment_link($comment)) . '">' . esc_html(get_the_title((int) $comment->comment_post_ID)) . '</a>' : '') . '</div></li>';
}
return $html . '</ul>';
}
private static function render_links(array $i): string {
$html = '<div class="aksrt-widget-links aksrt-widget-links--' . esc_attr((string) $i['style']) . '">';
foreach (array_slice((array) $i['links'], 0, (int) $i['limit']) as $item) {
$link = RendererHelpers::link((array) ($item['link'] ?? []));
if ($link['url']) {
$html .= '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '><strong>' . esc_html((string) ($item['title'] ?? $link['text'])) . '</strong><small>' . esc_html((string) ($item['description'] ?? '')) . '</small></a>';
}
}
return $html . '</div>';
}
private static function render_notice(array $i): string {
$slides = '';
foreach ((array) $i['items'] as $item) {
$link = RendererHelpers::link((array) ($item['link'] ?? []));
$content = '<strong>' . esc_html((string) ($item['title'] ?? '')) . '</strong><span>' . esc_html((string) ($item['description'] ?? '')) . '</span>';
$slides .= '<div class="swiper-slide">' . ($link['url'] ? '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . $content . '</a>' : $content) . '</div>';
}
return '<div class="aksrt-widget-notice"><i class="fas fa-bullhorn" aria-hidden="true"></i><div class="swiper aksrt-widget-swiper" data-aksrt-swiper="notice" data-interval="' . ((int) $i['interval'] * 1000) . '"><div class="swiper-wrapper">' . $slides . '</div></div></div>';
}
}
@@ -0,0 +1,119 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render the seven knowledge-product presentation widgets. */
final class KnowledgeProductRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
switch ($slug) {
case 'pricing_plans': return self::pricing($instance);
case 'reviews': return self::reviews($instance);
case 'faq': return self::faq($instance);
case 'stats': return self::stats($instance);
case 'highlights': return self::highlights($instance);
case 'feature_intro': return self::feature_intro($instance);
case 'tilt_cases': return self::tilt($instance);
}
return '';
}
private static function pricing(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-pricing" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['plans'] as $plan) {
if (!is_array($plan)) {
continue;
}
$url = '';
$label = __('立即了解', 'aksrt');
if (($plan['cta_type'] ?? 'link') === 'post' && !empty($plan['post_id'])) {
$url = (string) get_permalink((int) $plan['post_id']);
$label = __('查看付费内容', 'aksrt');
} elseif (($plan['cta_type'] ?? 'link') === 'vip') {
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('vip') : home_url('/');
$url = add_query_arg('level', (int) ($plan['vip_level'] ?? 1), $url);
$label = __('开通会员', 'aksrt');
} else {
$link = RendererHelpers::link((array) ($plan['link'] ?? []));
$url = $link['url'];
$label = $link['text'] ?: $label;
}
$html .= '<article class="aksrt-pricing-card' . (!empty($plan['featured']) ? ' is-featured' : '') . '">';
if (!empty($plan['featured'])) {
$html .= '<span class="aksrt-pricing-card__badge">' . esc_html__('推荐', 'aksrt') . '</span>';
}
$html .= '<h3>' . esc_html((string) ($plan['title'] ?? '')) . '</h3><div class="aksrt-pricing-card__price">' . esc_html((string) ($plan['price'] ?? '')) . '</div><p>' . esc_html((string) ($plan['description'] ?? '')) . '</p><ul>';
foreach (RendererHelpers::lines((string) ($plan['features'] ?? '')) as $feature) {
$html .= '<li><i class="fas fa-check" aria-hidden="true"></i>' . esc_html($feature) . '</li>';
}
$html .= '</ul>' . ($url !== '' ? '<a class="aksrt-widget-button" href="' . esc_url($url) . '">' . esc_html($label) . '</a>' : '') . '</article>';
}
return $html . '</div>';
}
private static function reviews(array $i): string {
$slides = '';
foreach ((array) $i['items'] as $item) {
if (!is_array($item)) {
continue;
}
$rating = max(1, min(5, (int) ($item['rating'] ?? 5)));
$slides .= '<div class="swiper-slide"><article class="aksrt-review-card"><div class="aksrt-review-card__rating" aria-label="' . esc_attr(sprintf(__('%d 星评价', 'aksrt'), $rating)) . '">' . str_repeat('<i class="fas fa-star" aria-hidden="true"></i>', $rating) . '</div><blockquote>' . esc_html((string) ($item['description'] ?? '')) . '</blockquote><footer>' . RendererHelpers::image((string) ($item['image'] ?? ''), '', 'aksrt-review-card__avatar') . '<span><strong>' . esc_html((string) ($item['title'] ?? '')) . '</strong><small>' . esc_html((string) ($item['value'] ?? '')) . '</small></span></footer></article></div>';
}
return '<div class="swiper aksrt-widget-swiper aksrt-reviews" data-aksrt-swiper="cards"><div class="swiper-wrapper">' . $slides . '</div><div class="swiper-pagination"></div></div>';
}
private static function faq(array $i): string {
$html = '<div class="aksrt-faq">';
if (!empty($i['search'])) {
$html .= '<label class="aksrt-faq__search"><i class="fas fa-search" aria-hidden="true"></i><span class="screen-reader-text">' . esc_html__('搜索常见问题', 'aksrt') . '</span><input type="search" placeholder="' . esc_attr__('搜索问题', 'aksrt') . '"></label>';
}
$html .= '<div class="aksrt-faq__items">';
foreach ((array) $i['items'] as $index => $item) {
$id = 'aksrt-faq-' . $index . '-' . wp_rand(1000, 9999);
$html .= '<section class="aksrt-faq__item" data-search="' . esc_attr(wp_strip_all_tags((string) ($item['question'] ?? '') . ' ' . (string) ($item['answer'] ?? ''))) . '"><button type="button" aria-expanded="false" aria-controls="' . esc_attr($id) . '"><span>' . esc_html((string) ($item['question'] ?? '')) . '</span><i class="fas fa-plus" aria-hidden="true"></i></button><div id="' . esc_attr($id) . '" hidden>' . wp_kses_post((string) ($item['answer'] ?? '')) . '</div></section>';
}
return $html . '</div></div>';
}
private static function stats(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-stats" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $item) {
$html .= '<article class="aksrt-stat"><strong data-count-up="' . esc_attr((string) ($item['value'] ?? '')) . '">' . esc_html((string) ($item['value'] ?? '')) . '</strong><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p></article>';
}
return $html . '</div>';
}
private static function highlights(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-highlights" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $index => $item) {
$html .= RendererHelpers::card_media((array) $item, 'aksrt-highlight aksrt-highlight--' . (($index % 5 === 0) ? 'large' : 'normal'));
}
return $html . '</div>';
}
private static function feature_intro(array $i): string {
$tabs = $panels = '';
foreach ((array) $i['items'] as $index => $item) {
$tabs .= '<button type="button" role="tab" data-feature-index="' . $index . '" aria-selected="' . ($index === 0 ? 'true' : 'false') . '">' . esc_html((string) ($item['title'] ?? '')) . '</button>';
$features = '';
foreach (RendererHelpers::lines((string) ($item['features'] ?? '')) as $feature) {
$features .= '<li>' . esc_html($feature) . '</li>';
}
$panels .= '<article role="tabpanel" data-feature-panel="' . $index . '"' . ($index ? ' hidden' : '') . '><div class="aksrt-feature-intro__media">' . RendererHelpers::card_media((array) $item) . '</div><div class="aksrt-feature-intro__copy"><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p><ul>' . $features . '</ul></div></article>';
}
return '<div class="aksrt-feature-intro"><div class="aksrt-feature-intro__tabs" role="tablist">' . $tabs . '</div><div class="aksrt-feature-intro__panels">' . $panels . '</div></div>';
}
private static function tilt(array $i): string {
$html = '<div class="aksrt-widget-grid aksrt-tilt-cases" style="--aksrt-cols:' . (int) $i['pc_row'] . ';--aksrt-cols-m:' . (int) $i['m_row'] . '">';
foreach ((array) $i['items'] as $item) {
$html .= '<div class="aksrt-tilt-case" data-aksrt-tilt data-tilt-max="9" data-tilt-speed="500">' . RendererHelpers::card_media((array) $item) . '</div>';
}
return $html . '</div>';
}
}
@@ -0,0 +1,55 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render full sliders and the slider/media combination. */
final class SliderRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
$slides = '';
foreach ((array) $instance['slides'] as $slide) {
if (!is_array($slide) || (empty($slide['background']) && empty($slide['background_video']))) {
continue;
}
$link = RendererHelpers::link((array) ($slide['link'] ?? []));
$media = !empty($slide['background_video'])
? '<video src="' . esc_url((string) $slide['background_video']) . '" muted loop playsinline preload="metadata"></video>'
: RendererHelpers::image((string) $slide['background'], (string) ($slide['title'] ?? ''), 'swiper-lazy');
$layers = '';
foreach ((array) ($slide['layers'] ?? []) as $layer) {
if (!empty($layer['image'])) {
$layers .= '<img class="aksrt-slide-layer" src="' . esc_url((string) $layer['image']) . '" alt="" data-swiper-parallax="' . (int) ($layer['parallax'] ?? 0) . '" data-swiper-parallax-scale="' . esc_attr((string) ((float) ($layer['scale'] ?? 100) / 100)) . '">';
}
}
$copy = '<div class="aksrt-slide-copy is-' . esc_attr((string) ($slide['text_align'] ?? 'left-bottom')) . '"><h3>' . esc_html((string) ($slide['title'] ?? '')) . '</h3><p>' . esc_html((string) ($slide['description'] ?? '')) . '</p></div>';
$body = $media . $layers . $copy;
if ($link['url']) {
$body = '<a href="' . $link['url'] . '"' . RendererHelpers::link_attrs($link) . '>' . $body . '</a>';
}
$slides .= '<div class="swiper-slide' . (!empty($slide['hide_mobile']) ? ' aksrt-slide--hide-mobile' : '') . '">' . $body . '</div>';
}
if ($slides === '') {
return RendererHelpers::empty_state(__('请配置幻灯片', 'aksrt'));
}
$slider = '<div class="swiper aksrt-widget-swiper aksrt-slider" data-aksrt-swiper="slider" data-direction="' . esc_attr((string) $instance['direction']) . '" data-effect="' . esc_attr((string) $instance['effect']) . '" data-loop="' . (!empty($instance['loop']) ? '1' : '0') . '" data-autoplay="' . (!empty($instance['autoplay']) ? '1' : '0') . '" data-interval="' . ((int) $instance['interval'] * 1000) . '" data-speed="' . (int) $instance['speed'] . '" data-space="' . (int) $instance['spacebetween'] . '" style="--aksrt-slider-height:' . (int) $instance['pc_height'] . 'px;--aksrt-slider-height-m:' . (int) $instance['m_height'] . 'px"><div class="swiper-wrapper">' . $slides . '</div>';
if (!empty($instance['button'])) {
$slider .= '<button type="button" class="swiper-button-prev" aria-label="' . esc_attr__('上一张', 'aksrt') . '"></button><button type="button" class="swiper-button-next" aria-label="' . esc_attr__('下一张', 'aksrt') . '"></button>';
}
if (!empty($instance['pagination'])) {
$slider .= '<div class="swiper-pagination"></div>';
}
$slider .= '</div>';
if ($slug !== 'slider_media_combo') {
return $slider;
}
$cards = '';
foreach ((array) ($instance['side_cards'] ?? []) as $card) {
$cards .= RendererHelpers::card_media((array) $card);
}
return '<div class="aksrt-slider-combo"><div class="aksrt-slider-combo__main">' . $slider . '</div><aside class="aksrt-slider-combo__side">' . $cards . '</aside></div>';
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace AKSRT\WidgetSystem\Renderers;
use AKSRT\WidgetSystem\RendererHelpers;
defined('ABSPATH') || exit;
/** Render login, author, ranking and user-list widgets. */
final class UserRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
switch ($slug) {
case 'user_card': return self::user_card($instance);
case 'author_card': return self::author_card($instance);
case 'user_ranking': return self::ranking($instance);
case 'user_list': return self::user_list($instance);
}
return '';
}
private static function user_card(array $i): string {
$user_id = get_current_user_id();
if ($user_id <= 0) {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
$register = function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url();
$oauth_html = '';
if (class_exists('AKSRT\\Auth\\OAuth')) {
$oauth = \AKSRT\Auth\OAuth::get_instance();
if (is_callable([$oauth, 'render_social_buttons'])) {
$oauth_html = (string) $oauth->render_social_buttons(__('社交账号登录', 'aksrt'));
} elseif (is_callable([$oauth, 'get_login_buttons'])) {
$oauth_html = (string) $oauth->get_login_buttons();
}
}
return '<div class="aksrt-user-card is-guest"><div class="aksrt-user-card__cover"></div><div class="aksrt-user-card__body">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html(0, 88) : get_avatar(0, 88)) . '<h3>' . esc_html__('Hi,请登录', 'aksrt') . '</h3><p>' . esc_html__('登录后管理课程、会员、订单与创作内容。', 'aksrt') . '</p><div class="aksrt-user-card__actions"><a class="aksrt-widget-button signin-loader" href="' . esc_url($login) . '">' . esc_html__('登录', 'aksrt') . '</a><a class="aksrt-widget-button is-ghost signup-loader" href="' . esc_url($register) . '">' . esc_html__('注册', 'aksrt') . '</a></div>' . ($oauth_html !== '' ? '<div class="aksrt-user-card__oauth">' . $oauth_html . '</div>' : '') . '</div></div>';
}
$user = get_userdata($user_id);
return $user instanceof \WP_User ? self::profile_card($user, $i, true) : '';
}
private static function author_card(array $i): string {
$post = get_post();
if (!$post instanceof \WP_Post) {
return '';
}
$user = get_userdata((int) $post->post_author);
return $user instanceof \WP_User ? self::profile_card($user, $i, false) : '';
}
private static function profile_card(\WP_User $user, array $i, bool $self_card): string {
$user_id = (int) $user->ID;
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
$cover = (string) get_user_meta($user_id, 'aksrt_background_image', true);
$followers = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id) : 0;
$following = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_following_count($user_id) : 0;
$html = '<article class="aksrt-user-card' . (!$self_card ? ' is-author' : '') . (empty($i['show_cover']) ? ' no-cover' : '') . '">';
if (!empty($i['show_cover'])) {
$html .= '<div class="aksrt-user-card__cover"' . ($cover ? ' style="background-image:url(' . esc_url($cover) . ')"' : '') . '></div>';
}
$html .= '<div class="aksrt-user-card__body"><a class="aksrt-user-card__avatar" href="' . esc_url($home) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($user_id, 96) : get_avatar($user_id, 96)) . '</a><h3><a href="' . esc_url($home) . '">' . esc_html($user->display_name) . '</a>' . (function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($user_id) : '') . '</h3>';
if (function_exists('aksrt_render_user_identity_chips')) {
$html .= '<div class="aksrt-user-card__identity">' . aksrt_render_user_identity_chips($user_id, 'widget') . '</div>';
}
$html .= '<p>' . esc_html(wp_trim_words((string) $user->description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
if (!empty($i['show_buttons'])) {
$html .= '<div class="aksrt-user-card__actions">' . self::action((string) $i['primary_action'], $user, true) . self::action((string) $i['secondary_action'], $user, false) . '</div>';
}
if (!$self_card && get_current_user_id() > 0 && get_current_user_id() !== $user_id) {
$active = class_exists('AKSRT\\User\\Follow') && \AKSRT\User\Follow::get_instance()->is_following(get_current_user_id(), $user_id);
$html .= '<button class="aksrt-user-card__follow' . ($active ? ' is-following' : '') . '" type="button" data-aksrt-follow="' . $user_id . '">' . esc_html($active ? __('已关注', 'aksrt') : __('关注', 'aksrt')) . '</button>';
}
$html .= '</div>';
if (!empty($i['show_posts']) && function_exists('aksrt_render_user_card_posts')) {
$html .= '<div class="aksrt-user-card__posts">' . aksrt_render_user_card_posts(['user_id' => $user_id, 'count' => 4, 'post_style' => 'mini', 'show_empty' => false]) . '</div>';
}
return $html . '</article>';
}
private static function action(string $action, \WP_User $user, bool $primary): string {
$labels = ['home' => __('个人主页', 'aksrt'), 'submit' => __('发布文章', 'aksrt'), 'vip' => __('会员中心', 'aksrt'), 'orders' => __('订单中心', 'aksrt'), 'recharge' => __('余额充值', 'aksrt'), 'signin' => __('签到', 'aksrt')];
if ($action === 'signin' && function_exists('aksrt_header_user_checkin_btn') && get_current_user_id() === (int) $user->ID) {
return '<span class="aksrt-user-card__checkin">' . aksrt_header_user_checkin_btn((int) $user->ID, true) . '</span>';
}
if ($action === 'home') {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url((int) $user->ID);
} elseif ($action === 'submit') {
$url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : home_url('/submit/');
} else {
$tab = $action === 'signin' ? 'credits' : $action;
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url($tab) : home_url('/user/');
}
if ($url === '') {
return '';
}
return '<a class="aksrt-widget-button' . (!$primary ? ' is-ghost' : '') . '" href="' . esc_url($url) . '">' . esc_html($labels[$action] ?? $labels['home']) . '</a>';
}
private static function ranking(array $i): string {
$users = get_users(['number' => 200, 'orderby' => 'registered', 'order' => 'DESC']);
$metric = (string) $i['orderby'];
usort($users, static fn(\WP_User $a, \WP_User $b): int => self::metric($b, $metric) <=> self::metric($a, $metric));
$users = array_slice($users, 0, (int) $i['number']);
if (!$users) {
return RendererHelpers::empty_state(__('暂无用户数据', 'aksrt'));
}
$html = '<ol class="aksrt-user-ranking">';
foreach ($users as $index => $user) {
$html .= '<li><span class="aksrt-user-ranking__index' . (!empty($i['show_badge']) && $index < 3 ? ' is-top' : '') . '">' . ($index + 1) . '</span>' . self::user_tile($user, self::metric($user, $metric), self::metric_label($metric)) . '</li>';
}
return $html . '</ol>';
}
private static function user_list(array $i): string {
$args = ['number' => 200];
if (!empty($i['include'])) {
$args['include'] = RendererHelpers::ids((string) $i['include']);
}
if (!empty($i['exclude'])) {
$args['exclude'] = RendererHelpers::ids((string) $i['exclude']);
}
$users = get_users($args);
$orderby = (string) $i['orderby'];
usort($users, static function (\WP_User $a, \WP_User $b) use ($orderby): int {
if ($orderby === 'display_name') {
return strcasecmp($a->display_name, $b->display_name);
}
if ($orderby === 'registered') {
return strcmp($b->user_registered, $a->user_registered);
}
return self::metric($b, $orderby) <=> self::metric($a, $orderby);
});
$html = '<div class="aksrt-widget-grid aksrt-user-list">';
foreach (array_slice($users, 0, (int) $i['number']) as $user) {
$html .= '<article>' . self::user_tile($user, self::metric($user, $orderby), self::metric_label($orderby)) . '</article>';
}
return $html . '</div>';
}
private static function user_tile(\WP_User $user, int $value, string $label): string {
$url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url((int) $user->ID);
return '<a class="aksrt-user-tile" href="' . esc_url($url) . '">' . (function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($user->ID, 64) : get_avatar($user->ID, 64)) . '<span><strong>' . esc_html($user->display_name) . '</strong><small>' . esc_html($label) . ' ' . number_format_i18n($value) . '</small></span></a>';
}
private static function metric(\WP_User $user, string $metric): int {
$id = (int) $user->ID;
if ($metric === 'points') {
return function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($id) : 0;
}
if ($metric === 'followers') {
return class_exists('AKSRT\\User\\Follow') ? (int) \AKSRT\User\Follow::get_instance()->get_follower_count($id) : 0;
}
if ($metric === 'post_count') {
return (int) count_user_posts($id, 'post', true);
}
$key = $metric === 'checkin_continuous_day' ? 'aksrt_checkin_continuous_day' : 'aksrt_checkin_all_day';
return (int) get_user_meta($id, $key, true);
}
private static function metric_label(string $metric): string {
$labels = ['points' => __('积分', 'aksrt'), 'followers' => __('粉丝', 'aksrt'), 'post_count' => __('文章', 'aksrt'), 'registered' => __('新加入', 'aksrt'), 'display_name' => __('用户', 'aksrt'), 'checkin_all_day' => __('累计签到', 'aksrt'), 'checkin_continuous_day' => __('连续签到', 'aksrt')];
return $labels[$metric] ?? $labels['points'];
}
}