/* 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 (
{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])=>(
))}
{/* usage chart + quick actions */}
{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')}
{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')}
);
}
/* ───────── 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])=>(
))}
{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 });