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
+207
View File
@@ -530,3 +530,210 @@
color: var(--color-primary, #3b82f6);
flex-shrink: 0;
}
/* ---- Pricing plans ---- */
.aksrt-pricing { margin-bottom: 16px; }
.aksrt-pricing__cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.aksrt-pricing__card {
position: relative;
border: 1px solid var(--color-line, #e5e7eb);
border-radius: 14px;
padding: 22px 18px 18px;
background: var(--surface-1, #fff);
display: flex;
flex-direction: column;
min-height: 100%;
}
.aksrt-pricing__card.is-featured {
box-shadow: 0 10px 28px rgba(0,0,0,.08);
border-color: transparent;
border-top: 3px solid var(--plan-accent, #3b82f6);
}
.aksrt-pricing__card.accent-pink { --plan-accent: #ec4899; }
.aksrt-pricing__card.accent-purple { --plan-accent: #a855f7; }
.aksrt-pricing__card.accent-blue { --plan-accent: #3b82f6; }
.aksrt-pricing__card.accent-green { --plan-accent: #22c55e; }
.aksrt-pricing__card.accent-yellow { --plan-accent: #eab308; }
.aksrt-pricing__card.accent-cyan { --plan-accent: #06b6d4; }
.aksrt-pricing__badge {
position: absolute;
top: 12px;
right: 12px;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: var(--plan-accent, #3b82f6);
color: #fff;
}
.aksrt-pricing__name { font-size: 18px; font-weight: 700; margin: 0 0 6px; }
.aksrt-pricing__desc { font-size: 13px; color: var(--muted-2-color, #888); margin: 0 0 12px; }
.aksrt-pricing__price { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
.aksrt-pricing__main { font-size: 32px; font-weight: 800; color: var(--plan-accent, #3b82f6); line-height: 1; }
.aksrt-pricing__mark { font-size: .55em; margin-right: 2px; }
.aksrt-pricing__original { font-size: 13px; color: #999; text-decoration: line-through; }
.aksrt-pricing__features { list-style: none; margin: 0 0 16px; padding: 0; flex: 1; }
.aksrt-pricing__features li { display: flex; gap: 8px; font-size: 13px; padding: 5px 0; }
.aksrt-pricing__features i { color: var(--plan-accent, #3b82f6); margin-top: 2px; }
.aksrt-pricing__btn {
display: block;
text-align: center;
padding: 10px 14px;
border-radius: 999px;
background: var(--plan-accent, #3b82f6);
color: #fff !important;
text-decoration: none;
font-weight: 600;
}
.aksrt-pricing__trust {
display: flex;
flex-wrap: wrap;
gap: 14px 22px;
margin-top: 18px;
justify-content: center;
}
.aksrt-pricing__trust-item { display: flex; gap: 8px; align-items: flex-start; font-size: 13px; }
.aksrt-pricing__trust-item i { color: var(--color-primary, #3b82f6); margin-top: 2px; }
.aksrt-pricing__trust-item strong { display: block; }
.aksrt-pricing__trust-item span { color: var(--muted-2-color, #888); font-size: 12px; }
/* ---- FAQ ---- */
.aksrt-faq { margin-bottom: 16px; }
.aksrt-faq__search { display: flex; gap: 8px; margin-bottom: 14px; }
.aksrt-faq__search-input {
flex: 1;
border: 1px solid var(--color-line, #e5e7eb);
border-radius: 8px;
padding: 8px 12px;
}
.aksrt-faq__search-btn {
border: 0;
border-radius: 8px;
padding: 8px 14px;
background: var(--color-primary, #3b82f6);
color: #fff;
cursor: pointer;
}
.aksrt-faq__item {
border: 1px solid var(--color-line, #e5e7eb);
border-radius: 10px;
padding: 0;
margin-bottom: 10px;
background: var(--surface-1, #fff);
}
.aksrt-faq__q {
list-style: none;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
cursor: pointer;
font-weight: 600;
}
.aksrt-faq__q::-webkit-details-marker { display: none; }
.aksrt-faq__num {
width: 28px;
height: 28px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
background: var(--surface-2, #f3f4f6);
color: var(--color-primary, #3b82f6);
flex-shrink: 0;
}
.aksrt-faq__a {
padding: 0 14px 14px 52px;
font-size: 13px;
color: var(--muted-2-color, #666);
line-height: 1.6;
}
.aksrt-faq__aside { display: grid; gap: 12px; margin-top: 14px; }
.aksrt-faq__doc, .aksrt-faq__help {
border-radius: 10px;
border: 1px solid var(--color-line, #e5e7eb);
padding: 14px;
background: var(--surface-2, #f8fafc);
}
.aksrt-faq__help-btn {
display: inline-block;
margin-top: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--color-primary, #3b82f6);
color: #fff !important;
text-decoration: none;
font-size: 13px;
}
/* ---- Stats ---- */
.aksrt-stats { margin-bottom: 16px; }
.aksrt-stats__intro { margin-bottom: 14px; }
.aksrt-stats__intro-title { font-size: 22px; margin: 0 0 6px; }
.aksrt-stats__intro-sub { color: var(--muted-2-color, #888); margin: 0; font-size: 13px; }
.aksrt-stats__grid {
display: grid;
grid-template-columns: repeat(var(--aksrt-stats-pc, 4), minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 767.98px) {
.aksrt-stats__grid {
grid-template-columns: repeat(var(--aksrt-stats-m, 2), minmax(0, 1fr));
}
}
.aksrt-stats__item {
border-radius: 12px;
padding: 16px 14px;
background: var(--surface-1, #fff);
border: 1px solid var(--color-line, #e5e7eb);
text-align: center;
}
.aksrt-stats__item.color-pink { --stat-c: #ec4899; }
.aksrt-stats__item.color-purple { --stat-c: #a855f7; }
.aksrt-stats__item.color-orange { --stat-c: #f97316; }
.aksrt-stats__item.color-blue { --stat-c: #3b82f6; }
.aksrt-stats__item.color-green { --stat-c: #22c55e; }
.aksrt-stats__icon { font-size: 20px; color: var(--stat-c, #3b82f6); margin-bottom: 8px; }
.aksrt-stats__value { font-size: 28px; font-weight: 800; color: var(--stat-c, #3b82f6); line-height: 1.1; }
.aksrt-stats__prefix, .aksrt-stats__unit { font-size: .55em; font-weight: 600; }
.aksrt-stats__label { margin-top: 6px; font-size: 12px; color: var(--muted-2-color, #888); }
/* ---- Forum topic lists ---- */
.aksrt-ftopic-list { list-style: none; margin: 0; padding: 0; }
.aksrt-ftopic-list__item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 0;
border-bottom: 1px dashed var(--color-line, #e5e7eb);
}
.aksrt-ftopic-list__item:last-child { border-bottom: 0; }
.aksrt-ftopic-list__link {
flex: 1;
min-width: 0;
color: inherit;
text-decoration: none;
font-size: 13px;
line-height: 1.45;
}
.aksrt-ftopic-list__views {
flex-shrink: 0;
font-size: 11px;
color: var(--muted-2-color, #999);
}
.aksrt-ftopic-list.is-detail .aksrt-ftopic-list__item { flex-direction: column; gap: 4px; }
.aksrt-ftopic-list__meta {
display: flex;
gap: 10px;
font-size: 11px;
color: var(--muted-2-color, #999);
}
/* Forum widget sidebar shell */
.aksrt-forum-widget-zone { margin-top: 12px; }
.aksrt-forum-widget-zone .zib-widget { margin-bottom: 14px; }
+8
View File
@@ -63,6 +63,11 @@ function aksrt_register_widgets() {
register_widget('\AKSRT\Widgets\WidgetTextTitle');
register_widget('\AKSRT\Widgets\WidgetUserLists');
register_widget('\AKSRT\Widgets\WidgetPricingPlans');
register_widget('\AKSRT\Widgets\WidgetFaq');
register_widget('\AKSRT\Widgets\WidgetStats');
// Forum widgets
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
@@ -82,6 +87,9 @@ function aksrt_register_widgets() {
if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) {
register_widget('\AKSRT\Widgets\WidgetForumPlateModerator');
}
if (class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) {
register_widget('\AKSRT\Widgets\WidgetForumTopicLists');
}
// Shop product widgets (§6.9)
register_widget('\AKSRT\Widgets\WidgetProductList');
+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'] ?? '')),
];
}
}
+13
View File
@@ -436,6 +436,19 @@ $initial_content = $draft_data ? $draft_data['content'] : '';
</div>
</div>
<?php if (is_active_sidebar('bbs_new_posts_sidebar_top') || is_active_sidebar('bbs_new_posts_sidebar_bottom')) : ?>
<div class="aksrt-forum-widget-zone">
<?php
if (is_active_sidebar('bbs_new_posts_sidebar_top')) {
dynamic_sidebar('bbs_new_posts_sidebar_top');
}
if (is_active_sidebar('bbs_new_posts_sidebar_bottom')) {
dynamic_sidebar('bbs_new_posts_sidebar_bottom');
}
?>
</div>
<?php endif; ?>
</div><!-- /.aksrt-forum-compose__sidebar -->
</form>
</div>
+7 -1
View File
@@ -56,8 +56,9 @@ $max_pages = (int) ($view['pagination']['max_pages'] ?? 1);
?>
<main id="main-content" class="site-main content-shell content-shell--stream content-shell--forum" tabindex="-1">
<?php if (is_active_sidebar('bbs_plate_top_fluid')) { echo '<div class="aksrt-fluid-widget aksrt-fluid-widget--forum">'; dynamic_sidebar('bbs_plate_top_fluid'); echo '</div>'; } ?>
<div class="content-wrap container">
<div class="content-layout content-layout--no-sidebar">
<div class="content-layout content-layout--with-sidebar">
<div class="content-layout__primary">
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
@@ -318,6 +319,11 @@ $max_pages = (int) ($view['pagination']['max_pages'] ?? 1);
<?php endif; ?>
</div>
<?php if (is_active_sidebar('bbs_plate_sidebar')) : ?>
<aside class="content-layout__sidebar aksrt-forum-widget-zone">
<?php dynamic_sidebar('bbs_plate_sidebar'); ?>
</aside>
<?php endif; ?>
</div>
</div>
</main>
+5
View File
@@ -412,6 +412,11 @@ $comments_enabled = !empty($permissions['can_reply']);
</div>
</div>
</section>
<?php if (is_active_sidebar('bbs_posts_sidebar')) : ?>
<div class="aksrt-forum-widget-zone">
<?php dynamic_sidebar('bbs_posts_sidebar'); ?>
</div>
<?php endif; ?>
</aside>
<div class="aksrt-forum-report-modal" id="aksrt-report-modal" hidden>
+5
View File
@@ -348,6 +348,11 @@ if (empty($public_home_tabs)) {
</ul>
</div>
<?php endif; ?>
<?php if (is_active_sidebar('bbs_home_sidebar')) : ?>
<div class="aksrt-forum-widget-zone">
<?php dynamic_sidebar('bbs_home_sidebar'); ?>
</div>
<?php endif; ?>
</aside>
</div>
</div>