feat(widgets): pricing/FAQ/stats + forum topic list and bbs sidebars

Add zibll produck widgets (pricing plans, FAQ, stats), forum topic
lists, register bbs_* sidebar zones, and wire plate/topic/home/compose
templates to render those widget areas.
This commit is contained in:
2026-07-19 00:22:31 +08:00
parent 81bf3193a4
commit d8682ce1fe
11 changed files with 1036 additions and 1 deletions
+23
View File
@@ -382,6 +382,29 @@ trait SetupPart1 {
'after_title' => $title_after,
]);
// ── Forum / BBS widget areas (zibll bbs_* parity lite) ──
$forum_widgets = [
'bbs_home_sidebar' => [__('论坛首页 · 侧栏', 'aksrt'), __('论坛首页侧栏区域。', 'aksrt')],
'bbs_home_top_fluid' => [__('论坛首页 · 顶部全宽', 'aksrt'), __('论坛首页顶部全宽。', 'aksrt')],
'bbs_plate_sidebar' => [__('论坛版块 · 侧栏', 'aksrt'), __('版块页侧栏。', 'aksrt')],
'bbs_plate_top_fluid' => [__('论坛版块 · 顶部全宽', 'aksrt'), __('版块页顶部全宽。', 'aksrt')],
'bbs_posts_sidebar' => [__('论坛帖子 · 侧栏', 'aksrt'), __('帖子页侧栏挂件区。', 'aksrt')],
'bbs_posts_top_fluid' => [__('论坛帖子 · 顶部全宽', 'aksrt'), __('帖子页顶部全宽。', 'aksrt')],
'bbs_new_posts_sidebar_top' => [__('论坛发帖 · 侧栏顶部', 'aksrt'), __('发帖页侧栏顶部。', 'aksrt')],
'bbs_new_posts_sidebar_bottom' => [__('论坛发帖 · 侧栏底部', 'aksrt'), __('发帖页侧栏底部。', 'aksrt')],
];
foreach ($forum_widgets as $sid => $meta) {
register_sidebar([
'id' => $sid,
'name' => $meta[0],
'description' => $meta[1],
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
}
// ── Shop widget areas (§6.9) ──
$shop_widgets = [
'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'],
+189
View File
@@ -0,0 +1,189 @@
<?php
/**
* FAQ accordion (zibll zib_widget_faq lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetFaq extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_faq',
esc_html__('AKSRT 常见问题', 'aksrt'),
[
'description' => esc_html__('可折叠 FAQ 列表,支持搜索框与侧栏文档卡。', 'aksrt'),
'classname' => 'widget_aksrt_faq',
]
);
}
public function widget($args, $instance): void {
$faqs = $this->normalize_faqs($instance['faqs'] ?? []);
if (!$faqs) {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$uid = 'aksrt-faq-' . esc_attr($this->id);
echo '<div class="aksrt-faq" id="' . $uid . '">';
if (!empty($instance['search_s'])) {
$ph = (string) ($instance['search_placeholder'] ?? __('搜索问题…', 'aksrt'));
echo '<form class="aksrt-faq__search" method="get" action="' . esc_url(home_url('/')) . '" role="search">';
echo '<input type="search" name="s" class="aksrt-faq__search-input" placeholder="' . esc_attr($ph) . '" />';
echo '<button type="submit" class="aksrt-faq__search-btn">' . esc_html((string) ($instance['search_btn_text'] ?? __('搜索', 'aksrt'))) . '</button>';
echo '</form>';
}
echo '<div class="aksrt-faq__list">';
$n = 0;
foreach ($faqs as $f) {
$n++;
$num = str_pad((string) $n, 2, '0', STR_PAD_LEFT);
echo '<details class="aksrt-faq__item">';
echo '<summary class="aksrt-faq__q"><span class="aksrt-faq__num">' . esc_html($num) . '</span><span class="aksrt-faq__qtext">' . esc_html($f['question']) . '</span></summary>';
if ($f['answer'] !== '') {
echo '<div class="aksrt-faq__a">' . wp_kses_post(wpautop($f['answer'])) . '</div>';
}
echo '</details>';
}
echo '</div>';
$doc_title = trim((string) ($instance['doc_title'] ?? ''));
$doc_desc = trim((string) ($instance['doc_desc'] ?? ''));
$help_title = trim((string) ($instance['help_title'] ?? ''));
$help_url = esc_url_raw(trim((string) ($instance['help_btn_url'] ?? '')));
$help_btn = trim((string) ($instance['help_btn_text'] ?? ''));
if ($doc_title !== '' || $help_title !== '') {
echo '<div class="aksrt-faq__aside">';
if ($doc_title !== '') {
echo '<div class="aksrt-faq__doc"><strong>' . esc_html($doc_title) . '</strong>';
if ($doc_desc !== '') {
echo '<p>' . esc_html($doc_desc) . '</p>';
}
echo '</div>';
}
if ($help_title !== '') {
echo '<div class="aksrt-faq__help"><strong>' . esc_html($help_title) . '</strong>';
$sub = trim((string) ($instance['help_subtitle'] ?? ''));
if ($sub !== '') {
echo '<p>' . esc_html($sub) . '</p>';
}
if ($help_btn !== '' && $help_url !== '') {
echo '<a class="aksrt-faq__help-btn" href="' . esc_url($help_url) . '">' . esc_html($help_btn) . '</a>';
}
echo '</div>';
}
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param mixed $raw
* @return array<int, array{question:string,answer:string}>
*/
private function normalize_faqs($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$q = sanitize_text_field((string) ($row['question'] ?? $row['q'] ?? ''));
if ($q === '') {
continue;
}
$a = (string) ($row['answer'] ?? $row['a'] ?? '');
// Allow basic HTML in answers after kses on output.
$out[] = [
'question' => $q,
'answer' => wp_kses_post($a),
];
if (count($out) >= 30) {
break;
}
}
return $out;
}
public function form($instance): void {
$faqs = is_string($instance['faqs'] ?? null)
? (string) $instance['faqs']
: wp_json_encode($this->normalize_faqs($instance['faqs'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('FAQ JSON', 'aksrt'); ?>
<textarea class="widefat" rows="8" name="<?php echo esc_attr($this->get_field_name('faqs')); ?>" placeholder='[{"question":"如何付费?","answer":"进入用户中心…"}]'><?php echo esc_textarea((string) $faqs); ?></textarea>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('search_s')); ?>" value="1" <?php checked(!empty($instance['search_s'])); ?>> <?php esc_html_e('显示搜索框', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('文档卡标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('doc_title')); ?>" value="<?php echo esc_attr((string) ($instance['doc_title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('文档卡描述', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('doc_desc')); ?>" value="<?php echo esc_attr((string) ($instance['doc_desc'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('帮助卡标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('help_title')); ?>" value="<?php echo esc_attr((string) ($instance['help_title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('帮助按钮文案', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('help_btn_text')); ?>" value="<?php echo esc_attr((string) ($instance['help_btn_text'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('帮助按钮链接', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('help_btn_url')); ?>" value="<?php echo esc_attr((string) ($instance['help_btn_url'] ?? '')); ?>">
</label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'faqs' => $this->normalize_faqs($new_instance['faqs'] ?? []),
'search_s' => !empty($new_instance['search_s']) ? 1 : 0,
'search_placeholder' => sanitize_text_field((string) ($new_instance['search_placeholder'] ?? '')),
'search_btn_text' => sanitize_text_field((string) ($new_instance['search_btn_text'] ?? '')),
'doc_title' => sanitize_text_field((string) ($new_instance['doc_title'] ?? '')),
'doc_desc' => sanitize_text_field((string) ($new_instance['doc_desc'] ?? '')),
'help_title' => sanitize_text_field((string) ($new_instance['help_title'] ?? '')),
'help_subtitle' => sanitize_text_field((string) ($new_instance['help_subtitle'] ?? '')),
'help_btn_text' => sanitize_text_field((string) ($new_instance['help_btn_text'] ?? '')),
'help_btn_url' => esc_url_raw(trim((string) ($new_instance['help_btn_url'] ?? ''))),
];
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
/**
* Forum topic list widget (zibll zib_bbs_widget_ui_posts_lists lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetForumTopicLists extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_forum_topic_lists',
esc_html__('论坛 - 帖子列表', 'aksrt'),
[
'description' => esc_html__('按排序/版块筛选展示论坛帖子。', 'aksrt'),
'classname' => 'widget_aksrt_forum_topic_lists',
]
);
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_topic')) {
return;
}
$count = max(1, min(30, (int) ($instance['count'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'date'));
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
$plate_id = (int) ($instance['plate_id'] ?? 0);
$current_plate = !empty($instance['current_plate']);
if ($current_plate) {
$resolved = $this->resolve_current_plate_id();
if ($resolved <= 0) {
return;
}
$plate_id = $resolved;
}
$query = [
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => $count,
'order' => 'DESC',
];
switch ($orderby) {
case 'views':
$query['meta_key'] = '_aksrt_views';
$query['orderby'] = 'meta_value_num';
break;
case 'comments':
$query['orderby'] = 'comment_count';
break;
case 'modified':
$query['orderby'] = 'modified';
break;
default:
$query['orderby'] = 'date';
}
if ($plate_id > 0) {
$query['meta_query'] = [[
'key' => '_aksrt_plate_id',
'value' => $plate_id,
]];
}
$q = new \WP_Query($query);
if (!$q->have_posts()) {
wp_reset_postdata();
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$list_class = $style === 'detail' ? 'aksrt-ftopic-list is-detail' : 'aksrt-ftopic-list is-mini';
echo '<ul class="' . esc_attr($list_class) . '">';
while ($q->have_posts()) {
$q->the_post();
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
echo '<li class="aksrt-ftopic-list__item">';
echo '<a class="aksrt-ftopic-list__link" href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a>';
if ($style === 'detail') {
echo '<div class="aksrt-ftopic-list__meta">';
echo '<span>' . esc_html(get_the_author()) . '</span>';
echo '<span>' . esc_html(get_the_date()) . '</span>';
echo '<span><i class="fas fa-eye"></i> ' . esc_html((string) $views) . '</span>';
echo '</div>';
} else {
echo '<span class="aksrt-ftopic-list__views">' . esc_html((string) $views) . '</span>';
}
echo '</li>';
}
echo '</ul>';
wp_reset_postdata();
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function resolve_current_plate_id(): int {
if (is_singular('aksrt_forum_plate')) {
return (int) get_queried_object_id();
}
if (is_singular('aksrt_forum_topic')) {
$topic_id = (int) get_queried_object_id();
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
return max(0, $plate_id);
}
return 0;
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? __('最新帖子', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="30" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['count'] ?? 8))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="date" <?php selected(($instance['orderby'] ?? 'date'), 'date'); ?>><?php esc_html_e('发布时间', 'aksrt'); ?></option>
<option value="views" <?php selected(($instance['orderby'] ?? ''), 'views'); ?>><?php esc_html_e('浏览量', 'aksrt'); ?></option>
<option value="comments" <?php selected(($instance['orderby'] ?? ''), 'comments'); ?>><?php esc_html_e('评论数', 'aksrt'); ?></option>
<option value="modified" <?php selected(($instance['orderby'] ?? ''), 'modified'); ?>><?php esc_html_e('更新时间', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected(($instance['style'] ?? 'mini'), 'mini'); ?>><?php esc_html_e('紧凑', 'aksrt'); ?></option>
<option value="detail" <?php selected(($instance['style'] ?? ''), 'detail'); ?>><?php esc_html_e('详情', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('限定版块 ID(0=全部)', 'aksrt'); ?>
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('plate_id')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['plate_id'] ?? 0))); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('current_plate')); ?>" value="1" <?php checked(!empty($instance['current_plate'])); ?>> <?php esc_html_e('仅当前版块(版块/帖子页)', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'date'));
if (!in_array($orderby, ['date', 'views', 'comments', 'modified'], true)) {
$orderby = 'date';
}
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'count' => max(1, min(30, (int) ($new_instance['count'] ?? 8))),
'orderby' => $orderby,
'style' => ($new_instance['style'] ?? '') === 'detail' ? 'detail' : 'mini',
'plate_id' => max(0, (int) ($new_instance['plate_id'] ?? 0)),
'current_plate' => !empty($new_instance['current_plate']) ? 1 : 0,
];
}
}
+236
View File
@@ -0,0 +1,236 @@
<?php
/**
* Pricing plan cards (zibll zib_widget_pricing_plans lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetPricingPlans extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_pricing_plans',
esc_html__('AKSRT 价格方案', 'aksrt'),
[
'description' => esc_html__('多列定价卡片 + 信任条(落地页/VIP 介绍)。', 'aksrt'),
'classname' => 'widget_aksrt_pricing_plans',
]
);
}
public function widget($args, $instance): void {
$plans = $this->normalize_plans($instance['lists'] ?? $instance['plans'] ?? []);
if (!$plans) {
return;
}
$trust = $this->normalize_trust($instance['trust_items'] ?? []);
$currency = sanitize_text_field((string) ($instance['currency'] ?? '¥'));
if ($currency === '') {
$currency = '¥';
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="aksrt-pricing">';
echo '<div class="aksrt-pricing__cards">';
foreach ($plans as $plan) {
$accent = sanitize_key((string) ($plan['color'] ?? 'blue'));
if (!in_array($accent, ['pink', 'purple', 'blue', 'green', 'yellow', 'cyan'], true)) {
$accent = 'blue';
}
$featured = !empty($plan['is_featured']);
$class = 'aksrt-pricing__card accent-' . $accent . ($featured ? ' is-featured' : '');
echo '<div class="' . esc_attr($class) . '">';
if ($featured || !empty($plan['tag'])) {
$badge = (string) ($plan['tag'] !== '' ? $plan['tag'] : __('推荐', 'aksrt'));
echo '<span class="aksrt-pricing__badge">' . esc_html($badge) . '</span>';
}
echo '<h3 class="aksrt-pricing__name">' . esc_html($plan['name']) . '</h3>';
if ($plan['desc'] !== '') {
echo '<p class="aksrt-pricing__desc">' . esc_html($plan['desc']) . '</p>';
}
$price = (float) $plan['price'];
$show = (float) $plan['show_price'];
echo '<div class="aksrt-pricing__price">';
echo '<span class="aksrt-pricing__main"><span class="aksrt-pricing__mark">' . esc_html($currency) . '</span>' . esc_html($this->fmt($price)) . '</span>';
if ($show > $price && $show > 0) {
echo '<span class="aksrt-pricing__original"><span class="aksrt-pricing__mark">' . esc_html($currency) . '</span>' . esc_html($this->fmt($show)) . '</span>';
}
echo '</div>';
if ($plan['features']) {
echo '<ul class="aksrt-pricing__features">';
foreach ($plan['features'] as $ft) {
echo '<li><i class="fa fa-check" aria-hidden="true"></i><span>' . esc_html($ft) . '</span></li>';
}
echo '</ul>';
}
if ($plan['btn_text'] !== '' && $plan['btn_url'] !== '') {
$url = $plan['btn_url'];
if (!empty($instance['go_link']) && function_exists('aksrt_go_link')) {
$url = aksrt_go_link($url, true);
}
echo '<div class="aksrt-pricing__action"><a class="aksrt-pricing__btn" href="' . esc_url($url) . '">' . esc_html($plan['btn_text']) . '</a></div>';
}
echo '</div>';
}
echo '</div>';
if ($trust) {
echo '<div class="aksrt-pricing__trust">';
foreach ($trust as $ti) {
echo '<div class="aksrt-pricing__trust-item">';
if ($ti['icon'] !== '') {
echo '<i class="' . esc_attr($ti['icon']) . '" aria-hidden="true"></i>';
}
echo '<div><strong>' . esc_html($ti['title']) . '</strong>';
if ($ti['desc'] !== '') {
echo '<span>' . esc_html($ti['desc']) . '</span>';
}
echo '</div></div>';
}
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function fmt(float $n): string {
if (abs($n - round($n)) < 0.001) {
return (string) (int) round($n);
}
return number_format($n, 2, '.', '');
}
/**
* @param mixed $raw
* @return array<int, array<string,mixed>>
*/
private function normalize_plans($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$name = sanitize_text_field((string) ($row['name'] ?? $row['title'] ?? ''));
if ($name === '') {
continue;
}
$intro = (string) ($row['intro'] ?? $row['features'] ?? '');
if (is_array($row['features'] ?? null)) {
$features = array_values(array_filter(array_map('sanitize_text_field', $row['features'])));
} else {
$features = array_values(array_filter(array_map('sanitize_text_field', preg_split('/\r\n|\r|\n/', $intro) ?: [])));
}
$btn = is_array($row['btn'] ?? null) ? $row['btn'] : [];
$out[] = [
'name' => $name,
'desc' => sanitize_text_field((string) ($row['desc'] ?? '')),
'price' => (float) ($row['price'] ?? 0),
'show_price' => (float) ($row['show_price'] ?? 0),
'color' => sanitize_key((string) ($row['color'] ?? 'blue')),
'is_featured' => !empty($row['is_featured']) ? 1 : 0,
'tag' => sanitize_text_field((string) ($row['tag'] ?? '')),
'features' => $features,
'btn_text' => sanitize_text_field((string) ($row['btn_text'] ?? $btn['text'] ?? '')),
'btn_url' => esc_url_raw(trim((string) ($row['btn_url'] ?? $btn['url'] ?? ''))),
];
if (count($out) >= 6) {
break;
}
}
return $out;
}
/**
* @param mixed $raw
* @return array<int, array{title:string,desc:string,icon:string}>
*/
private function normalize_trust($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$title = sanitize_text_field((string) ($row['title'] ?? ''));
if ($title === '') {
continue;
}
$out[] = [
'title' => $title,
'desc' => sanitize_text_field((string) ($row['desc'] ?? '')),
'icon' => sanitize_text_field((string) ($row['icon'] ?? 'fa fa-check-circle')),
];
if (count($out) >= 6) {
break;
}
}
return $out;
}
public function form($instance): void {
$plans = is_string($instance['lists'] ?? null)
? (string) $instance['lists']
: wp_json_encode($this->normalize_plans($instance['lists'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$trust = is_string($instance['trust_items'] ?? null)
? (string) $instance['trust_items']
: wp_json_encode($this->normalize_trust($instance['trust_items'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('货币符号', 'aksrt'); ?>
<input class="tiny-text" type="text" name="<?php echo esc_attr($this->get_field_name('currency')); ?>" value="<?php echo esc_attr((string) ($instance['currency'] ?? '¥')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('方案 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="8" name="<?php echo esc_attr($this->get_field_name('lists')); ?>" placeholder='[{"name":"Pro","price":99,"show_price":199,"desc":"适合创作者","features":["无限下载","优先支持"],"btn_text":"立即开通","btn_url":"#","is_featured":1,"color":"pink"}]'><?php echo esc_textarea((string) $plans); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('信任条 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="3" name="<?php echo esc_attr($this->get_field_name('trust_items')); ?>"><?php echo esc_textarea((string) $trust); ?></textarea>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('go_link')); ?>" value="1" <?php checked(!empty($instance['go_link'])); ?>> <?php esc_html_e('外链跳转页', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'currency' => sanitize_text_field((string) ($new_instance['currency'] ?? '¥')),
'lists' => $this->normalize_plans($new_instance['lists'] ?? []),
'trust_items' => $this->normalize_trust($new_instance['trust_items'] ?? []),
'go_link' => !empty($new_instance['go_link']) ? 1 : 0,
];
}
}
+167
View File
@@ -0,0 +1,167 @@
<?php
/**
* Stats number cards (zibll zib_widget_stats lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetStats extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_stats',
esc_html__('AKSRT 数据统计', 'aksrt'),
[
'description' => esc_html__('数字统计卡片网格(站点数据展示)。', 'aksrt'),
'classname' => 'widget_aksrt_stats',
]
);
}
public function widget($args, $instance): void {
$stats = $this->normalize_stats($instance['stats'] ?? []);
if (!$stats) {
return;
}
$cols = max(1, min(4, (int) ($instance['columns'] ?? 4)));
$m_cols = max(1, min(3, (int) ($instance['mobile_columns'] ?? 2)));
$intro_title = trim((string) ($instance['intro_title'] ?? ''));
$intro_sub = trim((string) ($instance['intro_subtitle'] ?? ''));
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$style = sprintf('--aksrt-stats-pc:%d;--aksrt-stats-m:%d;', $cols, $m_cols);
echo '<div class="aksrt-stats" style="' . esc_attr($style) . '">';
if ($intro_title !== '' || $intro_sub !== '') {
echo '<div class="aksrt-stats__intro">';
if ($intro_title !== '') {
echo '<h2 class="aksrt-stats__intro-title">' . esc_html($intro_title) . '</h2>';
}
if ($intro_sub !== '') {
echo '<p class="aksrt-stats__intro-sub">' . esc_html($intro_sub) . '</p>';
}
echo '</div>';
}
echo '<div class="aksrt-stats__grid">';
$palette = ['pink', 'purple', 'orange', 'blue', 'green'];
$i = 0;
foreach ($stats as $s) {
$color = sanitize_key((string) ($s['color'] ?? 'auto'));
if ($color === '' || $color === 'auto') {
$color = $palette[$i % count($palette)];
}
$i++;
echo '<div class="aksrt-stats__item color-' . esc_attr($color) . '">';
if ($s['icon'] !== '') {
echo '<div class="aksrt-stats__icon"><i class="' . esc_attr($s['icon']) . '" aria-hidden="true"></i></div>';
}
echo '<div class="aksrt-stats__value">';
if ($s['prefix'] !== '') {
echo '<span class="aksrt-stats__prefix">' . esc_html($s['prefix']) . '</span>';
}
echo '<span class="aksrt-stats__number" data-target="' . esc_attr($s['number']) . '">' . esc_html($s['number']) . '</span>';
if ($s['unit'] !== '') {
echo '<span class="aksrt-stats__unit">' . esc_html($s['unit']) . '</span>';
}
echo '</div>';
if ($s['label'] !== '') {
echo '<div class="aksrt-stats__label">' . esc_html($s['label']) . '</div>';
}
echo '</div>';
}
echo '</div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param mixed $raw
* @return array<int, array{number:string,label:string,unit:string,prefix:string,icon:string,color:string}>
*/
private function normalize_stats($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$number = sanitize_text_field((string) ($row['number'] ?? ''));
if ($number === '') {
continue;
}
$out[] = [
'number' => $number,
'label' => sanitize_text_field((string) ($row['label'] ?? '')),
'unit' => sanitize_text_field((string) ($row['unit'] ?? '')),
'prefix' => sanitize_text_field((string) ($row['prefix'] ?? '')),
'icon' => sanitize_text_field((string) ($row['icon'] ?? '')),
'color' => sanitize_key((string) ($row['color'] ?? 'auto')),
];
if (count($out) >= 12) {
break;
}
}
return $out;
}
public function form($instance): void {
$stats = is_string($instance['stats'] ?? null)
? (string) $instance['stats']
: wp_json_encode($this->normalize_stats($instance['stats'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数据 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="6" name="<?php echo esc_attr($this->get_field_name('stats')); ?>" placeholder='[{"number":"1280","label":"用户","unit":"+","icon":"fas fa-users","color":"blue"}]'><?php echo esc_textarea((string) $stats); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('PC 列数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="4" name="<?php echo esc_attr($this->get_field_name('columns')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['columns'] ?? 4))); ?>">
</label>
<label><?php esc_html_e('移动列数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="3" name="<?php echo esc_attr($this->get_field_name('mobile_columns')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['mobile_columns'] ?? 2))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('介绍标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('intro_title')); ?>" value="<?php echo esc_attr((string) ($instance['intro_title'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('介绍副标题', 'aksrt'); ?>
<textarea class="widefat" rows="2" name="<?php echo esc_attr($this->get_field_name('intro_subtitle')); ?>"><?php echo esc_textarea((string) ($instance['intro_subtitle'] ?? '')); ?></textarea>
</label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'stats' => $this->normalize_stats($new_instance['stats'] ?? []),
'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 4))),
'mobile_columns' => max(1, min(3, (int) ($new_instance['mobile_columns'] ?? 2))),
'intro_title' => sanitize_text_field((string) ($new_instance['intro_title'] ?? '')),
'intro_subtitle' => sanitize_text_field((string) ($new_instance['intro_subtitle'] ?? '')),
];
}
}