修复粉丝/关注 TAB 的用户列表渲染

This commit is contained in:
2026-08-23 14:12:42 +08:00
parent c6c2c0af6d
commit 84321801a6
7 changed files with 109 additions and 72 deletions
@@ -0,0 +1,42 @@
# 复刻 Zibll 粉丝/关注用户列表(公开主页 follow TAB)
## 问题根源
`templates/parts/user-home/panel-follow.php:46-65` 手写的列表 markup(`.uh-follow__list/item/avatar/name`)在全仓库没有任何 CSS 定义,头像+用户名以内联文本堆叠显示。列表项也只有头像和用户名,没有 Zibll 的卡片结构。
关键事实:AKSRT 已 vendored Zibll 的 `main.css`(`.author-minicard` 卡片规则、工具类齐全),且 `inc/helpers/aksrt-graphic-card.php:215` 已有 `aksrt_author_minicard()` —— Zibll `zib_author_card()` 的移植版(42px 懒加载头像、用户名+勋章/认证/等级图标、逐项关注按钮、个性签名一行省略、默认文案"这家伙很懒,什么都没有写…"),搜索页在用。**修复 = 粉丝/关注列表改走这条既有链路**,不新写卡片样式。
## 改动清单
### 1. `templates/parts/user-home/panel-follow.php`(核心)
- 列表循环改为 `aksrt_author_minicard((int) $row['user_id'])`,删除手写的 `.uh-follow__item/avatar/name` markup(渲染链路与搜索页用户列表完全一致)。
- 子切换复刻 Zibll 结构(`zib-author.php:407-410`):居中文字链接「粉丝 N | 关注 N」(`list-inline splitters` + 激活 `focus-color` / 非激活 `muted-color`),**顺序改为粉丝在前、默认子列表改为粉丝**(Zibll 逻辑:TAB 名"粉丝"、默认显示粉丝列表,`zib_get_follow_user_list` 默认 `type='followed'`)。链接仍走现有 `?follow_sub=&follow_page=` 路由。
- 空状态图标换心形 SVG(见第 6 条),继续用 `aksrt_render_profile_empty` 组件(对齐 Zibll null-love 爱心语义)。
- 分页保持 `paginate_links` 服务端翻页:Zibll 的 AJAX"加载更多"框架(post_ajax/ajax-next)未移植进 AKSRT(main.js 仅 27 行),且公开主页所有 TAB 统一为服务端渲染 + URL 参数翻页,不为此引入独立 AJAX 体系。
### 2. `inc/helpers/aksrt-graphic-card.php` — `aksrt_author_minicard()`
- 关注按钮改为**服务端渲染真实状态**(复刻 Zibll 逻辑):已登录且 `\AKSRT\User\Follow::is_following(current, target)` 为真时输出 `is-following` class +「已关注」文案。消除每次进页"全部显示关注→异步闪变"的问题。搜索页同样受益。
### 3. `templates/parts/user/author-hero.php`
- hero 关注按钮(L151-163)同样服务端渲染真实初始状态(`is-following` +「已关注」),与卡片按钮同一逻辑。
### 4. `assets/js/follow.js`
- 删除逐按钮 `/follow/status` 轮询(L54-69):服务端已输出真值,不留双路。保留点击 toggle、`updateFollowButton` 状态切换、用户中心取关移除行、计数更新。
### 5. `assets/css/routes/user-home.css` — follow 区样式替换
- `.uh-follow__list { text-align: center; }`:对齐 Zibll pane 的 `text-center`(卡片为 inline-block,自动 3 列居中换行,移动端单列规则 main.css 已有)。
- 子切换:删除旧 pill 按钮样式块(L486-519),替换为 Zibll 风格的少量布局规则(居中、间距),颜色/激活态复用 vendored 工具类。
- 同步删除不再被引用的 `__subtab-count` 等旧规则。
### 6. `inc/helpers/aksrt-user-home.php`
- context 数组新增 `svg_empty_follow`(心形 stroke SVG,风格同现有 `svg_empty_post` 等)。
## 不做的事(明确排除)
- 不引入 AJAX"加载更多"(无框架基础,破坏公开主页统一服务端分页架构)。
- 不动数据层 `src/User/Follow.php`(`get_user_list` 已提供所需 user_id;徽章/签名由 minicard 自行取)。
- 不写任何双路、兼容、迁移代码:旧的手写列表 markup 和 pill 子切换样式直接替换删除。
## 验证
- `php -l` 检查全部改动的 PHP 文件。
- `node --test tests/widget-system/*.test.cjs` 确认不破坏既有契约(无该面板专属测试)。
- 桌面 + 移动宽度下目视核对:卡片 3 列/单列、关注按钮初始状态、子切换、空状态、分页。
+21 -18
View File
@@ -482,40 +482,43 @@
}
}
/* User-home follow tab */
/* User-home follow tab — Zibll parity
* 子切换复刻 zibll inc/functions/zib-author.php:407-410(list-inline splitters
* 居中文字链接,激活 focus-color);列表项为 .author-minicard inline-block
* 三列卡片流(卡片规则、640px 单列均在 vendored main.css),容器居中对齐
* zibll pane 的 text-center。 */
.aksrt-user-home .uh-follow__subtabs {
display: flex;
justify-content: center;
gap: 4px;
margin-bottom: 16px;
margin: 10px 0 16px;
padding: 0;
padding-bottom: 12px;
border-bottom: 1px solid var(--c-border, #e8e8e8);
border-bottom: 1px solid var(--main-border-color, rgba(50, 50, 50, 0.06));
list-style: none;
}
.aksrt-user-home .uh-follow__subtab {
display: inline-flex;
align-items: center;
align-items: baseline;
gap: 4px;
padding: 6px 14px;
border-radius: var(--radius-pill, 999px);
font-size: 13px;
color: var(--c-text-3, #999);
padding: 4px 12px;
font-size: 14px;
text-decoration: none;
transition: all .2s;
transition: color .2s;
}
.aksrt-user-home .uh-follow__subtab:hover {
color: var(--c-text-1, #333);
background: var(--c-fill-1, #f5f5f5);
}
.aksrt-user-home .uh-follow__subtab.is-active {
background: var(--c-primary, #1890ff);
color: #fff;
color: var(--focus-color, var(--color-primary, #f04494));
}
.aksrt-user-home .uh-follow__subtab-count {
font-size: 11px;
opacity: .7;
font-size: 12px;
opacity: .8;
}
.aksrt-user-home .uh-follow__list {
text-align: center;
}
.aksrt-user-home .uh-follow__pagination {
+1 -17
View File
@@ -15,6 +15,7 @@
})();
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
// 初始关注状态由服务端渲染(is-following class + 文案),无需逐按钮查询。
function initFollowButtons() {
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-aksrt-follow]');
@@ -50,23 +51,6 @@
btn.disabled = false;
});
});
// Check initial follow status for all follow buttons
document.querySelectorAll('[data-aksrt-follow]').forEach(function (btn) {
var targetUserId = parseInt(btn.getAttribute('data-aksrt-follow'));
if (!targetUserId) return;
fetch(API_ROOT + '/follow/status?target_user_id=' + targetUserId, {
headers: { 'X-WP-Nonce': NONCE },
})
.then(function (res) { return res.json(); })
.then(function (result) {
if (result.success && result.data) {
updateFollowButton(btn, result.data.is_following, result.data.follower_count);
}
})
.catch(function () { /* silent */ });
});
}
function updateFollowButton(btn, isFollowing, count) {
+9 -1
View File
@@ -242,11 +242,19 @@ if (!function_exists('aksrt_author_minicard')) :
: '<a href="' . esc_url($home) . '">' . esc_html($user->display_name) . '</a>';
// Follow control (separate from name, legacy places both in dt).
// 服务端直接输出真实关注状态(legacy parity: zib_get_user_follow 恒渲染当前态)。
$follow = '';
$current = get_current_user_id();
if ($current !== $user_id) {
$is_following = false;
if (is_user_logged_in() && class_exists('\AKSRT\User\Follow')) {
$is_following = \AKSRT\User\Follow::get_instance()->is_following($current, $user_id);
}
$follow_label = $is_following ? __('已关注', 'aksrt') : __('关注', 'aksrt');
$follow_title = $is_following ? __('点击取消关注', 'aksrt') : __('关注该用户', 'aksrt');
$follow_state = $is_following ? ' is-following' : '';
if (is_user_logged_in()) {
$follow = '<button type="button" class="focus-color px12 ml10 follow flex0 but" data-aksrt-follow="' . (int) $user_id . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</button>';
$follow = '<button type="button" class="focus-color px12 ml10 follow flex0 but' . $follow_state . '" data-aksrt-follow="' . (int) $user_id . '" title="' . esc_attr($follow_title) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i> ' . esc_html($follow_label) . '</button>';
} else {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
$follow = '<a class="focus-color px12 ml10 follow flex0 but" href="' . esc_url($login) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</a>';
+1
View File
@@ -137,6 +137,7 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'svg_empty_post' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6M9 9h1"/></svg>',
'svg_empty_comment' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
'svg_empty_favorite' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'svg_empty_follow' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
];
}
}
+22 -28
View File
@@ -2,6 +2,10 @@
/**
* Public user home — follow tab panel (social following/followers).
*
* Legacy parity: 粉丝/关注用户列表复刻 zib_get_follow_user_list + zib_author_card
* (inc/functions/zib-author.php:395-454, 685-716):子切换「粉丝 | 关注」居中文字链接,
* 列表项走 aksrt_author_minicard(.author-minicard 三列 inline-block 卡片流)。
*
* @package AKSRT
*/
@@ -15,8 +19,8 @@ extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ext
<div id="panel-follow" class="dashboard-panel<?php echo $active_tab === 'follow' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'follow' ? ' hidden' : ''; ?>>
<?php
$follow_page = max(1, isset($_GET['follow_page']) ? (int) $_GET['follow_page'] : 1);
$follow_sub = isset($_GET['follow_sub']) ? sanitize_key(wp_unslash($_GET['follow_sub'])) : 'following';
$follow_sub = in_array($follow_sub, ['following', 'followers'], true) ? $follow_sub : 'following';
$follow_sub = isset($_GET['follow_sub']) ? sanitize_key(wp_unslash($_GET['follow_sub'])) : 'followers';
$follow_sub = in_array($follow_sub, ['following', 'followers'], true) ? $follow_sub : 'followers';
$follow_instance = class_exists('\AKSRT\User\Follow') ? \AKSRT\User\Follow::get_instance() : null;
$following_count = $follow_instance ? (int) $follow_instance->get_following_count((int) $user_id) : 0;
@@ -30,38 +34,28 @@ extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ext
$items = is_array($list['items'] ?? null) ? $list['items'] : [];
$total_pages = max(1, (int) ($list['total_pages'] ?? 1));
?>
<div class="uh-follow__subtabs">
<a class="uh-follow__subtab<?php echo $follow_sub === 'following' ? ' is-active' : ''; ?>"
href="<?php echo esc_url(add_query_arg(['tab' => 'follow', 'follow_sub' => 'following', 'follow_page' => 1], $home_url)); ?>">
<i class="fa fas fa-user-plus"></i> <?php esc_html_e('关注', 'aksrt'); ?>
<span class="uh-follow__subtab-count"><?php echo esc_html((string) $following_count); ?></span>
</a>
<a class="uh-follow__subtab<?php echo $follow_sub === 'followers' ? ' is-active' : ''; ?>"
<ul class="uh-follow__subtabs list-inline splitters">
<li>
<a class="uh-follow__subtab<?php echo $follow_sub === 'followers' ? ' is-active focus-color' : ' muted-color'; ?>"
href="<?php echo esc_url(add_query_arg(['tab' => 'follow', 'follow_sub' => 'followers', 'follow_page' => 1], $home_url)); ?>">
<i class="fa fas fa-users"></i> <?php esc_html_e('粉丝', 'aksrt'); ?>
<?php esc_html_e('粉丝', 'aksrt'); ?>
<span class="uh-follow__subtab-count"><?php echo esc_html((string) $follower_count); ?></span>
</a>
</div>
</li>
<li>
<a class="uh-follow__subtab<?php echo $follow_sub === 'following' ? ' is-active focus-color' : ' muted-color'; ?>"
href="<?php echo esc_url(add_query_arg(['tab' => 'follow', 'follow_sub' => 'following', 'follow_page' => 1], $home_url)); ?>">
<?php esc_html_e('关注', 'aksrt'); ?>
<span class="uh-follow__subtab-count"><?php echo esc_html((string) $following_count); ?></span>
</a>
</li>
</ul>
<?php if (!empty($items)) : ?>
<div class="uh-follow__list">
<?php foreach ($items as $row) :
$uid = (int) ($row['user_id'] ?? 0);
$name = (string) ($row['display_name'] ?? '');
$avatar = (string) ($row['avatar_url'] ?? '');
$profile = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($uid) : home_url('/');
?>
<a class="uh-follow__item" href="<?php echo esc_url($profile); ?>">
<span class="uh-follow__avatar">
<?php if ($avatar !== '') : ?>
<img<?php echo aksrt_get_lazy_attr('lazy_avatar', (string) $avatar, 'avatar', aksrt_get_lazy_thumb('sm')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> alt="" width="48" height="48">
<?php else : ?>
<?php echo get_avatar($uid, 48); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?>
</span>
<span class="uh-follow__name"><?php echo esc_html($name !== '' ? $name : sprintf(__('用户 #%d', 'aksrt'), $uid)); ?></span>
</a>
<?php endforeach; ?>
echo aksrt_author_minicard((int) ($row['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
endforeach; ?>
</div>
<?php if ($total_pages > 1) : ?>
<div class="uh-follow__pagination">
@@ -81,7 +75,7 @@ extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ext
$empty_msg = $follow_sub === 'followers'
? __('暂无粉丝', 'aksrt')
: __('暂未关注任何人', 'aksrt');
aksrt_render_profile_empty($svg_empty_post, $empty_msg);
aksrt_render_profile_empty($svg_empty_follow ?? $svg_empty_post ?? '', $empty_msg);
?>
<?php endif; ?>
</div>
+7 -2
View File
@@ -150,9 +150,14 @@ $messages_url = function_exists('aksrt_get_messages_page_url') ? aksrt_get_messa
<?php else : ?>
<?php if ($show_follow) : ?>
<?php if (is_user_logged_in()) : ?>
<button type="button" class="btn-outline btn-sm aksrt-author-hero__follow-btn" data-aksrt-follow="<?php echo (int) $user_id; ?>" title="<?php esc_attr_e('关注该用户', 'aksrt'); ?>">
<?php
$hero_following = class_exists('\AKSRT\User\Follow')
? \AKSRT\User\Follow::get_instance()->is_following(get_current_user_id(), (int) $user_id)
: false;
?>
<button type="button" class="btn-outline btn-sm aksrt-author-hero__follow-btn<?php echo $hero_following ? ' is-following' : ''; ?>" data-aksrt-follow="<?php echo (int) $user_id; ?>" title="<?php echo $hero_following ? esc_attr__('点击取消关注', 'aksrt') : esc_attr__('关注该用户', 'aksrt'); ?>">
<i class="fa fas fa-heart" aria-hidden="true"></i>
<span><?php esc_html_e('关注', 'aksrt'); ?></span>
<span><?php echo esc_html($hero_following ? __('已关注', 'aksrt') : __('关注', 'aksrt')); ?></span>
</button>
<?php else : ?>
<a href="<?php echo esc_url(aksrt_get_login_url($home_url)); ?>" class="btn-outline btn-sm aksrt-author-hero__follow-btn">