Files
pangolin/design/ui_kits/mobile/screens.jsx
T
wangjia b98ad9dce4 chore(design): stats-overhaul 设计 WIP — CONTRACT + ui_kits 统计页规格
stats-overhaul 的设计稿(CONTRACT.md + mobile/tablet/desktop ui_kits)。

注:这批 ui_kits 描述的是统计页「上聚合·下分设备」原方案;统计页后续已
重设计为「月/周/日 周期卡 + 设备下拉 + 两周折线」(见 client/lib/screens/
stats_page.dart)。设计稿与新页布局有出入,待更新到新布局(单列 TODO 跟踪)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:13:49 +08:00

376 lines
27 KiB
React

/* screens.jsx — 穿山甲 VPN mobile · screens + bottom tab (i18n) */
const { useState, useEffect, useRef } = React;
/* ───────── Top brand bar ───────── */
function TopBar({ t, lang, right }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 20px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
<Mark size={28} />
<span style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 18, color: 'var(--fg1)', letterSpacing: '-0.01em' }}>{t('brand')}</span>
</div>
{right}
</div>
);
}
/* ───────── CONNECT screen ───────── */
function ConnectScreen({ t, lang, status, onToggle, server, smart, free, mins, adDone, onAd, goServers, elapsed }) {
const map = {
off: { fill: 'var(--sand-100)', fg: 'var(--clay-500)', icon: 'power', cap: t('capOff'), ring: 'track' },
connecting: { fill: 'var(--accent)', fg: '#fff', icon: 'loader-circle', cap: t('capConnecting'), ring: 'spin' },
on: { fill: 'var(--success)', fg: '#fff', icon: 'shield-check', cap: t('capOn'), ring: 'full' },
}[status];
const fmt = (s) => `${String(Math.floor(s/3600)).padStart(2,'0')}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
const shadow = status==='off'
? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)'
: `0 0 0 9px ${status==='on' ? 'var(--success-subtle)' : 'var(--accent-subtle)'}, var(--shadow-lg)`;
// in-circle center block
const center = status==='off'
? (<><span style={{ fontFamily:'var(--font-sans)', fontWeight:700, fontSize:14 }}>{t('connectNow')}</span></>)
: status==='connecting'
? (<span style={{ fontFamily:'var(--font-mono)', fontWeight:700, fontSize:14, letterSpacing:'0.08em' }}>···</span>)
: (<><span style={{ fontFamily:'var(--font-mono)', fontWeight:600, fontSize:18 }}>{fmt(elapsed)}</span><span style={{ fontFamily:'var(--font-sans)', fontWeight:700, fontSize:10, letterSpacing:'0.1em', opacity:.9 }}>{t('secure')}</span></>);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<TopBar t={t} lang={lang} right={<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, whiteSpace: 'nowrap', color: status==='on'?'var(--success)':'var(--fg3)', fontWeight: 600 }}>{status==='on'?t('online'):t('offline')}</span>} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 26, padding: '0 24px' }}>
<button onClick={onToggle} style={{
position: 'relative', width: 208, height: 208, borderRadius: '50%', border: 'none', cursor: 'pointer',
background: map.fill, color: map.fg, boxShadow: shadow,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
transition: 'box-shadow 300ms var(--ease-out), background-color 300ms var(--ease-out)',
}}>
{/* track ring */}
<svg viewBox="0 0 208 208" style={{ position:'absolute', inset:0, width:'100%', height:'100%', transformOrigin:'center', animation: map.ring==='spin' ? 'pg-spin 1.4s linear infinite' : 'none' }}>
{map.ring==='track' && <circle cx="104" cy="104" r="96" fill="none" stroke="var(--sand-200)" strokeWidth="3" strokeDasharray="2 8" strokeLinecap="round"/>}
{map.ring==='spin' && <><circle cx="104" cy="104" r="96" fill="none" stroke="rgba(255,255,255,.3)" strokeWidth="4"/><circle cx="104" cy="104" r="96" fill="none" stroke="#fff" strokeWidth="4" strokeLinecap="round" strokeDasharray="150 453"/></>}
{map.ring==='full' && <circle cx="104" cy="104" r="96" fill="none" stroke="rgba(255,255,255,.85)" strokeWidth="4" strokeLinecap="round"/>}
</svg>
<Icon name={map.icon} size={50} stroke={1.7} style={status==='connecting' ? { animation: 'pg-spin 1s linear infinite' } : {}} />
{center}
</button>
<div style={{ textAlign: 'center' }}>
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600, color: 'var(--fg2)', whiteSpace: 'nowrap' }}>{map.cap}</div>
</div>
</div>
<div style={{ padding: '0 20px 18px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{free && (
<div style={{ background: 'var(--surface)', borderRadius: 'var(--radius-lg)', padding: '12px 14px', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 8 }}>
<Icon name="clock" size={15} stroke={2.2} color="var(--accent)" />
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg2)' }}>{t('quotaToday')}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: 'var(--fg1)', whiteSpace: 'nowrap' }}>{mins} {t('minutes')}</span>
<span style={{ marginLeft: 'auto', fontSize: 10.5, fontWeight: 600, color: 'var(--fg3)', background: 'var(--bg-subtle)', padding: '3px 9px', borderRadius: 'var(--radius-full)', whiteSpace: 'nowrap' }}>{t('quotaFree')}</span>
</div>
<div style={{ height: 6, background: 'var(--bg-subtle)', borderRadius: 3, overflow: 'hidden', marginBottom: 10 }}>
<div style={{ width: `${Math.min(100, mins / 10 * 100)}%`, height: '100%', background: mins <= 3 ? 'var(--warning)' : 'var(--accent)', borderRadius: 3, transition: 'width 300ms var(--ease-out)' }}></div>
</div>
{adDone ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, color: 'var(--success)', fontWeight: 700, fontSize: 13, padding: '9px' }}>
<Icon name="check-circle" size={16} stroke={2.2} color="var(--success)" />{t('adUnlocked')}
</div>
) : (
<button onClick={onAd} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, width: '100%', border: '1.5px solid var(--accent-border)', background: 'var(--accent-subtle)', color: 'var(--accent)', fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 13, padding: '9px', borderRadius: 'var(--radius-full)', cursor: 'pointer' }}>
<Icon name="play-circle" size={16} stroke={2.2} />{t('watchAd')}
</button>
)}
</div>
)}
{status === 'on' && (
<div style={{ display: 'flex', gap: 12 }}>
{[['arrow-down',t('download'),'86.4','Mb/s'],['arrow-up',t('upload'),'12.1','Mb/s'],['zap',t('latency'), String(server.ping),'ms']].map(([ic,l,v,u]) => (
<div key={l} style={{ flex: 1, background: 'var(--surface)', borderRadius: 'var(--radius-lg)', padding: '12px 14px', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--fg3)', fontSize: 11, fontWeight: 600, marginBottom: 4 }}><Icon name={ic} size={13} stroke={2.2} color="var(--accent)" />{l}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 17, fontWeight: 500, color: 'var(--fg1)' }}>{v}<span style={{ fontSize: 11, color: 'var(--fg3)' }}> {u}</span></div>
</div>
))}
</div>
)}
<button onClick={goServers} style={{
display: 'flex', alignItems: 'center', gap: 12, width: '100%', textAlign: 'left',
background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)',
padding: '12px 14px', cursor: 'pointer', boxShadow: 'var(--shadow-sm)',
}}>
<CC code={server.code} active />
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600, color: 'var(--fg1)', display: 'flex', alignItems: 'center', gap: 7 }}>
{smart ? t('smartSelect') : srvName(server, lang)}
{smart && <Icon name="zap" size={13} stroke={2.4} color="var(--accent)" />}
</div>
<div style={{ fontSize: 12, color: 'var(--fg3)' }}>{smart ? `${srvName(server, lang)} · ${server.ping}ms` : `${srvSub(server, lang)} · ${server.ping}ms`}</div>
</div>
<Icon name="chevron-right" size={20} color="var(--fg3)" />
</button>
</div>
</div>
);
}
/* ───────── SERVERS screen ───────── */
function ServersScreen({ t, lang, current, onPick, q, setQ }) {
const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase()));
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<TopBar t={t} lang={lang} />
<div style={{ padding: '0 20px 12px' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 24, fontWeight: 700, color: 'var(--fg1)', marginBottom: 12 }}>{t('chooseNode')}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'var(--bg-subtle)', borderRadius: 'var(--radius-md)', padding: '10px 14px' }}>
<Icon name="search" size={18} color="var(--fg3)" />
<input value={q} onChange={e => setQ(e.target.value)} placeholder={t('search')}
style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 15, color: 'var(--fg1)' }} />
</div>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
{/* smart select — recommended card */}
<button onClick={() => onPick('AUTO')} style={{
display: 'flex', alignItems: 'center', gap: 13, width: '100%', textAlign: 'left',
padding: '15px 14px', border: `1.5px solid ${current === 'AUTO' ? 'var(--accent)' : 'var(--accent-border)'}`, cursor: 'pointer',
background: 'var(--accent-subtle)',
borderRadius: 'var(--radius-xl)', boxShadow: current === 'AUTO' ? 'var(--shadow-md)' : 'var(--shadow-sm)', marginBottom: 12,
}}>
<div style={{ width: 42, height: 42, borderRadius: 'var(--radius-md)', background: 'linear-gradient(150deg,var(--clay-500),var(--clay-700))', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><Icon name="zap" size={21} stroke={2.2} color="#fff" /></div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontFamily: 'var(--font-sans)', fontSize: 15.5, fontWeight: 700, color: 'var(--fg1)' }}>{t('smartSelect')}</span>
<span style={{ fontSize: 10, fontWeight: 700, color: '#fff', background: 'var(--accent)', padding: '2px 8px', borderRadius: 999, letterSpacing: '0.04em' }}>{t('recommended')}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--fg2)', marginTop: 3, lineHeight: 1.5 }}>{t('smartSub')}</div>
</div>
{current === 'AUTO' && <Icon name="check" size={19} color="var(--accent)" stroke={2.6} />}
</button>
<div style={{ background: 'var(--surface)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)', overflow: 'hidden', border: '1px solid var(--border)' }}>
{list.map((s, i) => {
const active = s.code === current;
return (
<button key={s.code} onClick={() => onPick(s.code)} style={{
display: 'flex', alignItems: 'center', gap: 13, width: '100%', textAlign: 'left',
padding: '12px 14px', border: 'none', cursor: 'pointer',
borderBottom: i < list.length-1 ? '1px solid var(--border)' : 'none',
background: active ? 'var(--accent-subtle)' : 'transparent',
}}>
<CC code={s.code} active={active} />
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600, color: 'var(--fg1)' }}>{srvName(s, lang)}</div>
<div style={{ fontSize: 12, color: 'var(--fg3)' }}>{srvSub(s, lang)}</div>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg2)' }}>{s.ping}ms</span>
<Signal ping={s.ping} />
{active && <Icon name="check" size={18} color="var(--accent)" />}
</button>
);
})}
</div>
</div>
</div>
);
}
/* ───────── SETTINGS screen ───────── */
function Toggle({ on, onChange }) {
return (
<div onClick={onChange} style={{ width: 46, height: 28, borderRadius: 999, position: 'relative', cursor: 'pointer', flexShrink: 0,
background: on ? 'var(--accent)' : 'var(--border-strong)', transition: 'background 220ms var(--ease-out)' }}>
<div style={{ position: 'absolute', top: 3, left: on ? 21 : 3, width: 22, height: 22, borderRadius: '50%', background: '#fff', boxShadow: 'var(--shadow-sm)', transition: 'left 220ms var(--ease-out)' }} />
</div>
);
}
/* segmented language switch 中 / EN */
function LangSwitch({ lang, setLang }) {
return (
<div style={{ display: 'flex', background: 'var(--bg-subtle)', borderRadius: 'var(--radius-full)', padding: 3, gap: 2 }}>
{[['zh','中文'],['en','EN']].map(([v,l]) => (
<button key={v} onClick={() => setLang(v)} style={{
border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-full)', padding: '5px 13px',
fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 700,
background: lang===v ? 'var(--accent)' : 'transparent', color: lang===v ? 'var(--fg-on-accent)' : 'var(--fg3)',
transition: 'all 140ms var(--ease-out)',
}}>{l}</button>
))}
</div>
);
}
function Row({ icon, title, sub, right, last, onClick }) {
return (
<div onClick={onClick} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '13px 16px', borderBottom: last ? 'none' : '1px solid var(--border)', cursor: onClick ? 'pointer' : 'default' }}>
{icon && <div style={{ width: 32, height: 32, borderRadius: 'var(--radius-sm)', background: 'var(--accent-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><Icon name={icon} size={17} color="var(--accent)" stroke={2.2} /></div>}
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 500, color: 'var(--fg1)' }}>{title}</div>
{sub && <div style={{ fontSize: 12, color: 'var(--fg3)' }}>{sub}</div>}
</div>
{right}
</div>
);
}
function SettingsScreen({ t, lang, setLang, dark, setDark, free, setFree, onRedeem, onUpgrade, onContact, onSignOut, onReplayAuth, onReplayOnboarding }) {
const [autostart, setAuto] = useState(true);
const [kill, setKill] = useState(true);
const [twoFA, setTwoFA] = useState(true);
const [devices, setDevices] = useState([
{ id: 1, icon: 'laptop', name: 'MacBook Pro', os: 'macOS 26', active: t('thisDevice'), me: true },
{ id: 2, icon: 'smartphone', name: 'iPhone 17', os: 'iOS 26', active: '2 min', me: false },
{ id: 3, icon: 'monitor-smartphone', name: 'iPad Air', os: 'iPadOS 26', active: lang === 'zh' ? '3 小时前' : '3h ago', me: false },
]);
const card = { background: 'var(--surface)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--border)', overflow: 'hidden', marginBottom: 18 };
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<TopBar t={t} lang={lang} />
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 24, fontWeight: 700, color: 'var(--fg1)', marginBottom: 14 }}>{t('meTitle')}</div>
{/* plan banner */}
<div style={{ background: 'linear-gradient(155deg,var(--clay-600),var(--clay-800))', borderRadius: 'var(--radius-xl)', padding: 18, color: '#fff', marginBottom: 18, boxShadow: 'var(--shadow-md)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
<div style={{ width: 44, height: 44, borderRadius: '50%', background: 'rgba(255,255,255,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Icon name="crown" size={22} color="#fff" /></div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 700, fontSize: 16 }}>me@pangolin.vpn</div>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600, background: 'rgba(255,255,255,0.2)', padding: '2px 8px', borderRadius: 999, marginTop: 4 }}>{free ? t('freePlanName') : t('proMember')}</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: 16, paddingTop: 14, borderTop: '1px solid rgba(255,255,255,0.18)', fontSize: 12 }}>
<div>
<div style={{ opacity: 0.7 }}>{free ? t('quotaToday') : t('expires')}</div>
<div style={{ fontWeight: 600, marginTop: 2, fontFamily: 'var(--font-mono)', whiteSpace: 'nowrap' }}>{free ? t('quotaFree') : '2026-12-31 · 42.6 GB'}</div>
</div>
<button onClick={onUpgrade} style={{ border: 'none', background: '#fff', color: 'var(--clay-700)', fontWeight: 700, fontSize: 13, padding: '9px 16px', borderRadius: 'var(--radius-full)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6 }}><Icon name="ticket" size={15} color="var(--clay-700)" />{t('upgradeBtn')}</button>
</div>
</div>
{/* account info */}
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--fg2)', margin: '4px 4px 10px' }}>{t('accInfoTitle')}</div>
<div style={card}>
<Row icon="mail" title={t('accEmail')} sub="me@pangolin.vpn" right={<span style={{ fontSize: 13, color: 'var(--accent)', fontWeight: 600 }}>{t('accChange')}</span>} />
<Row icon="lock" title={t('accPassword')} sub="••••••••••" right={<span style={{ fontSize: 13, color: 'var(--accent)', fontWeight: 600 }}>{t('accChange')}</span>} />
<Row icon="shield" title={t('twoFA')} sub={t('twoFASub')} right={<Toggle on={twoFA} onChange={() => setTwoFA(!twoFA)} />} last />
</div>
{/* devices */}
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--fg2)', margin: '4px 4px 4px' }}>{t('myDevices')}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg3)', margin: '0 4px 10px' }}>{t('devicesSub')}</div>
<div style={card}>
{devices.map((d, i) => (
<div key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', borderBottom: i < devices.length - 1 ? '1px solid var(--border)' : 'none' }}>
<div style={{ width: 34, height: 34, borderRadius: 'var(--radius-md)', background: 'var(--accent-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><Icon name={d.icon} size={18} color="var(--accent)" /></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg1)', display: 'flex', alignItems: 'center', gap: 7 }}>{d.name}{d.me && <span style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--success)', background: 'var(--success-subtle)', padding: '1px 7px', borderRadius: 999 }}>{d.active}</span>}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg3)', marginTop: 1 }}>{d.os}{!d.me && ` · ${d.active}`}</div>
</div>
{!d.me && <button onClick={() => setDevices(devices.filter(x => x.id !== d.id))} style={{ border: '1.5px solid var(--border-strong)', background: 'transparent', color: 'var(--fg2)', fontSize: 12.5, fontWeight: 600, padding: '6px 12px', borderRadius: 'var(--radius-full)', cursor: 'pointer' }}>{t('remove')}</button>}
</div>
))}
</div>
{/* redeem + contact */}
<div style={card}>
<Row icon="shopping-bag" title={t('redeemEntry')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} onClick={onRedeem} />
<Row icon="external-link" title={t('ucEntry')} sub={t('ucEntrySub')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} onClick={() => window.open('../usercenter/index.html', '_blank')} />
<Row icon="message-circle" title={t('contactEntry')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} last onClick={onContact} />
</div>
{/* app settings */}
<div style={card}>
<Row icon="zap" title={t('smartRoute')} sub={t('smartRouteSub')} right={<Toggle on={autostart} onChange={() => setAuto(!autostart)} />} />
<Row icon="shield" title={t('killSwitch')} sub={t('killSwitchSub')} right={<Toggle on={kill} onChange={() => setKill(!kill)} />} last />
</div>
<div style={card}>
<Row icon="globe" title={t('language')} right={<LangSwitch lang={lang} setLang={setLang} />} />
<Row icon={dark ? 'moon' : 'sun'} title={t('darkAppearance')} sub={dark ? t('stateOn') : t('followLight')} right={<Toggle on={dark} onChange={() => setDark(!dark)} />} />
<Row icon="shield" title={t('protocol')} right={<span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--fg3)' }}>WireGuard</span>} />
<Row icon="refresh-cw" title={t('checkUpdate')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} last />
</div>
<div style={card}>
<Row icon="log-out" title={t('replayAuth')} sub={t('replayAuthSub')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} onClick={onReplayAuth} />
<Row icon="compass" title={t('replayOnboarding')} sub={t('replayOnboardingSub')} right={<Icon name="chevron-right" size={18} color="var(--fg3)" />} onClick={onReplayOnboarding} />
<Row icon="clock" title={t('freeDemo')} sub={t('freeDemoSub')} right={<Toggle on={free} onChange={() => setFree(!free)} />} last />
</div>
<div style={{ textAlign: 'center', fontSize: 12, color: 'var(--fg3)', fontFamily: 'var(--font-mono)' }}>穿山甲 · v2.4.0</div>
</div>
</div>
);
}
/* ───────── STATS screen ───────── */
function StatsScreen({ t, lang }) {
const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; const max = 6.1; const labels = t('days7');
const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' };
// 分设备用量(下分设备):名称 + 占比迷你条 + 流量/时长,流量降序。
const devs = [
{ name: 'iPhone 15', icon: 'smartphone', gb: '11.3', h: '5.2', frac: 1.0 },
{ name: 'MacBook Pro', icon: 'laptop', gb: '6.0', h: '3.1', frac: 0.53 },
{ name: 'Pixel 8', icon: 'smartphone', gb: '1.6', h: '1.1', frac: 0.14 },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<TopBar t={t} lang={lang} />
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 24, fontWeight: 700, color: 'var(--fg1)', marginBottom: 16 }}>{t('statsTitle')}</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
{[[t('trafficMonth'), '42.6', 'GB'], [t('avgPing'), '29', 'ms'], [t('durMonth'), '86.4', 'h']].map(([l, v, u]) => (
<div key={l} style={{ ...card, flex: 1, padding: '14px 14px' }}>
<div style={{ fontSize: 11, color: 'var(--fg3)', fontWeight: 600 }}>{l}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 500, color: 'var(--fg1)', marginTop: 6 }}>{v}<span style={{ fontSize: 11, color: 'var(--fg3)' }}> {u}</span></div>
</div>
))}
</div>
<div style={{ ...card, padding: '18px 18px' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg1)', marginBottom: 18 }}>{t('weekTraffic')}</div>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 12, height: 120 }}>
{vals.map((v, i) => (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 7 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg3)' }}>{v}</div>
<div style={{ width: '100%', maxWidth: 30, height: `${v / max * 84}px`, background: 'var(--accent)', borderRadius: '6px 6px 0 0', opacity: 0.85 }} />
<div style={{ fontSize: 11, color: 'var(--fg3)' }}>{labels[i]}</div>
</div>
))}
</div>
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg1)', margin: '20px 0 12px 2px' }}>{t('byDevice')}</div>
<div style={{ ...card, overflow: 'hidden' }}>
{devs.map((d, i) => (
<div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', borderBottom: i < devs.length - 1 ? '1px solid var(--border)' : 'none' }}>
<div style={{ width: 38, height: 38, borderRadius: 'var(--radius-md)', background: 'var(--accent-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Icon name={d.icon} size={19} color="var(--accent)" stroke={2.2} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14.5, fontWeight: 600, color: 'var(--fg1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.name}</div>
<div style={{ marginTop: 8, height: 4, borderRadius: 999, background: 'var(--border)', overflow: 'hidden' }}>
<div style={{ width: `${d.frac * 100}%`, height: '100%', background: 'var(--accent)', opacity: 0.85, borderRadius: 999 }} />
</div>
</div>
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14.5, fontWeight: 500, color: 'var(--fg1)' }}>{d.gb}<span style={{ fontSize: 11, color: 'var(--fg3)' }}> GB</span></div>
<div style={{ fontSize: 11, color: 'var(--fg3)', marginTop: 2 }}>{d.h} h</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
/* ───────── Bottom tab ───────── */
function BottomTab({ t, tab, setTab }) {
const items = [['connect','power',t('tabConnect')],['servers','globe',t('tabServers')],['stats','chart',t('tabStats')],['me','user',t('tabMe')]];
return (
<div style={{ display: 'flex', borderTop: '1px solid var(--border)', background: 'var(--surface)', paddingBottom: 22 }}>
{items.map(([id, ic, label]) => {
const a = tab === id;
return (
<button key={id} onClick={() => setTab(id)} style={{ flex: 1, border: 'none', background: 'transparent', cursor: 'pointer', padding: '10px 0 4px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<Icon name={ic} size={22} stroke={a ? 2.4 : 2} color={a ? 'var(--accent)' : 'var(--fg3)'} />
<span style={{ fontFamily: 'var(--font-sans)', fontSize: 11, fontWeight: a ? 700 : 500, color: a ? 'var(--accent)' : 'var(--fg3)' }}>{label}</span>
</button>
);
})}
</div>
);
}
Object.assign(window, { ConnectScreen, ServersScreen, StatsScreen, SettingsScreen, BottomTab, LangSwitch });