<!-- @dsCard group="UI Kit — Mobile" name="UI Kit · 移动 App" subtitle="iOS 连接界面 · 节点 · 账户 · 中英切换 · 双主题" viewport="390x760" -->
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>穿山甲 · 移动 App</title>
<link rel="stylesheet" href="../../colors_and_type.css">
<style>
  html, body { margin: 0; height: 100%; }
  body { background: var(--sand-100); display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 32px 0; box-sizing: border-box; font-family: var(--font-sans); }
  [data-theme="dark"] ~ * , body:has(#root [data-theme="dark"]) { }
  @keyframes pg-spin { to { transform: rotate(360deg); } }
  @keyframes pg-in-l { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); } }
  @keyframes pg-in-r { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: translateX(0); } }
  * { -webkit-font-smoothing: antialiased; }
  button { font-family: inherit; }
</style>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="ios-frame.jsx?v8"></script>
<script type="text/babel" src="parts.jsx?v8"></script>
<script type="text/babel" src="screens.jsx?v8"></script>
<script type="text/babel" src="flows.jsx?v8"></script>
<script type="text/babel">
const { useState, useEffect, useRef } = React;

function App() {
  const [stage, setStage] = useState('app');        // app | auth | onboarding — UI kit opens on the main app
  const [tab, setTab] = useState('connect');
  const [status, setStatus] = useState('off');     // off | connecting | on
  const [code, setCode] = useState('AUTO');         // 'AUTO' = 智能选择(默认) | 节点 code
  const [dark, setDark] = useState(false);
  const [lang, setLang] = useState('zh');           // zh | en
  const [free, setFree] = useState(true);           // 演示:免费版视角(额度 UI)
  const [mins, setMins] = useState(6);              // 今日剩余分钟(演示值,每日 10)
  const [adDone, setAdDone] = useState(false);      // 今日是否已看广告解锁
  const [elapsed, setElapsed] = useState(0);
  const [q, setQ] = useState('');
  const [dir, setDir] = useState(0);                // 滑动方向:1 左滑进 / -1 右滑进 / 0 无动画
  const timer = useRef(null);
  const touchRef = useRef({ x: 0, y: 0 });
  const t = makeT(lang);
  const SWIPE_ORDER = ['connect', 'servers', 'stats', 'me'];

  // 统一导航:主 Tab 间带方向动画,子页(兑换/套餐/联系)无动画
  function goTab(id) {
    const a = SWIPE_ORDER.indexOf(tab), b = SWIPE_ORDER.indexOf(id);
    setDir(a >= 0 && b >= 0 ? (b > a ? 1 : b < a ? -1 : 0) : 0);
    setTab(id);
    setTimeout(() => setDir(0), 300);   // 动画结束后清除(隐藏环境下 animationend 不可靠)
  }
  function onSwipeStart(e) { const p = e.touches[0]; touchRef.current = { x: p.clientX, y: p.clientY }; }
  function onSwipeEnd(e) {
    if (!SWIPE_ORDER.includes(tab)) return;          // 子页不响应滑动
    const p = e.changedTouches[0];
    const dx = p.clientX - touchRef.current.x, dy = p.clientY - touchRef.current.y;
    if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) {
      const i = SWIPE_ORDER.indexOf(tab);
      const ni = dx < 0 ? Math.min(SWIPE_ORDER.length - 1, i + 1) : Math.max(0, i - 1);
      if (ni !== i) goTab(SWIPE_ORDER[ni]);
    }
  }

  const smart = code === 'AUTO';
  const server = smart ? SERVERS.reduce((a, b) => (a.ping < b.ping ? a : b)) : SERVERS.find(s => s.code === code);

  function toggle() {
    if (status === 'off') {
      setStatus('connecting');
      setTimeout(() => { setStatus('on'); setElapsed(0); }, 1500);
    } else {
      setStatus('off');
    }
  }
  // pick a server: if connected, brief reconnect
  function pick(c) {
    setCode(c);
    setTab('connect');
    if (status === 'on') { setStatus('connecting'); setTimeout(() => { setStatus('on'); setElapsed(0); }, 1200); }
  }

  useEffect(() => {
    if (status === 'on') { timer.current = setInterval(() => setElapsed(e => e + 1), 1000); }
    return () => clearInterval(timer.current);
  }, [status]);

  let body;
  if (stage === 'auth') {
    body = <AuthFlow t={t} lang={lang} onDone={() => setStage('onboarding')} />;
  } else if (stage === 'onboarding') {
    body = <Onboarding t={t} lang={lang} onDone={() => setStage('app')} />;
  } else {
    let screen;
    if (tab === 'connect') screen = <ConnectScreen t={t} lang={lang} status={status} onToggle={toggle} server={server} smart={smart} free={free} mins={mins} adDone={adDone} onAd={() => setAdDone(true)} goServers={() => goTab('servers')} elapsed={elapsed} />;
    else if (tab === 'servers') screen = <ServersScreen t={t} lang={lang} current={code} onPick={pick} q={q} setQ={setQ} />;
    else if (tab === 'stats') screen = <StatsScreen t={t} lang={lang} />;
    else if (tab === 'redeem') screen = <RedeemScreen t={t} lang={lang} onBack={() => goTab('plans')} />;
    else if (tab === 'plans') screen = <PlansScreen t={t} lang={lang} onBack={() => goTab('me')} onChoose={() => goTab('redeem')} />;
    else if (tab === 'contact') screen = <ContactScreen t={t} lang={lang} onBack={() => goTab('me')} />;
    else screen = <SettingsScreen t={t} lang={lang} setLang={setLang} dark={dark} setDark={setDark} free={free} setFree={setFree} onRedeem={() => goTab('redeem')} onUpgrade={() => goTab('plans')} onContact={() => goTab('contact')} onSignOut={() => setStage('auth')} onReplayAuth={() => setStage('auth')} onReplayOnboarding={() => setStage('onboarding')} />;
    body = (<>
      <div onTouchStart={onSwipeStart} onTouchEnd={onSwipeEnd} style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
        <div key={tab} onAnimationEnd={() => setDir(0)} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', animation: dir !== 0 ? `pg-in-${dir === 1 ? 'l' : 'r'} 200ms var(--ease-out)` : 'none' }}>
          {screen}
        </div>
      </div>
      <BottomTab t={t} tab={(tab === 'redeem' || tab === 'plans' || tab === 'contact') ? 'me' : tab} setTab={goTab} />
    </>);
  }

  return (
    <IOSDevice dark={dark}>
      <div data-theme={dark ? 'dark' : 'light'} style={{ position: 'absolute', inset: 0, background: 'var(--bg)', display: 'flex', flexDirection: 'column', paddingTop: 56 }}>
        {body}
      </div>
    </IOSDevice>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
