a642bf16a2
design/ 同步自最新设计导出,新增 ui_kits/tablet/ 平板分栏布局;todo/ 录入 18 个并行实施任务。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
279 lines
23 KiB
React
279 lines
23 KiB
React
/* dapp.jsx — 穿山甲 VPN desktop · window + views (i18n) */
|
|
const { useState, useEffect, useRef } = React;
|
|
|
|
function DToggle({ on, onChange }) {
|
|
return <div onClick={onChange} style={{ width:42, height:25, 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?20:3, width:19, height:19, borderRadius:'50%', background:'#fff', boxShadow:'var(--shadow-sm)', transition:'left 220ms var(--ease-out)' }} /></div>;
|
|
}
|
|
function DLangSwitch({ lang, setLang }) {
|
|
return <div style={{ display:'flex', background:'var(--bg-subtle)', borderRadius:999, padding:3, gap:2 }}>
|
|
{[['zh','中文'],['en','EN']].map(([v,l])=>(
|
|
<button key={v} onClick={()=>setLang(v)} style={{ border:'none', cursor:'pointer', borderRadius:999, padding:'4px 12px', fontFamily:'var(--font-sans)', fontSize:12, 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 NavItem({ icon, label, active, onClick }) {
|
|
return (
|
|
<button onClick={onClick} style={{ display:'flex', alignItems:'center', gap:11, width:'100%', textAlign:'left', border:'none', cursor:'pointer',
|
|
background: active?'var(--accent-subtle)':'transparent', color: active?'var(--accent)':'var(--fg2)',
|
|
padding:'9px 12px', borderRadius:'var(--radius-md)', fontFamily:'var(--font-sans)', fontSize:13.5, fontWeight: active?600:500, transition:'background-color 140ms var(--ease-out), color 140ms var(--ease-out)' }}>
|
|
<DIcon name={icon} size={18} stroke={active?2.3:2} color={active?'var(--accent)':'var(--fg3)'} />{label}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function DConnect({ t, lang, status, onToggle, server, smart, free, mins, adDone, onAd, elapsed }) {
|
|
const map = {
|
|
off:{ fill:'var(--sand-100)', fg:'var(--clay-500)', icon:'power', cap:t('capOff'), sub:t('subOff'), ring:'track' },
|
|
connecting:{ fill:'var(--accent)', fg:'#fff', icon:'loader-circle', cap:t('capConnecting'), sub:t('subConnecting'), ring:'spin' },
|
|
on:{ fill:'var(--success)', fg:'#fff', icon:'shield-check', cap:t('capOn'), sub:t('subOn'), 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 8px ${status==='on'?'var(--success-subtle)':'var(--accent-subtle)'}, var(--shadow-lg)`;
|
|
return (
|
|
<div style={{ display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', height:'100%', gap:24, padding:'10px 40px' }}>
|
|
<button onClick={onToggle} style={{ position:'relative', width:176, height:176, borderRadius:'50%', border:'none', cursor:'pointer', background:map.fill, color:map.fg,
|
|
boxShadow:shadow, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:7, transition:'box-shadow 300ms var(--ease-out), background-color 300ms var(--ease-out)' }}>
|
|
<svg viewBox="0 0 176 176" 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="88" cy="88" r="81" fill="none" stroke="var(--sand-200)" strokeWidth="3" strokeDasharray="2 8" strokeLinecap="round"/>}
|
|
{map.ring==='spin' && <><circle cx="88" cy="88" r="81" fill="none" stroke="rgba(255,255,255,.3)" strokeWidth="4"/><circle cx="88" cy="88" r="81" fill="none" stroke="#fff" strokeWidth="4" strokeLinecap="round" strokeDasharray="130 380"/></>}
|
|
{map.ring==='full' && <circle cx="88" cy="88" r="81" fill="none" stroke="rgba(255,255,255,.85)" strokeWidth="4" strokeLinecap="round"/>}
|
|
</svg>
|
|
<DIcon name={map.icon} size={46} stroke={1.7} style={status==='connecting'?{animation:'pg-spin 1s linear infinite'}:{}} />
|
|
{status==='off' && <span style={{ fontFamily:'var(--font-sans)', fontWeight:700, fontSize:13 }}>{t('connectNow')}</span>}
|
|
{status==='connecting' && <span style={{ fontFamily:'var(--font-mono)', fontWeight:700, fontSize:13, letterSpacing:'0.08em' }}>···</span>}
|
|
{status==='on' && <span style={{ fontFamily:'var(--font-mono)', fontWeight:600, fontSize:15 }}>{fmt(elapsed)}</span>}
|
|
</button>
|
|
<div style={{ textAlign:'center' }}>
|
|
<div style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:22, color:'var(--fg1)' }}>{map.cap}</div>
|
|
<div style={{ fontSize:13, color:'var(--fg3)', marginTop:4 }}>{map.sub}</div>
|
|
<div style={{ display:'inline-flex', alignItems:'center', gap:7, marginTop:10, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-full)', padding:'6px 14px', fontSize:12.5, fontWeight:600, color:'var(--fg2)' }}>
|
|
{smart && <DIcon name="zap" size={13} stroke={2.4} color="var(--accent)" />}
|
|
<span style={{ whiteSpace:'nowrap' }}>{smart ? `${t('smartSelect')} · ` : ''}{dName(server,lang)} · {server.ping}ms</span>
|
|
</div>
|
|
</div>
|
|
{free && (
|
|
<div style={{ width:340, 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 }}>
|
|
<DIcon name="clock" size={14} 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:13.5, 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 }}></div>
|
|
</div>
|
|
{adDone ? (
|
|
<div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:7, color:'var(--success)', fontWeight:700, fontSize:13, padding:'8px' }}>
|
|
<DIcon name="check-circle" size={15} 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:'8px', borderRadius:'var(--radius-full)', cursor:'pointer' }}>
|
|
<DIcon name="play-circle" size={15} stroke={2.2} />{t('watchAd')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
{status==='on' &&
|
|
<div style={{ display:'flex', gap:10 }}>
|
|
{[['arrow-down','86.4','Mb/s'],['arrow-up','12.1','Mb/s'],['zap',String(server.ping),'ms']].map(([ic,v,u])=>(
|
|
<div key={ic} style={{ display:'flex', alignItems:'center', gap:7, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:'8px 14px', boxShadow:'var(--shadow-sm)' }}>
|
|
<DIcon name={ic} size={15} stroke={2.2} color="var(--accent)" /><span style={{ fontFamily:'var(--font-mono)', fontSize:14, color:'var(--fg1)' }}>{v}<span style={{ fontSize:10, color:'var(--fg3)' }}> {u}</span></span>
|
|
</div>
|
|
))}
|
|
</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DServers({ t, lang, current, onPick }) {
|
|
const [q,setQ]=useState('');
|
|
const list = DSERVERS.filter(s=>(s.name+s.en).toLowerCase().includes(q.toLowerCase()));
|
|
const smartActive = current==='AUTO';
|
|
return (
|
|
<div style={{ padding:'4px 28px 24px' }}>
|
|
<div style={{ display:'flex', alignItems:'center', gap:9, background:'var(--bg-subtle)', borderRadius:'var(--radius-md)', padding:'9px 13px', marginBottom:16, maxWidth:340 }}>
|
|
<DIcon name="search" size={17} 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:13.5, color:'var(--fg1)' }} />
|
|
</div>
|
|
{/* smart select — recommended card */}
|
|
<button onClick={()=>onPick('AUTO')} style={{ display:'flex', alignItems:'center', gap:12, width:'100%', textAlign:'left', cursor:'pointer',
|
|
background:'var(--accent-subtle)', border:`1.5px solid ${smartActive?'var(--accent)':'var(--accent-border)'}`,
|
|
borderRadius:'var(--radius-xl)', padding:'14px', boxShadow:smartActive?'var(--shadow-md)':'var(--shadow-sm)', marginBottom:10 }}>
|
|
<div style={{ width:38, height:38, borderRadius:'var(--radius-md)', background:'linear-gradient(150deg,var(--clay-500),var(--clay-700))', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}><DIcon name="zap" size={18} 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:14.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:11.5, color:'var(--fg2)', marginTop:3 }}>{t('smartSub')}</div>
|
|
</div>
|
|
{smartActive && <DIcon name="check" size={18} color="var(--accent)" stroke={2.6} />}
|
|
</button>
|
|
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
|
|
{list.map(s=>{
|
|
const active = s.code===current;
|
|
return (
|
|
<button key={s.code} onClick={()=>onPick(s.code)} style={{ display:'flex', alignItems:'center', gap:12, textAlign:'left', cursor:'pointer',
|
|
background: active?'var(--accent-subtle)':'var(--surface)', border:`1.5px solid ${active?'var(--accent-border)':'var(--border)'}`,
|
|
borderRadius:'var(--radius-lg)', padding:'12px 14px', boxShadow:'var(--shadow-sm)' }}>
|
|
<DCC code={s.code} active={active} />
|
|
<div style={{ flex:1, minWidth:0 }}>
|
|
<div style={{ fontFamily:'var(--font-sans)', fontSize:14, fontWeight:600, color:'var(--fg1)' }}>{dName(s,lang)}</div>
|
|
<div style={{ fontSize:11.5, color:'var(--fg3)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{dSub(s,lang)}</div>
|
|
</div>
|
|
<div style={{ display:'flex', flexDirection:'column', alignItems:'flex-end', gap:5 }}>
|
|
<span style={{ fontFamily:'var(--font-mono)', fontSize:11.5, color:'var(--fg2)' }}>{s.ping}ms</span>
|
|
<DSignal ping={s.ping} />
|
|
</div>
|
|
{active && <DIcon name="check" size={17} color="var(--accent)" />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DStats({ t, lang }) {
|
|
const vals=[2.1,3.4,1.8,4.6,5.2,6.1,3.0]; const labels=t('days'); const max=6.1;
|
|
return (
|
|
<div style={{ padding:'8px 32px 24px' }}>
|
|
<div style={{ display:'flex', gap:14, marginBottom:22 }}>
|
|
{[[t('trafficMonth'),'42.6 GB'],[t('avgPing'),'29 ms'],[t('durMonth'),'86.4 h']].map(([l,v])=>(
|
|
<div key={l} style={{ flex:1, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-lg)', padding:'16px 18px', boxShadow:'var(--shadow-sm)' }}>
|
|
<div style={{ fontSize:12, color:'var(--fg3)', fontWeight:600 }}>{l}</div>
|
|
<div style={{ fontFamily:'var(--font-mono)', fontSize:24, fontWeight:500, color:'var(--fg1)', marginTop:6 }}>{v}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div style={{ background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-lg)', padding:'18px 20px', boxShadow:'var(--shadow-sm)' }}>
|
|
<div style={{ fontSize:13, fontWeight:600, color:'var(--fg1)', marginBottom:18 }}>{t('weekTraffic')}</div>
|
|
<div style={{ display:'flex', alignItems:'flex-end', gap:18, height:120 }}>
|
|
{vals.map((v,i)=>(
|
|
<div key={i} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', gap:8 }}>
|
|
<div style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--fg3)' }}>{v}</div>
|
|
<div style={{ width:'100%', maxWidth:34, height:`${v/max*86}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>
|
|
);
|
|
}
|
|
|
|
function DSettings({ t, lang, setLang, dark, setDark, free, setFree, onReplayAuth, onReplayOnboarding, onRedeem }) {
|
|
const [auto,setAuto]=useState(true); const [kill,setKill]=useState(true); const [boot,setBoot]=useState(true);
|
|
const card={ background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-lg)', boxShadow:'var(--shadow-sm)', overflow:'hidden', marginBottom:16 };
|
|
const Row=({title,sub,right,last,onClick})=>(
|
|
<div onClick={onClick} style={{ display:'flex', alignItems:'center', gap:12, padding:'14px 18px', borderBottom:last?'none':'1px solid var(--border)', cursor:onClick?'pointer':'default' }}>
|
|
<div style={{ flex:1 }}><div style={{ fontSize:14, fontWeight:500, color:'var(--fg1)' }}>{title}</div>{sub&&<div style={{ fontSize:12, color:'var(--fg3)', marginTop:2 }}>{sub}</div>}</div>{right}
|
|
</div>
|
|
);
|
|
return (
|
|
<div style={{ padding:'8px 32px 24px', maxWidth:560 }}>
|
|
<div style={card}>
|
|
<Row title={t('redeemEntry')} sub={t('proMember')} right={<DIcon name="chevron-right" size={18} color="var(--fg3)" />} last onClick={onRedeem} />
|
|
</div>
|
|
<div style={card}>
|
|
<Row title={t('autostart')} sub={t('autostartSub')} right={<DToggle on={boot} onChange={()=>setBoot(!boot)} />} />
|
|
<Row title={t('smartRoute')} sub={t('smartRouteSub')} right={<DToggle on={auto} onChange={()=>setAuto(!auto)} />} />
|
|
<Row title={t('killSwitch')} sub={t('killSwitchSub')} right={<DToggle on={kill} onChange={()=>setKill(!kill)} />} last />
|
|
</div>
|
|
<div style={card}>
|
|
<Row title={t('language')} right={<DLangSwitch lang={lang} setLang={setLang} />} />
|
|
<Row title={t('darkAppearance')} sub={dark?t('stateOn'):t('followLight')} right={<DToggle on={dark} onChange={()=>setDark(!dark)} />} />
|
|
<Row title={t('protocol')} right={<span style={{ fontFamily:'var(--font-mono)', fontSize:13, color:'var(--fg3)' }}>WireGuard</span>} />
|
|
<Row title={t('checkUpdate')} right={<DIcon name="chevron-right" size={18} color="var(--fg3)" />} />
|
|
<Row title={t('version')} right={<span style={{ fontFamily:'var(--font-mono)', fontSize:13, color:'var(--fg3)' }}>v2.4.0 (macOS)</span>} last />
|
|
</div>
|
|
<div style={card}>
|
|
<Row title={t('replayAuth')} sub={t('replayAuthSub')} right={<DIcon name="chevron-right" size={18} color="var(--fg3)" />} onClick={onReplayAuth} />
|
|
<Row title={t('replayOnboarding')} sub={t('replayOnboardingSub')} right={<DIcon name="chevron-right" size={18} color="var(--fg3)" />} onClick={onReplayOnboarding} />
|
|
<Row title={t('freeDemo')} sub={t('freeDemoSub')} right={<DToggle on={free} onChange={()=>setFree(!free)} />} last />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DesktopApp() {
|
|
const [stage,setStage]=useState('app'); // app | auth | onboarding — UI kit opens on the main app
|
|
const [view,setView]=useState('connect');
|
|
const [status,setStatus]=useState('off');
|
|
const [code,setCode]=useState('AUTO'); // 'AUTO' = 智能选择(默认)
|
|
const [dark,setDark]=useState(false);
|
|
const [lang,setLang]=useState('zh');
|
|
const [free,setFree]=useState(true); // 演示:免费版视角(与移动端一致)
|
|
const [mins,setMins]=useState(6);
|
|
const [adDone,setAdDone]=useState(false);
|
|
const [elapsed,setElapsed]=useState(0);
|
|
const timer=useRef(null);
|
|
const t=dT(lang);
|
|
const smart=code==='AUTO';
|
|
const server=smart ? DSERVERS.reduce((a,b)=>a.ping<b.ping?a:b) : DSERVERS.find(s=>s.code===code);
|
|
const titles={connect:t('connect'),servers:t('servers'),stats:t('stats'),account:t('navAccount'),plans:t('choosePlan'),redeem:t('redeemTitle'),contact:t('navContact'),settings:t('settings')};
|
|
|
|
function toggle(){ if(status==='off'){ setStatus('connecting'); setTimeout(()=>{setStatus('on');setElapsed(0);},1500); } else setStatus('off'); }
|
|
function pick(c){ setCode(c); setView('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]);
|
|
|
|
if(stage==='auth') return (
|
|
<div style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:'100vh', padding:28, boxSizing:'border-box' }}>
|
|
<DAuthFlow t={t} lang={lang} dark={dark} onDone={()=>setStage('onboarding')} />
|
|
</div>
|
|
);
|
|
if(stage==='onboarding') return (
|
|
<div style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:'100vh', padding:28, boxSizing:'border-box' }}>
|
|
<DOnboarding t={t} lang={lang} dark={dark} onDone={()=>setStage('app')} />
|
|
</div>
|
|
);
|
|
|
|
let main;
|
|
if(view==='connect') main=<DConnect t={t} lang={lang} status={status} onToggle={toggle} server={server} smart={smart} free={free} mins={mins} adDone={adDone} onAd={()=>setAdDone(true)} elapsed={elapsed} />;
|
|
else if(view==='servers') main=<DServers t={t} lang={lang} current={code} onPick={pick} />;
|
|
else if(view==='stats') main=<DStats t={t} lang={lang} />;
|
|
else if(view==='account') main=<DAccountView t={t} lang={lang} free={free} onUpgrade={()=>setView('plans')} onSignOut={()=>setStage('auth')} />;
|
|
else if(view==='plans') main=<DPlansView t={t} lang={lang} onChoose={()=>setView('redeem')} />;
|
|
else if(view==='redeem') main=<DRedeemView t={t} lang={lang} />;
|
|
else if(view==='contact') main=<DContactView t={t} lang={lang} />;
|
|
else main=<DSettings t={t} lang={lang} setLang={setLang} dark={dark} setDark={setDark} free={free} setFree={setFree} onReplayAuth={()=>setStage('auth')} onReplayOnboarding={()=>setStage('onboarding')} onRedeem={()=>setView('redeem')} />;
|
|
|
|
return (
|
|
<div data-theme={dark?'dark':'light'} style={{ width:920, height:600, borderRadius:16, overflow:'hidden', display:'flex',
|
|
background:'var(--bg)', boxShadow:'0 0 0 1px rgba(0,0,0,0.12), 0 28px 70px rgba(0,0,0,0.4)', fontFamily:'var(--font-sans)' }}>
|
|
<div style={{ width:204, flexShrink:0, background:'var(--bg-subtle)', borderRight:'1px solid var(--border)', display:'flex', flexDirection:'column', padding:'14px 12px' }}>
|
|
<div style={{ padding:'2px 4px 16px' }}><MacTrafficLights /></div>
|
|
<div style={{ display:'flex', alignItems:'center', gap:9, padding:'0 6px 16px' }}>
|
|
<DMark size={28} /><div><div style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:16, color:'var(--fg1)', lineHeight:1 }}>{lang==='zh'?'穿山甲':'Pangolin'}</div><div style={{ fontSize:9, fontWeight:600, letterSpacing:'0.18em', color:'var(--accent)', marginTop:3 }}>PANGOLIN</div></div>
|
|
</div>
|
|
<div style={{ display:'flex', flexDirection:'column', gap:3 }}>
|
|
<NavItem icon="power" label={t('navConnect')} active={view==='connect'} onClick={()=>setView('connect')} />
|
|
<NavItem icon="globe" label={t('navServers')} active={view==='servers'} onClick={()=>setView('servers')} />
|
|
<NavItem icon="chart-no-axes-column" label={t('navStats')} active={view==='stats'} onClick={()=>setView('stats')} />
|
|
<NavItem icon="user" label={t('navAccount')} active={view==='account'||view==='plans'||view==='redeem'} onClick={()=>setView('account')} />
|
|
<NavItem icon="message-circle" label={t('navContact')} active={view==='contact'} onClick={()=>setView('contact')} />
|
|
<NavItem icon="settings" label={t('navSettings')} active={view==='settings'} onClick={()=>setView('settings')} />
|
|
</div>
|
|
<div style={{ flex:1 }} />
|
|
<div style={{ display:'flex', alignItems:'center', gap:10, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:'9px 11px' }}>
|
|
<div style={{ width:30, height:30, borderRadius:'50%', background:'linear-gradient(150deg,var(--clay-500),var(--clay-700))', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}><DIcon name="crown" size={15} color="#fff" /></div>
|
|
<div style={{ minWidth:0 }}><div style={{ fontSize:12, fontWeight:600, color:'var(--fg1)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{free?t('freePlanName'):t('proMember')}</div><div style={{ fontSize:10.5, color:'var(--fg3)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{free?t('quotaFree'):t('until')}</div></div>
|
|
</div>
|
|
</div>
|
|
<div style={{ flex:1, display:'flex', flexDirection:'column', minWidth:0 }}>
|
|
<div style={{ height:52, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 24px', borderBottom:'1px solid var(--border)' }}>
|
|
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
|
|
{(view==='redeem'||view==='plans') && <button onClick={()=>setView('account')} style={{ border:'none', background:'transparent', cursor:'pointer', padding:4, display:'flex' }}><DIcon name="arrow-left" size={20} color="var(--fg1)" /></button>}
|
|
<div style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:17, color:'var(--fg1)' }}>{titles[view]}</div>
|
|
</div>
|
|
<div style={{ display:'flex', alignItems:'center', gap:12 }}>
|
|
<span style={{ fontFamily:'var(--font-mono)', fontSize:12, fontWeight:600, whiteSpace:'nowrap', color:status==='on'?'var(--success)':'var(--fg3)' }}>{status==='on'?'● '+t('connected')+' '+server.code:'○ '+t('notConnected')}</span>
|
|
<button onClick={()=>setDark(!dark)} style={{ border:'none', background:'transparent', cursor:'pointer', padding:6, borderRadius:'var(--radius-sm)', display:'flex' }}><DIcon name={dark?'sun':'moon'} size={18} color="var(--fg2)" /></button>
|
|
</div>
|
|
</div>
|
|
<div style={{ flex:1, overflow:'auto', minHeight:0 }}>{main}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
Object.assign(window, { DesktopApp });
|