e2646346a6
新建 web/usercenter/:Next.js App Router + output:'export' 纯静态导出,
直接复用 design/ui_kits/usercenter React 源码(概览/订阅/兑换/邀请/设置)。
阶段 A(mock,本提交):
- 复刻五大页面,明/暗 × zh/en 四态;colors_and_type.css 原样链入;
顶栏主题切换 + 语言段控,移动端底部 Tab + 左右滑动切换。
- 设置页新增:偏好(语言/主题) + 设备管理(列表/移除/二次确认+刷新) +
TOTP 2FA(绑定二维码占位+密钥/验证/解禁),登录二段式 TOTP。
- 数据层 lib/api:ApiClient 抽象 + MockClient/HttpClient 双实现,构建期
NEXT_PUBLIC_API_MODE 切换;统一错误体 {code,message_zh,message_en} → 双语映射。
- 会话:access token 仅内存,refresh header token + localStorage,静默续期,
登出失效(取舍:静态导出无服务端 cookie 能力,详见 README)。
- 安全:构建期注入 SRI(sha384);_headers 严格 CSP + 安全基线;
红线词扫描(铁律13) CI 红线,零命中。
阶段 B(占位待联调):HttpClient 已写好域名池+退避重试+401 续期;
TOTP/登录二段式端点占位待 #1 契约增补;me/redeem/devices 切真实链路依赖 #2/#3/#4。
验证:npm run lint / redline / build 均通过,静态产物 out/ 无服务端依赖。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
3.8 KiB
TypeScript
69 lines
3.8 KiB
TypeScript
'use client';
|
|
// Invite.tsx — 邀请返利(邀请链接 + 统计 + 记录)。承袭 UCInvite。
|
|
import React, { useState } from 'react';
|
|
import { Icon } from './icons';
|
|
import { card } from './shared';
|
|
import type { TFn } from '../lib/i18n';
|
|
|
|
export default function Invite({ t }: { t: TFn }) {
|
|
const [copied, setCopied] = useState(false);
|
|
const link = 'https://pangolin.vpn/r/PG8F3K';
|
|
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 },
|
|
];
|
|
|
|
async function copy() {
|
|
try {
|
|
await navigator.clipboard?.writeText(link);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1800);
|
|
}
|
|
|
|
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={{ ...card, 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 }}>
|
|
<Icon 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' }}>{link}</span>
|
|
</div>
|
|
<button onClick={copy} 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 }}>
|
|
<Icon 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={{ ...card, 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={{ ...card, 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>
|
|
);
|
|
}
|