'use client'; // UserCenter.tsx — 应用外壳:顶栏 + 导航 + 视图切换 + 会话编排。承袭 ucapp.jsx UserCenterApp。 import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Icon, Mark } from './icons'; import { LangSeg } from './shared'; import Login from './Login'; import Overview from './Overview'; import Subscription from './Subscription'; import Redeem from './Redeem'; import Invite from './Invite'; import Settings from './Settings'; import { useUI } from '../lib/theme'; import { makeT } from '../lib/i18n'; import { apiMode, getClient } from '../lib/api/client'; import { hasRefresh } from '../lib/api/session'; import type { Me } from '../lib/api/types'; type View = 'overview' | 'sub' | 'redeem' | 'invite' | 'settings'; const ORDER: View[] = ['overview', 'sub', 'redeem', 'invite', 'settings']; function useIsMobile() { const [m, setM] = useState(false); useEffect(() => { const f = () => setM(window.innerWidth <= 700); f(); window.addEventListener('resize', f); return () => window.removeEventListener('resize', f); }, []); return m; } export default function UserCenter() { const { lang, setLang, theme, toggleTheme } = useUI(); const t = makeT(lang); const api = getClient(); const mobile = useIsMobile(); const [authed, setAuthed] = useState(false); const [ready, setReady] = useState(false); const [me, setMe] = useState(null); const [view, setView] = useState('overview'); const [dir, setDir] = useState(0); const touchRef = useRef({ x: 0, y: 0 }); const loadMe = useCallback(() => { api.getMe().then(setMe).catch(() => {}); }, [api]); // 启动会话:mock 直接进面板;http 有 refresh 则静默续期,否则去登录 useEffect(() => { let alive = true; (async () => { if (apiMode() === 'mock') { if (alive) setAuthed(true); } else if (hasRefresh()) { try { await api.refresh(); if (alive) setAuthed(true); } catch { if (alive) setAuthed(false); } } if (alive) setReady(true); })(); return () => { alive = false; }; }, [api]); useEffect(() => { if (authed) loadMe(); else setMe(null); }, [authed, loadMe]); function go(id: View) { const a = ORDER.indexOf(view); const b = ORDER.indexOf(id); setDir(b > a ? 1 : b < a ? -1 : 0); setView(id); setTimeout(() => setDir(0), 300); } function onTouchStart(e: React.TouchEvent) { const p = e.touches[0]; touchRef.current = { x: p.clientX, y: p.clientY }; } function onTouchEnd(e: React.TouchEvent) { const p = e.changedTouches[0]; const dx = p.clientX - touchRef.current.x; const dy = p.clientY - touchRef.current.y; if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) { const i = ORDER.indexOf(view); const ni = dx < 0 ? Math.min(ORDER.length - 1, i + 1) : Math.max(0, i - 1); if (ni !== i) go(ORDER[ni]); } } async function signOut() { await api.logout().catch(() => {}); setAuthed(false); setView('overview'); } if (!ready) { return
; } if (!authed) { return { setAuthed(true); setView('overview'); }} />; } const nav: [View, string, string][] = [ ['overview', 'layout-dashboard', t('navOverview')], ['sub', 'link', t('navSub')], ['redeem', 'ticket', t('navRedeem')], ['invite', 'users', t('navInvite')], ['settings', 'settings', t('navSettings')], ]; let main: React.ReactNode = null; if (!me && (view === 'overview')) { main =
{t('loading')}
; } else if (view === 'overview' && me) { main = go('sub')} goRedeem={() => go('redeem')} />; } else if (view === 'sub') { main = ; } else if (view === 'redeem') { main = ; } else if (view === 'invite') { main = ; } else if (view === 'settings') { main = ; } const navBtns = nav.map(([id, ic, l]) => ( )); return (
{/* top bar */}
穿山甲
{!mobile && } {mobile &&
}
{mobile && }
{main}
); }