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); }