feat(web/usercenter): Next.js 用户中心静态导出 + mock/http 双数据层 (tsk_3FIPC8lSnAfJ)
新建 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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// i18n.ts — 穿山甲 Web 用户中心 · 双语字串(单显,绝不并排)
|
||||
// 直接承袭 design/ui_kits/usercenter/ucparts.jsx 的 UCSTRINGS,并补充设置/设备/TOTP 键。
|
||||
|
||||
export type Lang = 'zh' | 'en';
|
||||
|
||||
type Entry = { zh: string; en: string };
|
||||
|
||||
export const STRINGS: Record<string, Entry> = {
|
||||
navOverview: { zh: '概览', en: 'Overview' },
|
||||
navSub: { zh: '订阅', en: 'Subscription' },
|
||||
navRedeem: { zh: '兑换 & 购买', en: 'Redeem & buy' },
|
||||
navInvite: { zh: '邀请返利', en: 'Referral' },
|
||||
navSettings: { zh: '设置', en: 'Settings' },
|
||||
signOut: { zh: '退出', en: 'Sign out' },
|
||||
|
||||
/* login */
|
||||
loginTitle: { zh: '登录用户中心', en: 'Log in to your account' },
|
||||
loginSub: { zh: '管理订阅、兑换激活码、查看用量', en: 'Manage subscription, redeem codes, track usage' },
|
||||
emailLabel: { zh: '邮箱', en: 'Email' },
|
||||
emailPh: { zh: '你的邮箱地址', en: 'your@email.com' },
|
||||
pwLabel: { zh: '密码', en: 'Password' },
|
||||
pwPh: { zh: '输入密码', en: 'Enter password' },
|
||||
doLogin: { zh: '登录', en: 'Log in' },
|
||||
forgotPw: { zh: '忘记密码?', en: 'Forgot password?' },
|
||||
noAccount: { zh: '没有账户?在 App 内注册', en: 'No account? Sign up in the app' },
|
||||
loginFailed: { zh: '邮箱或密码错误', en: 'Wrong email or password' },
|
||||
loginLocked: { zh: '失败次数过多,账户已临时锁定', en: 'Too many attempts — account temporarily locked' },
|
||||
lockedCountdown: { zh: '请于 {s} 秒后重试', en: 'Try again in {s}s' },
|
||||
|
||||
/* overview */
|
||||
greeting: { zh: '欢迎回来', en: 'Welcome back' },
|
||||
curPlan: { zh: '当前套餐', en: 'Current plan' },
|
||||
freePlan: { zh: '免费版', en: 'Free' },
|
||||
proMember: { zh: 'PRO 会员', en: 'PRO member' },
|
||||
expires: { zh: '有效期至', en: 'Expires' },
|
||||
renew: { zh: '续费 / 升级', en: 'Renew / Upgrade' },
|
||||
quotaToday: { zh: '今日剩余时长', en: 'Time left today' },
|
||||
dataToday: { zh: '今日已用流量', en: 'Data used today' },
|
||||
devices: { zh: '在线设备', en: 'Devices online' },
|
||||
quotaFree: { zh: '免费版 · 每日 10 分钟', en: 'Free · 10 min/day' },
|
||||
usageTitle: { zh: '近 7 日流量 (GB)', en: 'Last 7 days (GB)' },
|
||||
quickSub: { zh: '快速操作', en: 'Quick actions' },
|
||||
qaSub: { zh: '获取订阅链接', en: 'Get subscription' },
|
||||
qaRedeem: { zh: '兑换激活码', en: 'Redeem a code' },
|
||||
qaApp: { zh: '下载 App', en: 'Download apps' },
|
||||
|
||||
/* subscription */
|
||||
subTitle: { zh: '我的订阅', en: 'My subscription' },
|
||||
subDesc: { zh: '订阅链接是你的专属凭证,泄露后他人可使用你的额度。请勿分享。', en: 'Your subscription link is a private credential. Never share it.' },
|
||||
subCopy: { zh: '复制链接', en: 'Copy link' },
|
||||
subCopied: { zh: '已复制', en: 'Copied' },
|
||||
subReset: { zh: '重置链接', en: 'Reset link' },
|
||||
subResetSub: { zh: '旧链接立即失效,所有设备需重新导入', en: 'Old link stops working; re-import on all devices' },
|
||||
subResetOk: { zh: '已重置,请重新导入', en: 'Reset — re-import on your devices' },
|
||||
scanTitle: { zh: '扫码导入', en: 'Scan to import' },
|
||||
scanSub: { zh: '用穿山甲 App 或三方客户端扫码', en: 'Scan with the Pangolin app or a 3rd-party client' },
|
||||
importTitle: { zh: '一键导入三方客户端', en: 'One-tap import' },
|
||||
importSub: { zh: '已安装对应客户端时,点击即自动导入订阅。', en: 'If the client is installed, tapping imports the subscription automatically.' },
|
||||
fmtNote: { zh: '同一链接同时兼容 sing-box / Clash.Meta / v2ray 格式。', en: 'One link serves sing-box / Clash.Meta / v2ray formats.' },
|
||||
|
||||
/* redeem */
|
||||
redeemTitle: { zh: '兑换激活码', en: 'Redeem a code' },
|
||||
redeemPh: { zh: '输入激活码', en: 'Enter activation code' },
|
||||
redeemBtn: { zh: '激活', en: 'Redeem' },
|
||||
redeemOk: { zh: '激活成功 · 套餐已到账', en: 'Activated — plan applied' },
|
||||
buyTitle: { zh: '购买渠道', en: 'Where to buy' },
|
||||
buySub: { zh: '本站不直接收款。通过以下渠道购买激活码,回到本页兑换即可:', en: 'We never take payment on this site. Buy a code via a channel below, then redeem here:' },
|
||||
chStore: { zh: '自助发卡商店', en: 'Self-serve store' },
|
||||
chStoreSub: { zh: '支付宝 / 微信 · 自动发码', en: 'Alipay / WeChat · instant code' },
|
||||
chUsdtSub: { zh: '链上转账 · 最隐私 · 自动发码', en: 'On-chain · most private · instant code' },
|
||||
chEmail: { zh: '邮箱客服', en: 'Email support' },
|
||||
|
||||
/* invite */
|
||||
inviteTitle: { zh: '邀请返利', en: 'Referral' },
|
||||
inviteSub: { zh: '好友通过你的链接注册并付费,你获得其首单 20% 余额返利,可抵扣续费。', en: 'When a friend signs up and pays via your link, you earn 20% of their first order as credit.' },
|
||||
inviteLink: { zh: '我的邀请链接', en: 'My invite link' },
|
||||
invited: { zh: '已邀请', en: 'Invited' },
|
||||
paidUsers: { zh: '已付费', en: 'Converted' },
|
||||
earned: { zh: '累计返利', en: 'Credit earned' },
|
||||
inviteRecord: { zh: '邀请记录', en: 'History' },
|
||||
recEmpty: { zh: '暂无邀请记录', en: 'No referrals yet' },
|
||||
recRegistered: { zh: '已注册', en: 'Registered' },
|
||||
recPaid: { zh: '已付费', en: 'Paid' },
|
||||
|
||||
/* 2FA login step */
|
||||
twoFATitle: { zh: '双重认证', en: 'Two-factor auth' },
|
||||
twoFAHint: { zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.' },
|
||||
twoFAConfirm: { zh: '验证并登录', en: 'Verify & log in' },
|
||||
twoFABack: { zh: '返回上一步', en: 'Back' },
|
||||
twoFAWrong: { zh: '动态码不正确,请重试', en: 'Invalid code, try again' },
|
||||
|
||||
/* settings */
|
||||
settingsTitle: { zh: '设置', en: 'Settings' },
|
||||
prefTitle: { zh: '偏好', en: 'Preferences' },
|
||||
prefLang: { zh: '显示语言', en: 'Language' },
|
||||
prefTheme: { zh: '外观主题', en: 'Theme' },
|
||||
themeLight: { zh: '浅色', en: 'Light' },
|
||||
themeDark: { zh: '深色', en: 'Dark' },
|
||||
|
||||
/* devices */
|
||||
devTitle: { zh: '设备管理', en: 'Devices' },
|
||||
devSub: { zh: '管理已登录本账户的设备,移除后该设备需重新登录。', en: 'Devices signed in to your account. Removing one signs it out.' },
|
||||
devCurrent: { zh: '当前设备', en: 'This device' },
|
||||
devLastActive: { zh: '最近活跃', en: 'Last active' },
|
||||
devRemove: { zh: '移除', en: 'Remove' },
|
||||
devRemoveConfirmTitle: { zh: '移除此设备?', en: 'Remove this device?' },
|
||||
devRemoveConfirmSub: { zh: '该设备将被登出,需重新输入凭证才能再次连接。', en: 'It will be signed out and must re-authenticate to connect again.' },
|
||||
devEmpty: { zh: '暂无其他设备', en: 'No devices yet' },
|
||||
cancel: { zh: '取消', en: 'Cancel' },
|
||||
confirm: { zh: '确认移除', en: 'Remove' },
|
||||
|
||||
/* TOTP management */
|
||||
totpTitle: { zh: '双重认证 (TOTP)', en: 'Two-factor auth (TOTP)' },
|
||||
totpOff: { zh: '未开启', en: 'Off' },
|
||||
totpOn: { zh: '已开启', en: 'On' },
|
||||
totpDesc: { zh: '用身份验证器 App 生成的一次性动态码保护登录,显著提升账户安全。', en: 'Protect sign-in with one-time codes from an authenticator app.' },
|
||||
totpEnable: { zh: '开启双重认证', en: 'Enable 2FA' },
|
||||
totpDisableBtn: { zh: '关闭双重认证', en: 'Disable 2FA' },
|
||||
totpStep1: { zh: '1. 用身份验证器扫码,或手动输入密钥', en: '1. Scan with an authenticator, or enter the key manually' },
|
||||
totpSecretLabel: { zh: '手动密钥', en: 'Manual key' },
|
||||
totpStep2: { zh: '2. 输入 App 显示的 6 位动态码以确认', en: '2. Enter the 6-digit code shown in the app to confirm' },
|
||||
totpVerifyBtn: { zh: '确认开启', en: 'Confirm & enable' },
|
||||
totpEnabledOk: { zh: '双重认证已开启', en: 'Two-factor auth enabled' },
|
||||
totpDisabledOk: { zh: '双重认证已关闭', en: 'Two-factor auth disabled' },
|
||||
totpDisableHint: { zh: '输入当前动态码以关闭双重认证。', en: 'Enter your current code to disable 2FA.' },
|
||||
copyKey: { zh: '复制密钥', en: 'Copy key' },
|
||||
|
||||
/* generic errors */
|
||||
errNetwork: { zh: '网络异常,请稍后重试', en: 'Network error, please retry' },
|
||||
errUnknown: { zh: '操作失败,请稍后重试', en: 'Something went wrong, please retry' },
|
||||
loading: { zh: '加载中…', en: 'Loading…' },
|
||||
};
|
||||
|
||||
export type TFn = (key: string, vars?: Record<string, string | number>) => string;
|
||||
|
||||
export function makeT(lang: Lang): TFn {
|
||||
return (key, vars) => {
|
||||
const e = STRINGS[key];
|
||||
let s = e ? e[lang] || key : key;
|
||||
if (vars) for (const k of Object.keys(vars)) s = s.replace(`{${k}}`, String(vars[k]));
|
||||
return s;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user