diff --git a/assets/css/routes/user-home.css b/assets/css/routes/user-home.css index 90a3d4a2..21d05160 100644 --- a/assets/css/routes/user-home.css +++ b/assets/css/routes/user-home.css @@ -516,6 +516,53 @@ text-align: center; } +.aksrt-user-home .dashboard-panel .uh-follow__list--placeholder { + display: none; +} + +.aksrt-user-home .dashboard-panel.is-loading .uh-follow__list--placeholder { + display: block; +} + +.aksrt-user-home .dashboard-panel.is-loading .uh-follow__subtabs, +.aksrt-user-home .dashboard-panel.is-loading .uh-follow__list:not(.uh-follow__list--placeholder), +.aksrt-user-home .dashboard-panel.is-loading .uh-pagination, +.aksrt-user-home .dashboard-panel.is-loading .profile-empty { + display: none; +} + +.aksrt-user-home .dashboard-panel.is-load-error .uh-follow__list--placeholder { + display: block; +} + +.aksrt-user-home .uh-panel-load-error { + display: none; + margin: 12px 0; + color: var(--color-ink-4); + font-size: 13px; + text-align: center; + grid-column: 1 / -1; +} + +.aksrt-user-home .dashboard-panel.is-load-error .uh-panel-load-error { + display: block; +} + +.aksrt-user-home .uh-panel-load-error button { + margin-left: 8px; + padding: 0; + border: 0; + color: var(--color-primary); + background: transparent; + font: inherit; + cursor: pointer; +} + +.aksrt-user-home .uh-follow__placeholder { + display: inline-flex; + margin: 5px 8px; +} + .aksrt-user-home .uh-follow__pagination { margin-top: 16px; text-align: center; @@ -608,6 +655,21 @@ animation: aksrtFadeIn 0.2s ease both; } +.aksrt-user-home .dashboard-panel.is-load-error .post_ajax_loader { + display: block; +} + +.aksrt-user-home .dashboard-panel.is-load-error .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; +} + +.aksrt-user-home .dashboard-panel.is-load-error .placeholder { + animation: none; +} + @media (prefers-reduced-motion: reduce) { .aksrt-user-home .dashboard-panel .post_ajax_loader .placeholder { animation: none; diff --git a/assets/js/author.js b/assets/js/author.js index 226a65e0..7ffdd6f5 100644 --- a/assets/js/author.js +++ b/assets/js/author.js @@ -97,6 +97,7 @@ trigger.setAttribute('aria-busy', 'true'); trigger.textContent = '加载中…'; } + panel.classList.remove('is-load-error'); panel.classList.add('is-loading'); var url = restBase.replace(/\/$/, '') + '/user-home/panel/' + userId + '/' + encodeURIComponent(name) @@ -117,10 +118,14 @@ } else { panel.innerHTML = payload.data.html; } + panel.dataset.panelLoaded = 'true'; if (window.jQuery) jQuery(document).trigger('post_ajax.ed', [panel]); if (typeof window.auto_fun === 'function') window.auto_fun(); }); }).catch(function (error) { + if (panel.dataset.panelLoaded === 'false') { + panel.classList.add('is-load-error'); + } if (appendComments && trigger && trigger.isConnected) { trigger.disabled = false; trigger.removeAttribute('aria-busy'); @@ -141,7 +146,12 @@ tabs.forEach(function (tab) { tab.addEventListener('click', function () { - switchTo(tab.getAttribute('data-tab')); + var name = tab.getAttribute('data-tab'); + switchTo(name); + var panel = panels[name] && panels[name].panel; + if (panel && panel.dataset.panelLoaded === 'false') { + loadPanelBody(panel, name, 1, 'followers').catch(notifyLoadError); + } }); }); @@ -161,6 +171,17 @@ return; } + var retryBtn = target.closest('[data-uh-retry]'); + if (retryBtn) { + event.preventDefault(); + var retryPanel = retryBtn.closest('.dashboard-panel'); + if (!retryPanel) return; + var retryName = panelNameOf(retryPanel); + if (!retryName) return; + loadPanelBody(retryPanel, retryName, 1, 'followers').catch(notifyLoadError); + return; + } + var commentsNext = target.closest('[data-uh-comments-next]'); if (commentsNext) { event.preventDefault(); diff --git a/inc/helpers/aksrt-user-home.php b/inc/helpers/aksrt-user-home.php index 709f1730..bb585179 100644 --- a/inc/helpers/aksrt-user-home.php +++ b/inc/helpers/aksrt-user-home.php @@ -190,6 +190,23 @@ if (!function_exists('aksrt_render_user_home_pagination')) { } } +if (!function_exists('aksrt_get_user_home_follow_placeholder')) { + /** + * Skeleton cards used while the public home follow panel is loading. + * Mirrors zib_get_author_tab_loader('follow'). + */ + function aksrt_get_user_home_follow_placeholder(): string { + $item = ''; + + return '
' + . str_repeat($item, 6) + . '
'; + } +} + if (!function_exists('aksrt_capture_user_home_panel')) { /** * 捕获公开用户主页面板 body(templates/parts/user-home/panel-{tab}.php)。 diff --git a/templates/pages/page-user-home.php b/templates/pages/page-user-home.php index e60365b5..7226d688 100644 --- a/templates/pages/page-user-home.php +++ b/templates/pages/page-user-home.php @@ -71,9 +71,14 @@ require_once AKSRT_DIR . '/templates/parts/user-shared-helpers.php'; if (!locate_template(['templates/parts/user-home/panel-' . $panel_tab . '.php'], false, false)) { continue; } + $defer_panel = in_array($panel_tab, ['favorites', 'follow'], true) && $panel_tab !== $active_tab; + $panel_context = $ctx; + if ($defer_panel) { + $panel_context['placeholder_only'] = true; + } ?> -
> - +
> +
diff --git a/templates/parts/user-home/panel-favorites.php b/templates/parts/user-home/panel-favorites.php index e18e86c4..2371cbc1 100644 --- a/templates/parts/user-home/panel-favorites.php +++ b/templates/parts/user-home/panel-favorites.php @@ -14,15 +14,34 @@ defined('ABSPATH') || exit; $args = is_array($args ?? null) ? $args : []; extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -if (!empty($favorite_post_ids)) : - $user_home_render_config = aksrt_resolve_post_list_render_config([ - 'context' => 'author', - 'overrides' => [ - 'no_author' => true, - 'no_margin' => true, - ], - ]); +$user_home_render_config = aksrt_resolve_post_list_render_config([ + 'context' => 'author', + 'overrides' => [ + 'no_author' => true, + 'no_margin' => true, + ], +]); +if (!empty($placeholder_only)) { + $placeholder_mode = (string) ($user_home_render_config['mode'] ?? 'list'); + $placeholder_html = function_exists('aksrt_render_placeholder') + ? aksrt_render_placeholder($placeholder_mode, 4) + : ''; + ?> +
+
+ +
+ +
+ 'post', 'post_status' => 'publish', diff --git a/templates/parts/user-home/panel-follow.php b/templates/parts/user-home/panel-follow.php index e8784ac1..cde2cce9 100644 --- a/templates/parts/user-home/panel-follow.php +++ b/templates/parts/user-home/panel-follow.php @@ -21,6 +21,17 @@ extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ext $follow_page = max(1, (int) ($follow_page ?? 1)); $follow_sub = in_array($follow_sub ?? 'followers', ['following', 'followers'], true) ? $follow_sub : 'followers'; +if (!empty($placeholder_only)) : + ?> + + + get_following_count((int) $user_id) : 0; $follower_count = $follow_instance ? (int) $follow_instance->get_follower_count((int) $user_id) : 0; @@ -63,3 +74,4 @@ $total_pages = max(1, (int) ($list['total_pages'] ?? 1)); aksrt_render_profile_empty($svg_empty_follow ?? $svg_empty_post ?? '', $empty_msg); ?> + diff --git a/tests/widget-system/post-visual-contract.test.cjs b/tests/widget-system/post-visual-contract.test.cjs index 92ba9e7b..7e6625e6 100644 --- a/tests/widget-system/post-visual-contract.test.cjs +++ b/tests/widget-system/post-visual-contract.test.cjs @@ -570,16 +570,24 @@ test('public user home stays param-free and swaps panels via REST fragments', () // Shell mounts panel wrappers from the tab registry with a user id hook. assert.match(shell, /data-user-id="<\?php echo \(int\) \$user_id; \?>">/); - assert.match(shell, /get_template_part\('templates\/parts\/user-home\/panel', \$panel_tab, \$ctx\)/); + assert.match(shell, /in_array\(\$panel_tab, \['favorites', 'follow'\], true\) && \$panel_tab !== \$active_tab/); + assert.match(shell, /data-panel-loaded="<\?php echo \$defer_panel \? 'false' : 'true'; \?>"/); + assert.match(shell, /get_template_part\('templates\/parts\/user-home\/panel', \$panel_tab, \$panel_context\)/); // Follow panel: button-driven sub-tabs, pagination without URL building. assert.match(follow, /data-uh-sub="followers"/); assert.match(follow, /data-uh-sub="following"/); assert.match(follow, /aksrt_render_user_home_pagination\(\$total_pages, \$follow_page\)/); + assert.match(ctx, /function aksrt_get_user_home_follow_placeholder\(\): string[\s\S]*?author-minicard radius8 flex ac uh-follow__placeholder[\s\S]*?placeholder k1 mb6[\s\S]*?placeholder s1/); + assert.match(ctx, /str_repeat\(\$item, 6\)/); + assert.match(follow, /aksrt_get_user_home_follow_placeholder\(\)/); + assert.match(follow, /data-uh-retry[\s\S]*?重试/); assert.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/); assert.match(comments, /data-uh-comments-next=/); assert.match(favorites, /aksrt_render_user_home_pagination\(/); + assert.match(favorites, /if \(!empty\(\$placeholder_only\)\)[\s\S]*?aksrt_render_placeholder\(\$placeholder_mode, 4\)/); + assert.match(favorites, /data-uh-retry[\s\S]*?重试/); assert.doesNotMatch(comments + favorites, /add_query_arg|\$_GET/); // REST fragment endpoint serves public panel bodies. @@ -593,6 +601,16 @@ test('public user home stays param-free and swaps panels via REST fragments', () assert.match(authorJs, /data-uh-sub/); assert.match(authorJs, /post_ajax\.ed/); assert.match(authorJs, /window\.auto_fun\(\)/); + assert.match(authorJs, /panel\.dataset\.panelLoaded === 'false'[\s\S]*?loadPanelBody\(panel, name, 1, 'followers'\)/); + assert.match(authorJs, /target\.closest\('\[data-uh-retry\]'\)[\s\S]*?loadPanelBody\(retryPanel, retryName, 1, 'followers'\)/); + assert.match(authorJs, /panel\.dataset\.panelLoaded = 'true'/); + assert.match(authorJs, /panel\.classList\.add\('is-load-error'\)/); + const userHomeCss = read('assets/css/routes/user-home.css'); + assert.match(userHomeCss, /dashboard-panel\.is-loading \.uh-follow__list--placeholder\s*\{\s*display: block;/); + assert.match(userHomeCss, /dashboard-panel\.is-loading \.uh-follow__subtabs,[\s\S]*?dashboard-panel\.is-loading \.uh-follow__list:not\(\.uh-follow__list--placeholder\),[\s\S]*?dashboard-panel\.is-loading \.uh-pagination,[\s\S]*?dashboard-panel\.is-loading \.profile-empty\s*\{\s*display: none;/); + assert.match(userHomeCss, /dashboard-panel\.is-load-error \.uh-follow__list--placeholder\s*\{\s*display: block;/); + assert.match(userHomeCss, /dashboard-panel\.is-load-error \.post_ajax_loader\s*\{\s*display: block;/); + assert.match(userHomeCss, /dashboard-panel\.is-load-error \.uh-panel-load-error\s*\{\s*display: block;/); // 翻页拿子分组只读**当前面板内**的 is-active 子页签,不读 panel.dataset: // uhSub 是粉丝面板私有状态,写满所有面板会互相串味 —— 先点过粉丝子页签,