From 53d8ded7f6dd63059bad02c84789d62541b02f88 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 8 Jul 2026 14:43:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(usercenter):=20=E5=88=B7=E6=96=B0=20/user/?= =?UTF-8?q?=20=E4=B8=8D=E5=86=8D=E6=95=B4=E9=A1=B5=E9=87=8D=E7=BB=98(?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=E6=9C=9F=E4=B9=90=E8=A7=82=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=A4=96=E5=A3=B3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把首帧从登录页改成居中 Loading,消除了闪登录页,但用户仍感刷两次: 静态导出首帧无法知登录态 → 先渲染裸 Loading(无顶栏/导航)→ 再出现完整外壳 → 再拉 /me 填内容。顶栏在裸 Loading 到外壳间消失再出现,观感=整页重绘两次。 改:抽出可复用 appShell(content);mount 后引导仍进行(!ready)时,若本地有 refresh token,乐观渲染完整外壳(顶栏/导航就位,仅内容区转圈),没 token 才直接登录页。 于是顶栏/导航从乐观态到已登录态始终在位,只有内容区替换 → 不再整页重绘。 SSR 安全:!mounted(SSR + 首次客户端渲染)仍渲染中性 spinner(不读 localStorage), 与服务端 HTML 一致防水合不一致;mount 后才据 hasRefresh() 决定外壳/登录页。 Co-Authored-By: Claude Opus 4.8 --- web/usercenter/components/UserCenter.tsx | 39 +++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/web/usercenter/components/UserCenter.tsx b/web/usercenter/components/UserCenter.tsx index ba59124..86c7bf7 100644 --- a/web/usercenter/components/UserCenter.tsx +++ b/web/usercenter/components/UserCenter.tsx @@ -45,11 +45,16 @@ export default function UserCenter() { const [authed, setAuthed] = useState(false); const [ready, setReady] = useState(false); + const [mounted, setMounted] = useState(false); const [me, setMe] = useState(null); const [view, setView] = useState('overview'); const [dir, setDir] = useState(0); const touchRef = useRef({ x: 0, y: 0 }); + // SSR/首帧标记:服务端与客户端首次渲染都读不到 localStorage,必须渲染同一份中性 + // 首帧(避免水合不一致)。mount 后再据 hasRefresh() 乐观渲染外壳。 + useEffect(() => setMounted(true), []); + const loadMe = useCallback(() => { api.getMe().then(setMe).catch(() => {}); }, [api]); @@ -124,22 +129,6 @@ export default function UserCenter() { setView('overview'); } - // 引导(会话续期)未完成前一律显示加载态,不渲染登录页。关键:静态导出下 SSR 首屏 - // 读不到 localStorage(hasRefresh 服务端恒 false),若此时渲染登录页 → 刷新 /user/ 会 - // "先闪登录页再水合跳回面板"。且在 render 期调 hasRefresh() 会造成 SSR/客户端不一致。 - // 故引导期(!ready)只显示加载态(不读 localStorage),引导完再定:已登录→面板、 - // 未登录/续期失败→登录页。 - if (!ready) { - return ( -
- {t('loading')} -
- ); - } - if (!authed) { - return ; - } - const nav: [View, string, string][] = [ ['overview', 'layout-dashboard', t('navOverview')], ['sub', 'link', t('navSub')], @@ -175,7 +164,10 @@ export default function UserCenter() { )); - return ( + // 应用外壳(顶栏 + 导航 + 内容区);content 由调用方决定 —— 已就绪传真实视图, + // 引导期传加载占位。关键:外壳在「引导中(乐观)」与「已登录」两态都渲染,导航/顶栏 + // 始终在位,只有内容区替换 → 刷新时不再"整页重绘",消除"刷两次"的观感。 + const appShell = (content: React.ReactNode) => (
{/* top bar */}
@@ -207,9 +199,20 @@ export default function UserCenter() { style={{ maxWidth: 1000, margin: '0 auto', padding: mobile ? '20px 16px 40px' : '30px 24px 48px' }} >
- {main} + {content}
); + + const spinner =
{t('loading')}
; + + // ① SSR + 首次客户端渲染:读不到 localStorage,渲染中性首帧(与服务端 HTML 一致,防 + // 水合不一致)。② mount 后引导仍在进行(!ready):有 refresh token → 乐观渲染完整外壳 + // (只在内容区转圈),没 token → 直接登录页(均无闪现)。③ 引导完成:未登录→登录页、 + // 已登录→外壳 + 真实视图。外壳在 ②③ 始终在位 → 刷新不再整页重绘。 + if (!mounted) return spinner; + if (!ready) return hasRefresh() ? appShell(spinner) : ; + if (!authed) return ; + return appShell(main); }