Files
pangolin/design/ui_kits/usercenter/ucapp.jsx
T
wangjia a642bf16a2 feat: 同步 design/ 设计系统(含 iPad tablet kit) + 架构任务拆分(todo/)
design/ 同步自最新设计导出,新增 ui_kits/tablet/ 平板分栏布局;todo/ 录入 18 个并行实施任务。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:57:57 +08:00

368 lines
26 KiB
React

/* 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 <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:'5px 12px', fontFamily:'var(--font-sans)', fontSize:12.5, fontWeight:700, background:lang===v?'var(--accent)':'transparent', color:lang===v?'var(--fg-on-accent)':'var(--fg3)' }}>{l}</button>
))}</div>;
}
/* ───────── 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 (
<div style={{ minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', background:'var(--bg)', padding:24 }}>
<div style={{ ...ucCard, width:400, maxWidth:'100%', padding:'36px 34px', boxSizing:'border-box' }}>
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:26 }}>
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
<UCMark size={32} />
<div>
<div style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:17, color:'var(--fg1)', lineHeight:1 }}>穿山甲</div>
<div style={{ fontSize:8.5, fontWeight:600, letterSpacing:'0.2em', color:'var(--accent)', marginTop:3 }}>PANGOLIN</div>
</div>
</div>
<UCLang lang={lang} setLang={setLang} />
</div>
{step==='cred' ? (<>
<div style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:22, color:'var(--fg1)' }}>{t('loginTitle')}</div>
<div style={{ fontSize:13.5, color:'var(--fg3)', margin:'6px 0 24px' }}>{t('loginSub')}</div>
<label style={{ display:'block', marginBottom:14 }}>
<span style={{ display:'block', fontSize:12.5, fontWeight:600, color:'var(--fg2)', marginBottom:6 }}>{t('emailLabel')}</span>
<input style={ucInput} value={email} onChange={e=>setEmail(e.target.value)} placeholder={t('emailPh')} />
</label>
<label style={{ display:'block' }}>
<span style={{ display:'block', fontSize:12.5, fontWeight:600, color:'var(--fg2)', marginBottom:6 }}>{t('pwLabel')}</span>
<input style={ucInput} type="password" value={pw} onChange={e=>setPw(e.target.value)} placeholder={t('pwPh')} />
</label>
<div style={{ textAlign:'right', margin:'10px 0 18px' }}><span style={{ fontSize:13, color:'var(--accent)', fontWeight:600, cursor:'pointer' }}>{t('forgotPw')}</span></div>
<button onClick={ok?()=>setStep('totp'):undefined} disabled={!ok} style={{ width:'100%', border:'none', borderRadius:'var(--radius-full)', padding:'13px', fontFamily:'var(--font-sans)', fontWeight:700, fontSize:15, cursor:ok?'pointer':'not-allowed', background:'var(--accent)', color:'var(--fg-on-accent)', opacity:ok?1:0.45 }}>{t('doLogin')}</button>
<div style={{ textAlign:'center', fontSize:12.5, color:'var(--fg3)', marginTop:18 }}>{t('noAccount')}</div>
</>) : (<>
<div style={{ display:'flex', alignItems:'center', gap:9, marginBottom:6 }}>
<UCIcon name="shield-check" size={20} color="var(--accent)" />
<span style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:20, color:'var(--fg1)' }}>{t('twoFATitle')}</span>
<span style={{ fontSize:10.5, fontWeight:700, color:'var(--accent)', background:'var(--accent-subtle)', border:'1px solid var(--accent-border)', padding:'2px 8px', borderRadius:999 }}>PRO</span>
</div>
<div style={{ fontSize:13, color:'var(--fg3)', margin:'4px 0 20px', lineHeight:1.6 }}>{t('twoFAHint')}</div>
<input value={otp} onChange={e=>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' }} />
<button onClick={otp.length===6?onDone:undefined} disabled={otp.length!==6} style={{ width:'100%', border:'none', borderRadius:'var(--radius-full)', padding:'13px', marginTop:16, fontFamily:'var(--font-sans)', fontWeight:700, fontSize:15, cursor:otp.length===6?'pointer':'not-allowed', background:'var(--accent)', color:'var(--fg-on-accent)', opacity:otp.length===6?1:0.45 }}>{t('twoFAConfirm')}</button>
<div onClick={()=>setStep('cred')} style={{ textAlign:'center', fontSize:12.5, color:'var(--fg3)', marginTop:18, cursor:'pointer' }}>{t('twoFABack')}</div>
</>)}
</div>
</div>
);
}
/* ───────── 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 (
<div style={{ display:'flex', flexDirection:'column', gap:20 }}>
<div>
<div style={{ fontFamily:'var(--font-display)', fontSize:25, fontWeight:700, color:'var(--fg1)' }}>{t('greeting')}</div>
<div style={{ fontSize:13.5, color:'var(--fg3)', marginTop:3 }}>me@pangolin.vpn</div>
</div>
{/* plan banner */}
<div style={{ ...ucCard, background:'linear-gradient(150deg,var(--clay-600),var(--clay-800))', border:'none', color:'#fff', padding:'20px 24px', display:'flex', alignItems:'center', justifyContent:'space-between', gap:16, flexWrap:'wrap' }}>
<div style={{ display:'flex', alignItems:'center', gap:14 }}>
<div style={{ width:46, height:46, borderRadius:'50%', background:'rgba(255,255,255,.18)', display:'flex', alignItems:'center', justifyContent:'center' }}><UCIcon name="crown" size={23} color="#fff" /></div>
<div>
<div style={{ fontSize:12, opacity:.8 }}>{t('curPlan')}</div>
<div style={{ fontFamily:'var(--font-display)', fontSize:19, fontWeight:700, marginTop:2 }}>{free?t('freePlan'):t('proMember')}</div>
<div style={{ fontSize:11.5, opacity:.8, marginTop:3, fontFamily:'var(--font-mono)', whiteSpace:'nowrap' }}>{free?t('quotaFree'):`${t('expires')} 2026-12-31`}</div>
</div>
</div>
<button onClick={goRedeem} style={{ border:'none', background:'#fff', color:'var(--clay-700)', fontWeight:700, fontSize:13.5, padding:'11px 20px', borderRadius:'var(--radius-full)', cursor:'pointer', display:'inline-flex', alignItems:'center', gap:7 }}><UCIcon name="ticket" size={15} color="var(--clay-700)" />{t('renew')}</button>
</div>
{/* stats */}
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit,minmax(160px,1fr))', gap:14 }}>
{[['clock',t('quotaToday'), free?'6':'∞', free?'min':''],['arrow-down',t('dataToday'),'0.8','GB'],['smartphone',t('devices'),'2 / 5','']].map(([ic,l,v,u])=>(
<div key={l} style={{ ...ucCard, padding:'16px 18px' }}>
<div style={{ display:'flex', alignItems:'center', gap:7, color:'var(--fg3)', fontSize:12, fontWeight:600 }}><UCIcon name={ic} size={15} color="var(--accent)" />{l}</div>
<div style={{ fontFamily:'var(--font-mono)', fontSize:23, fontWeight:500, color:'var(--fg1)', marginTop:7 }}>{v}{u&&<span style={{ fontSize:12, color:'var(--fg3)' }}> {u}</span>}</div>
</div>
))}
</div>
{/* usage chart + quick actions */}
<div style={{ display:'grid', gridTemplateColumns:mobile?'1fr':'1.6fr 1fr', gap:14, alignItems:'start' }}>
<div style={{ ...ucCard, padding:'18px 22px' }}>
<div style={{ fontSize:13.5, fontWeight:600, color:'var(--fg1)', marginBottom:16 }}>{t('usageTitle')}</div>
<div style={{ display:'flex', alignItems:'flex-end', gap:14, height:110 }}>
{vals.map((v,i)=>(
<div key={i} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', gap:6 }}>
<div style={{ fontFamily:'var(--font-mono)', fontSize:9.5, color:'var(--fg3)' }}>{v}</div>
<div style={{ width:'100%', maxWidth:30, height:`${v/max*76}px`, background:'var(--accent)', borderRadius:'5px 5px 0 0', opacity:.85 }}></div>
<div style={{ fontSize:10.5, color:'var(--fg3)' }}>{labels[i]}</div>
</div>
))}
</div>
</div>
<div style={{ ...ucCard, padding:'16px 18px' }}>
<div style={{ fontSize:13.5, fontWeight:600, color:'var(--fg1)', marginBottom:12 }}>{t('quickSub')}</div>
<div style={{ display:'flex', flexDirection:'column', gap:8 }}>
{[['link',t('qaSub'),goSub],['ticket',t('qaRedeem'),goRedeem],['download',t('qaApp'),null]].map(([ic,l,fn])=>(
<button key={l} onClick={fn||undefined} style={{ display:'flex', alignItems:'center', gap:11, width:'100%', textAlign:'left', cursor:'pointer', background:'var(--bg-subtle)', border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:'11px 13px' }}>
<UCIcon name={ic} size={16} color="var(--accent)" />
<span style={{ flex:1, fontSize:13.5, fontWeight:600, color:'var(--fg1)' }}>{l}</span>
<UCIcon name="chevron-right" size={15} color="var(--fg3)" />
</button>
))}
</div>
</div>
</div>
</div>
);
}
/* ───────── 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 (
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth:720 }}>
<div>
<div style={{ fontFamily:'var(--font-display)', fontSize:25, fontWeight:700, color:'var(--fg1)' }}>{t('subTitle')}</div>
<div style={{ fontSize:13, color:'var(--fg3)', marginTop:5, lineHeight:1.6 }}>{t('subDesc')}</div>
</div>
<div style={{ display:'grid', gridTemplateColumns:mobile?'1fr':'1fr auto', gap:14, alignItems:'start' }}>
{/* link card */}
<div style={{ ...ucCard, padding:'18px 20px' }}>
<div style={{ display:'flex', alignItems:'center', gap:9, background:'var(--bg-subtle)', border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:'11px 13px', marginBottom:12 }}>
<UCIcon name="link" size={15} color="var(--fg3)" />
<span style={{ flex:1, fontFamily:'var(--font-mono)', fontSize:12.5, color:'var(--fg1)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{url}</span>
</div>
<div style={{ display:'flex', gap:10, flexWrap:'wrap' }}>
<button onClick={()=>{setCopied(true);setTimeout(()=>setCopied(false),1800);}} style={{ display:'inline-flex', alignItems:'center', gap:7, border:'none', borderRadius:'var(--radius-full)', padding:'10px 18px', background:'var(--accent)', color:'#fff', fontWeight:700, fontSize:13.5, cursor:'pointer' }}>
<UCIcon name={copied?'check':'copy'} size={15} color="#fff" />{copied?t('subCopied'):t('subCopy')}
</button>
<button onClick={()=>setReset(true)} style={{ display:'inline-flex', alignItems:'center', gap:7, border:'1.5px solid var(--border-strong)', borderRadius:'var(--radius-full)', padding:'10px 18px', background:'transparent', color:'var(--fg2)', fontWeight:600, fontSize:13.5, cursor:'pointer' }}>
<UCIcon name="refresh-cw" size={14} color="var(--fg2)" />{t('subReset')}
</button>
</div>
<div style={{ fontSize:11.5, color:reset?'var(--success)':'var(--fg3)', marginTop:10, display:'flex', alignItems:'center', gap:6 }}>
{reset&&<UCIcon name="check-circle" size={13} color="var(--success)" />}{reset?t('subResetOk'):t('subResetSub')}
</div>
</div>
{/* QR */}
<div style={{ ...ucCard, padding:'16px', width:mobile?'100%':170, boxSizing:'border-box', textAlign:'center' }}>
<div style={{ width:138, height:138, margin:'0 auto', borderRadius:'var(--radius-md)', background:'var(--bg-subtle)', border:'1px dashed var(--border-strong)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:8 }}>
<UCIcon name="qr-code" size={42} color="var(--fg3)" stroke={1.5} />
<span style={{ fontSize:10.5, color:'var(--fg3)', fontWeight:600 }}>{t('scanTitle')}</span>
</div>
<div style={{ fontSize:10.5, color:'var(--fg3)', marginTop:10, lineHeight:1.5 }}>{t('scanSub')}</div>
</div>
</div>
{/* one-tap import */}
<div style={{ ...ucCard, padding:'18px 20px' }}>
<div style={{ fontSize:14.5, fontWeight:700, color:'var(--fg1)' }}>{t('importTitle')}</div>
<div style={{ fontSize:12.5, color:'var(--fg3)', margin:'4px 0 14px' }}>{t('importSub')}</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(190px,1fr))', gap:10 }}>
{clients.map(c=>(
<button key={c.name} style={{ display:'flex', alignItems:'center', gap:11, textAlign:'left', cursor:'pointer',
background:c.accent?'var(--accent-subtle)':'var(--surface)', border:`1px solid ${c.accent?'var(--accent-border)':'var(--border)'}`, borderRadius:'var(--radius-lg)', padding:'12px 14px' }}>
<div style={{ width:34, height:34, borderRadius:'var(--radius-md)', background:c.accent?'var(--accent)':'var(--bg-subtle)', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}><UCIcon name={c.icon} size={17} color={c.accent?'#fff':'var(--accent)'} /></div>
<div style={{ minWidth:0 }}>
<div style={{ fontSize:13.5, fontWeight:600, color:'var(--fg1)', whiteSpace:'nowrap' }}>{c.name}</div>
<div style={{ fontSize:11, color:'var(--fg3)' }}>{c.sub}</div>
</div>
</button>
))}
</div>
<div style={{ fontSize:11.5, color:'var(--fg3)', marginTop:12 }}>{t('fmtNote')}</div>
</div>
</div>
);
}
/* ───────── 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 (
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth:680 }}>
<div style={{ fontFamily:'var(--font-display)', fontSize:25, fontWeight:700, color:'var(--fg1)' }}>{t('navRedeem')}</div>
<div style={{ ...ucCard, padding:'20px 22px' }}>
<div style={{ fontSize:14.5, fontWeight:700, color:'var(--fg1)', marginBottom:12 }}>{t('redeemTitle')}</div>
{ok ? (
<div style={{ display:'flex', alignItems:'center', gap:9, color:'var(--success)', fontWeight:600, fontSize:15, padding:'4px 0' }}><UCIcon name="check-circle" size={19} color="var(--success)" />{t('redeemOk')}</div>
) : (
<div style={{ display:'flex', gap:10 }}>
<input value={code} onChange={e=>setCode(e.target.value.toUpperCase())} placeholder={t('redeemPh')} style={{ ...ucInput, fontFamily:'var(--font-mono)', letterSpacing:'0.08em' }} />
<button onClick={()=>code.length>=4&&setOk(true)} style={{ border:'none', borderRadius:'var(--radius-md)', padding:'0 22px', background:'var(--accent)', color:'#fff', fontWeight:700, fontSize:14, cursor:'pointer', flexShrink:0 }}>{t('redeemBtn')}</button>
</div>
)}
</div>
<div>
<div style={{ fontSize:14.5, fontWeight:700, color:'var(--fg1)', marginBottom:5 }}>{t('buyTitle')}</div>
<div style={{ fontSize:12.5, color:'var(--fg3)', lineHeight:1.5, marginBottom:13 }}>{t('buySub')}</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit,minmax(240px,1fr))', gap:11 }}>
{channels.map(c=>(
<button key={c.name} style={{ display:'flex', alignItems:'center', gap:12, textAlign:'left', cursor:'pointer',
background:c.accent?'var(--accent-subtle)':'var(--surface)', border:`1px solid ${c.accent?'var(--accent-border)':'var(--border)'}`, borderRadius:'var(--radius-lg)', padding:'13px 15px', boxShadow:'var(--shadow-sm)' }}>
<div style={{ width:38, height:38, borderRadius:'var(--radius-md)', background:c.accent?'var(--accent)':'var(--bg-subtle)', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}><UCIcon name={c.icon} size={18} color={c.accent?'#fff':'var(--accent)'} /></div>
<div style={{ flex:1, minWidth:0 }}>
<div style={{ fontSize:14, fontWeight:600, color:'var(--fg1)' }}>{c.name}</div>
<div style={{ fontSize:11.5, color:'var(--fg3)', fontFamily:c.mono?'var(--font-mono)':'var(--font-sans)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.sub}</div>
</div>
<UCIcon name="external-link" size={15} color="var(--fg3)" />
</button>
))}
</div>
</div>
</div>
);
}
/* ───────── 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 (
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth:680 }}>
<div>
<div style={{ fontFamily:'var(--font-display)', fontSize:25, fontWeight:700, color:'var(--fg1)' }}>{t('inviteTitle')}</div>
<div style={{ fontSize:13, color:'var(--fg3)', marginTop:5, lineHeight:1.6 }}>{t('inviteSub')}</div>
</div>
<div style={{ ...ucCard, padding:'18px 20px' }}>
<div style={{ fontSize:12.5, fontWeight:600, color:'var(--fg2)', marginBottom:8 }}>{t('inviteLink')}</div>
<div style={{ display:'flex', gap:10 }}>
<div style={{ flex:1, display:'flex', alignItems:'center', gap:9, background:'var(--bg-subtle)', border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:'11px 13px', minWidth:0 }}>
<UCIcon name="gift" size={15} color="var(--fg3)" />
<span style={{ fontFamily:'var(--font-mono)', fontSize:12.5, color:'var(--fg1)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>https://pangolin.vpn/r/PG8F3K</span>
</div>
<button onClick={()=>{setCopied(true);setTimeout(()=>setCopied(false),1800);}} style={{ display:'inline-flex', alignItems:'center', gap:7, border:'none', borderRadius:'var(--radius-md)', padding:'0 18px', background:'var(--accent)', color:'#fff', fontWeight:700, fontSize:13.5, cursor:'pointer', flexShrink:0 }}>
<UCIcon name={copied?'check':'copy'} size={15} color="#fff" />{copied?t('subCopied'):t('subCopy')}
</button>
</div>
</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit,minmax(150px,1fr))', gap:14 }}>
{[[t('invited'),'8',''],[t('paidUsers'),'3',''],[t('earned'),'¥45','']].map(([l,v])=>(
<div key={l} style={{ ...ucCard, padding:'16px 18px' }}>
<div style={{ fontSize:12, color:'var(--fg3)', fontWeight:600 }}>{l}</div>
<div style={{ fontFamily:'var(--font-mono)', fontSize:23, fontWeight:500, color:'var(--fg1)', marginTop:7 }}>{v}</div>
</div>
))}
</div>
<div style={{ ...ucCard, overflow:'hidden' }}>
<div style={{ padding:'13px 18px', fontSize:13.5, fontWeight:700, color:'var(--fg1)', borderBottom:'1px solid var(--border)' }}>{t('inviteRecord')}</div>
{records.map((r,i)=>(
<div key={i} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 18px', borderBottom:i<records.length-1?'1px solid var(--border)':'none' }}>
<span style={{ flex:1, fontFamily:'var(--font-mono)', fontSize:13, color:'var(--fg1)' }}>{r.mail}</span>
<span style={{ fontFamily:'var(--font-mono)', fontSize:12, color:'var(--fg3)' }}>{r.date}</span>
<span style={{ fontSize:11, fontWeight:600, padding:'3px 10px', borderRadius:999, background:r.paid?'var(--success-subtle)':'var(--bg-subtle)', color:r.paid?'var(--success)':'var(--fg3)' }}>{r.paid?t('recPaid'):t('recRegistered')}</span>
</div>
))}
</div>
</div>
);
}
/* 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:b<a?-1:0); setView(id); setTimeout(()=>setDir(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 <UCLogin t={t} lang={lang} setLang={setLang} onDone={()=>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=<UCOverview t={t} lang={lang} free={free} mobile={mobile} goSub={()=>setView('sub')} goRedeem={()=>setView('redeem')} />;
else if(view==='sub') main=<UCSub t={t} lang={lang} mobile={mobile} />;
else if(view==='redeem') main=<UCRedeem t={t} lang={lang} />;
else main=<UCInvite t={t} lang={lang} />;
const navBtns = nav.map(([id,ic,l])=>(
<button key={id} onClick={()=>go(id)} style={{ display:'inline-flex', alignItems:'center', gap:7, border:'none', cursor:'pointer', borderRadius:'var(--radius-md)', padding:'8px 13px', flexShrink:0,
background:view===id?'var(--accent-subtle)':'transparent', color:view===id?'var(--accent)':'var(--fg2)', fontFamily:'var(--font-sans)', fontSize:13.5, fontWeight:view===id?700:500, whiteSpace:'nowrap' }}>
<UCIcon name={ic} size={15} color={view===id?'var(--accent)':'var(--fg3)'} />{l}
</button>
));
return (
<div style={{ minHeight:'100vh', background:'var(--bg)', fontFamily:'var(--font-sans)' }}>
{/* top bar */}
<div style={{ position:'sticky', top:0, zIndex:10, background:'color-mix(in srgb, var(--bg) 85%, transparent)', backdropFilter:'blur(12px)', borderBottom:'1px solid var(--border)' }}>
<div style={{ maxWidth:1000, margin:'0 auto', padding:mobile?'0 16px':'0 24px', height:mobile?54:60, display:'flex', alignItems:'center', gap:mobile?12:22 }}>
<div style={{ display:'flex', alignItems:'center', gap:9 }}>
<UCMark size={26} />
<span style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:16.5, color:'var(--fg1)' }}>穿山甲</span>
</div>
{!mobile && <nav style={{ display:'flex', gap:4, flex:1 }}>{navBtns}</nav>}
{mobile && <div style={{ flex:1 }} />}
<UCLang lang={lang} setLang={setLang} />
<button onClick={()=>setStage('login')} style={{ display:'inline-flex', alignItems:'center', gap:7, border:'none', background:'transparent', cursor:'pointer', color:'var(--fg2)', fontSize:13, fontWeight:600, padding:6 }}>
<UCIcon name="log-out" size={15} color="var(--fg3)" />{!mobile && t('signOut')}
</button>
</div>
{mobile && <nav style={{ display:'flex', gap:2, padding:'0 12px 8px', overflowX:'auto' }}>{navBtns}</nav>}
</div>
<div
onTouchStart={mobile?onTouchStart:undefined}
onTouchEnd={mobile?onTouchEnd:undefined}
style={{ maxWidth:1000, margin:'0 auto', padding:mobile?'20px 16px 40px':'30px 24px 48px' }}>
<div key={view} onAnimationEnd={()=>setDir(0)} style={{ animation: dir!==0 ? `uc-in-${dir===1?'l':'r'} 200ms var(--ease-out)` : 'none' }}>{main}</div>
</div>
</div>
);
}
Object.assign(window, { UserCenterApp });