/* ucapp.jsx — 穿山甲 Web 用户中心 · views */ const { useState: useStateU } = React; const ucCard = { background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-xl)', boxShadow:'var(--shadow-sm)' }; const ucInput = { border:'1.5px solid var(--border-strong)', borderRadius:'var(--radius-md)', padding:'12px 14px', fontFamily:'var(--font-sans)', fontSize:14.5, color:'var(--fg1)', background:'var(--surface)', outline:'none', width:'100%', boxSizing:'border-box' }; /* viewport hook — mobile ≤ 700px */ function useIsMobileUC() { const [m, setM] = useStateU(typeof window !== 'undefined' && window.innerWidth <= 700); React.useEffect(() => { const f = () => setM(window.innerWidth <= 700); window.addEventListener('resize', f); return () => window.removeEventListener('resize', f); }, []); return m; } function UCLang({ lang, setLang }) { return
{[['zh','中文'],['en','EN']].map(([v,l])=>( ))}
; } /* ───────── Login (+ PRO 2FA step) ───────── */ function UCLogin({ t, lang, setLang, onDone }) { const [email,setEmail]=useStateU(''); const [pw,setPw]=useStateU(''); const [step,setStep]=useStateU('cred'); // cred | totp const [otp,setOtp]=useStateU(''); const ok=/\S+@\S+\.\S+/.test(email)&&pw.length>0; return (
穿山甲
PANGOLIN
{step==='cred' ? (<>
{t('loginTitle')}
{t('loginSub')}
{t('forgotPw')}
{t('noAccount')}
) : (<>
{t('twoFATitle')} PRO
{t('twoFAHint')}
setOtp(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="······" style={{ ...ucInput, fontFamily:'var(--font-mono)', fontSize:22, letterSpacing:'0.5em', textAlign:'center', padding:'14px' }} />
setStep('cred')} style={{ textAlign:'center', fontSize:12.5, color:'var(--fg3)', marginTop:18, cursor:'pointer' }}>{t('twoFABack')}
)}
); } /* ───────── Overview ───────── */ function UCOverview({ t, lang, free, mobile, goSub, goRedeem }) { const vals=[1.8,2.4,1.2,3.1,4.0,5.2,2.6]; const max=5.2; const labels = lang==='zh'?['一','二','三','四','五','六','日']:['M','T','W','T','F','S','S']; return (
{t('greeting')}
me@pangolin.vpn
{/* plan banner */}
{t('curPlan')}
{free?t('freePlan'):t('proMember')}
{free?t('quotaFree'):`${t('expires')} 2026-12-31`}
{/* stats */}
{[['clock',t('quotaToday'), free?'6':'∞', free?'min':''],['arrow-down',t('dataToday'),'0.8','GB'],['smartphone',t('devices'),'2 / 5','']].map(([ic,l,v,u])=>(
{l}
{v}{u&& {u}}
))}
{/* usage chart + quick actions */}
{t('usageTitle')}
{vals.map((v,i)=>(
{v}
{labels[i]}
))}
{t('quickSub')}
{[['link',t('qaSub'),goSub],['ticket',t('qaRedeem'),goRedeem],['download',t('qaApp'),null]].map(([ic,l,fn])=>( ))}
); } /* ───────── Subscription ───────── */ function UCSub({ t, lang, mobile }) { const [copied,setCopied]=useStateU(false); const [reset,setReset]=useStateU(false); const url='https://sub.pangolin.vpn/s/8f3kx92m' + (reset?'-new':''); const clients=[ { name:'穿山甲 App', sub:'iOS / Android / 桌面', icon:'shield-check', accent:true }, { name:'Shadowrocket', sub:'iOS', icon:'external-link' }, { name:'Clash Verge', sub:'Windows / macOS', icon:'external-link' }, { name:'v2rayN', sub:'Windows', icon:'external-link' }, { name:'sing-box', sub:'全平台', icon:'external-link' }, ]; return (
{t('subTitle')}
{t('subDesc')}
{/* link card */}
{url}
{reset&&}{reset?t('subResetOk'):t('subResetSub')}
{/* QR */}
{t('scanTitle')}
{t('scanSub')}
{/* one-tap import */}
{t('importTitle')}
{t('importSub')}
{clients.map(c=>( ))}
{t('fmtNote')}
); } /* ───────── Redeem ───────── */ function UCRedeem({ t, lang }) { const [code,setCode]=useStateU(''); const [ok,setOk]=useStateU(false); const channels=[ { icon:'shopping-bag', name:t('chStore'), sub:t('chStoreSub'), accent:true }, { icon:'credit-card', name:'USDT (TRC20)', sub:t('chUsdtSub') }, { icon:'send', name:'Telegram', sub:'@PangolinVPN_bot', mono:true }, { icon:'message-circle', name:'LINE', sub:'@pangolinvpn', mono:true }, { icon:'mail', name:t('chEmail'), sub:'buy@pangolin.vpn', mono:true }, ]; return (
{t('navRedeem')}
{t('redeemTitle')}
{ok ? (
{t('redeemOk')}
) : (
setCode(e.target.value.toUpperCase())} placeholder={t('redeemPh')} style={{ ...ucInput, fontFamily:'var(--font-mono)', letterSpacing:'0.08em' }} />
)}
{t('buyTitle')}
{t('buySub')}
{channels.map(c=>( ))}
); } /* ───────── Invite ───────── */ function UCInvite({ t, lang }) { const [copied,setCopied]=useStateU(false); const records=[ { mail:'k***@gmail.com', date:'2026-06-08', paid:true }, { mail:'w***@outlook.com', date:'2026-06-05', paid:false }, { mail:'z***@qq.com', date:'2026-05-30', paid:true }, ]; return (
{t('inviteTitle')}
{t('inviteSub')}
{t('inviteLink')}
https://pangolin.vpn/r/PG8F3K
{[[t('invited'),'8',''],[t('paidUsers'),'3',''],[t('earned'),'¥45','']].map(([l,v])=>(
{l}
{v}
))}
{t('inviteRecord')}
{records.map((r,i)=>(
{r.mail} {r.date} {r.paid?t('recPaid'):t('recRegistered')}
))}
); } /* ───────── App shell ───────── */ function UserCenterApp() { const [stage,setStage]=useStateU('app'); // app | login — opens on the dashboard const [view,setView]=useStateU('overview'); const [lang,setLang]=useStateU('zh'); const [free]=useStateU(false); const [dir,setDir]=useStateU(0); // -1 左进 / 1 右进 / 0 无动画 const mobile=useIsMobileUC(); const t=ucT(lang); const touchRef=React.useRef({x:0,y:0}); const ORDER=['overview','sub','redeem','invite']; function go(id){ const a=ORDER.indexOf(view), b=ORDER.indexOf(id); setDir(b>a?1:bsetDir(0),300); } function onTouchStart(e){ const p=e.touches?e.touches[0]:e; touchRef.current={x:p.clientX,y:p.clientY}; } function onTouchEnd(e){ const p=e.changedTouches?e.changedTouches[0]:e; 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=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]); } } if(stage==='login') return setStage('app')} />; const nav=[['overview','layout-dashboard',t('navOverview')],['sub','link',t('navSub')],['redeem','ticket',t('navRedeem')],['invite','users',t('navInvite')]]; let main; if(view==='overview') main=setView('sub')} goRedeem={()=>setView('redeem')} />; else if(view==='sub') main=; else if(view==='redeem') main=; else main=; const navBtns = nav.map(([id,ic,l])=>( )); return (
{/* top bar */}
穿山甲
{!mobile && } {mobile &&
}
{mobile && }
setDir(0)} style={{ animation: dir!==0 ? `uc-in-${dir===1?'l':'r'} 200ms var(--ease-out)` : 'none' }}>{main}
); } Object.assign(window, { UserCenterApp });