feat(widgets): category tabs lazy load + ranking extras

SSR first category panel; remaining tabs fetch grid HTML via
/widgets/category-tabs. User ranking adds zibll-like desc modes.
This commit is contained in:
2026-07-20 21:05:23 +08:00
parent d508a9aef8
commit 90d84af2f5
3 changed files with 175 additions and 21 deletions
+39
View File
@@ -39,6 +39,11 @@ class WidgetAjax {
'callback' => [$this, 'rest_paid_posts'],
'permission_callback' => '__return_true',
]);
register_rest_route('aksrt/v1', '/widgets/category-tabs', [
'methods' => 'GET',
'callback' => [$this, 'rest_category_tabs'],
'permission_callback' => '__return_true',
]);
}
public function rest_main_posts(\WP_REST_Request $request) {
@@ -116,4 +121,38 @@ class WidgetAjax {
'total' => (int) ($payload['total'] ?? 0),
], 200);
}
public function rest_category_tabs(\WP_REST_Request $request) {
$cat = max(0, (int) ($request->get_param('cat') ?? 0));
$count = max(1, min(12, (int) ($request->get_param('count') ?? 6)));
$layout = sanitize_key((string) ($request->get_param('layout') ?? 'card'));
$columns = max(1, min(4, (int) ($request->get_param('columns') ?? 3)));
$context = sanitize_key((string) ($request->get_param('context') ?? 'homepage'));
if ($cat <= 0) {
return new \WP_REST_Response(['success' => true, 'html' => ''], 200);
}
$post_types = function_exists('aksrt_home_widget_post_types') ? aksrt_home_widget_post_types() : ['post'];
$query = new \WP_Query([
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => $count,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'cat' => $cat,
]);
ob_start();
if (function_exists('aksrt_home_widget_render_post_grid')) {
aksrt_home_widget_render_post_grid($query, $layout, $columns, $context);
} elseif ($query->have_posts()) {
echo '<ul class="widget-tab-posts__list">';
while ($query->have_posts()) {
$query->the_post();
echo '<li><a href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a></li>';
}
echo '</ul>';
}
wp_reset_postdata();
$html = (string) ob_get_clean();
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
}
+81 -18
View File
@@ -38,7 +38,7 @@ class CategoryTabsWidget extends \WP_Widget {
$tabs = $this->resolve_tabs($instance);
echo $args['before_widget'];
echo '<section class="aksrt-home-widget aksrt-home-widget--tabs widget-tab-posts">';
echo '<section class="aksrt-home-widget aksrt-home-widget--tabs widget-tab-posts" data-aksrt-home-cat-tabs>';
if (empty($tabs)) {
echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无可用分类', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无可用分类', 'aksrt') . '</p>';
@@ -69,33 +69,96 @@ class CategoryTabsWidget extends \WP_Widget {
<?php
foreach ($tabs as $index => $tab) {
$query = new \WP_Query([
'post_type' => aksrt_home_widget_post_types(),
'post_status' => 'publish',
'posts_per_page' => $count,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'cat' => (int) $tab['id'],
]);
?>
<div class="widget-tab-posts__panel<?php echo 0 === $index ? ' active' : ''; ?>" data-tab-panel="cat-<?php echo (int) $tab['id']; ?>" role="tabpanel"<?php echo 0 === $index ? '' : ' hidden'; ?>>
<?php aksrt_home_widget_render_post_grid($query, $layout, $columns, $context); ?>
<?php if ($show_more) : ?>
<div class="aksrt-home-widget__footer">
<a class="aksrt-home-widget__more" href="<?php echo esc_url($tab['link']); ?>">
<?php esc_html_e('查看更多', 'aksrt'); ?>
</a>
</div>
<div class="widget-tab-posts__panel<?php echo 0 === $index ? ' active' : ''; ?>"
data-tab-panel="cat-<?php echo (int) $tab['id']; ?>"
data-loaded="<?php echo 0 === $index ? '1' : '0'; ?>"
data-cat="<?php echo (int) $tab['id']; ?>"
data-more="<?php echo esc_url($tab['link']); ?>"
role="tabpanel"<?php echo 0 === $index ? '' : ' hidden'; ?>>
<?php if (0 === $index) :
$query = new \WP_Query([
'post_type' => aksrt_home_widget_post_types(),
'post_status' => 'publish',
'posts_per_page' => $count,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'cat' => (int) $tab['id'],
]);
aksrt_home_widget_render_post_grid($query, $layout, $columns, $context);
wp_reset_postdata();
if ($show_more) : ?>
<div class="aksrt-home-widget__footer">
<a class="aksrt-home-widget__more" href="<?php echo esc_url($tab['link']); ?>">
<?php esc_html_e('查看更多', 'aksrt'); ?>
</a>
</div>
<?php endif;
else : ?>
<div class="post_ajax_loader" data-aksrt-ajax-loader></div>
<?php endif; ?>
</div>
<?php
}
$rest = rest_url('aksrt/v1/widgets/category-tabs');
?>
<script>
(function(){
var section=document.currentScript?document.currentScript.parentElement:null;
if(!section||section._aksrtCatTabs)return; section._aksrtCatTabs=1;
var rest=<?php echo wp_json_encode($rest); ?>;
var count=<?php echo (int) $count; ?>;
var layout=<?php echo wp_json_encode($layout); ?>;
var columns=<?php echo (int) $columns; ?>;
var context=<?php echo wp_json_encode($context); ?>;
var showMore=<?php echo $show_more ? 'true' : 'false'; ?>;
var tabs=[].slice.call(section.querySelectorAll('.widget-tab-posts__tab'));
var panels=[].slice.call(section.querySelectorAll('.widget-tab-posts__panel'));
function load(panel){
if(!panel||panel.getAttribute('data-loaded')==='1')return;
var cat=panel.getAttribute('data-cat')||'0';
var more=panel.getAttribute('data-more')||'';
var qs=new URLSearchParams({cat:cat,count:String(count),layout:layout,columns:String(columns),context:context});
var url=rest+(rest.indexOf('?')>-1?'&':'?')+qs.toString();
panel.classList.add('is-ajax-loading');
var headers={Accept:'application/json'};
if(window.aksrtData&&aksrtData.restNonce)headers['X-WP-Nonce']=aksrtData.restNonce;
fetch(url,{credentials:'same-origin',headers:headers}).then(function(r){return r.json()}).then(function(j){
var d=(j&&j.data)||j||{};
var html=d.html||'';
if(showMore&&more){
html+='<div class="aksrt-home-widget__footer"><a class="aksrt-home-widget__more" href="'+more+'">查看更多</a></div>';
}
panel.innerHTML=html||'<p class="widget-empty">暂无文章</p>';
panel.setAttribute('data-loaded','1');
panel.classList.remove('is-ajax-loading');
}).catch(function(){panel.classList.remove('is-ajax-loading');});
}
tabs.forEach(function(tab){
tab.addEventListener('click',function(){
var target=tab.getAttribute('data-tab');
tabs.forEach(function(item){
var active=item===tab;
item.classList.toggle('active',active);
item.setAttribute('aria-selected',active?'true':'false');
});
panels.forEach(function(panel){
var active=panel.getAttribute('data-tab-panel')===target;
panel.classList.toggle('active',active);
if(active){panel.removeAttribute('hidden');load(panel);} else {panel.setAttribute('hidden','');}
});
});
});
})();
</script>
<?php
echo '</section>';
echo $args['after_widget'];
}
public function form($instance): void {
public function form($instance): void {
$title = (string) ($instance['title'] ?? '');
$category_ids = aksrt_home_widget_normalize_category_ids($instance['category_ids'] ?? [], 6);
$count = (int) ($instance['count'] ?? 6);
+55 -3
View File
@@ -23,7 +23,8 @@ class WidgetUserRanking extends \WP_Widget {
$count = max(1, min(20, (int) ($instance['count'] ?? 5)));
$show_avatar = !empty($instance['show_avatar']);
$users = $this->get_ranking($rank_by, $count);
$desc_mode = sanitize_key((string) ($instance['desc'] ?? 'desc'));
$users = $this->get_ranking($rank_by, $count, $desc_mode);
if (empty($users)) return;
echo $args['before_widget'];
@@ -60,8 +61,13 @@ class WidgetUserRanking extends \WP_Widget {
echo '</a>';
}
// 昵称
// 昵称 + 可选签名/附加信息
echo '<div class="aksrt-user-ranking__meta">';
echo '<a href="' . esc_url($user_url) . '" class="aksrt-user-ranking__name">' . esc_html($user['name']) . '</a>';
if (!empty($user['desc'])) {
echo '<div class="aksrt-user-ranking__desc em09 muted-2-color">' . esc_html((string) $user['desc']) . '</div>';
}
echo '</div>';
// 数值
echo '<span class="aksrt-user-ranking__value">' . esc_html($user['value']) . '</span>';
@@ -73,7 +79,7 @@ class WidgetUserRanking extends \WP_Widget {
echo $args['after_widget'];
}
private function get_ranking(string $rank_by, int $count): array {
private function get_ranking(string $rank_by, int $count, string $desc_mode = 'desc'): array {
$users = [];
switch ($rank_by) {
@@ -93,6 +99,7 @@ class WidgetUserRanking extends \WP_Widget {
'id' => (int) $row['user_id'],
'name' => $user->display_name,
'value' => number_format_i18n((int) $row['credits']) . ' 积分',
'desc' => $this->user_extra_desc((int) $row['user_id'], $user, $desc_mode),
];
}
}
@@ -112,6 +119,7 @@ class WidgetUserRanking extends \WP_Widget {
'id' => $user->ID,
'name' => $user->display_name,
'value' => number_format_i18n($post_count) . ' 篇',
'desc' => $this->user_extra_desc($user->ID, $user, $desc_mode),
];
}
}
@@ -137,6 +145,7 @@ class WidgetUserRanking extends \WP_Widget {
'id' => $uid,
'name' => $user->display_name,
'value' => number_format_i18n((int) $row['val']) . ' 天',
'desc' => $this->user_extra_desc($uid, $user, $desc_mode),
];
}
if (count($users) >= $count) break;
@@ -163,6 +172,7 @@ class WidgetUserRanking extends \WP_Widget {
'id' => $uid,
'name' => $user->display_name,
'value' => number_format_i18n((int) $row['val']) . ' 天',
'desc' => $this->user_extra_desc($uid, $user, $desc_mode),
];
}
if (count($users) >= $count) break;
@@ -189,6 +199,7 @@ class WidgetUserRanking extends \WP_Widget {
'id' => (int) $row['following_id'],
'name' => $user->display_name,
'value' => number_format_i18n((int) $row['cnt']) . ' 粉丝',
'desc' => $this->user_extra_desc((int) $row['following_id'], $user, $desc_mode),
];
}
}
@@ -200,6 +211,37 @@ class WidgetUserRanking extends \WP_Widget {
return $users;
}
private function user_extra_desc(int $user_id, $user, string $mode): string {
switch ($mode) {
case 'user_registered':
if ($user && !empty($user->user_registered)) {
$days = max(0, (int) floor((time() - strtotime($user->user_registered)) / DAY_IN_SECONDS));
return sprintf(__('加入 %d 天', 'aksrt'), $days);
}
return '';
case 'last_login':
$t = (string) get_user_meta($user_id, 'aksrt_last_login', true);
if ($t === '') {
$t = (string) get_user_meta($user_id, 'last_login', true);
}
return $t !== '' ? sprintf(__('最近登录 %s', 'aksrt'), $t) : '';
case 'last_checkin':
$t = (string) get_user_meta($user_id, 'aksrt_last_checkin', true);
if ($t === '') {
$t = (string) get_user_meta($user_id, 'last_checkin', true);
}
return $t !== '' ? sprintf(__('最近签到 %s', 'aksrt'), $t) : '';
case 'desc':
default:
$bio = get_user_meta($user_id, 'description', true);
if (!is_string($bio) || $bio === '') {
$bio = $user ? (string) $user->description : '';
}
return $bio !== '' ? wp_trim_words($bio, 12) : '';
}
}
public function form($instance): void {
$title = !empty($instance['title']) ? $instance['title'] : __('排行榜', 'aksrt');
$rank_by = !empty($instance['rank_by']) ? $instance['rank_by'] : 'credits';
@@ -230,6 +272,15 @@ class WidgetUserRanking extends \WP_Widget {
<?php esc_html_e('显示头像', 'aksrt'); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('desc')); ?>"><?php esc_html_e('附加显示:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('desc')); ?>" name="<?php echo esc_attr($this->get_field_name('desc')); ?>">
<option value="desc" <?php selected(($instance['desc'] ?? 'desc'), 'desc'); ?>><?php esc_html_e('用户签名', 'aksrt'); ?></option>
<option value="user_registered" <?php selected(($instance['desc'] ?? ''), 'user_registered'); ?>><?php esc_html_e('加入天数', 'aksrt'); ?></option>
<option value="last_login" <?php selected(($instance['desc'] ?? ''), 'last_login'); ?>><?php esc_html_e('最后登录', 'aksrt'); ?></option>
<option value="last_checkin" <?php selected(($instance['desc'] ?? ''), 'last_checkin'); ?>><?php esc_html_e('最后签到', 'aksrt'); ?></option>
</select>
</p>
<p>
<label>
<input type="checkbox" name="<?php echo esc_attr($this->get_field_name('checkin_btn')); ?>" value="1" <?php checked(!empty($instance['checkin_btn'])); ?>>
@@ -246,6 +297,7 @@ class WidgetUserRanking extends \WP_Widget {
$instance['count'] = max(1, min(20, (int) ($new_instance['count'] ?? 5)));
$instance['show_avatar'] = !empty($new_instance['show_avatar']);
$instance['checkin_btn'] = !empty($new_instance['checkin_btn']);
$instance['desc'] = in_array($new_instance['desc'] ?? '', ['desc', 'user_registered', 'last_login', 'last_checkin'], true) ? $new_instance['desc'] : 'desc';
return $instance;
}
}