设置与安全弹窗添加滚动区域
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-18 11:48:14 +00:00
parent 8c3fbd9bcf
commit 7f2860bc43

View File

@@ -117,11 +117,15 @@ export function AccountModal({
return (
<div
className="fixed inset-0 z-[70] flex items-end justify-center bg-black/62 px-4 py-4 sm:items-center"
className="fixed inset-0 z-[70] flex items-end justify-center overflow-y-auto bg-black/62 px-4 sm:items-center"
style={{
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 1rem)',
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 1rem)',
}}
onClick={onClose}
>
<div
className="w-full max-w-md rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,_rgba(20,23,31,0.96),_rgba(10,12,18,0.98))] p-5 shadow-[0_24px_80px_rgba(0,0,0,0.58)]"
className="flex max-h-full w-full max-w-md flex-col overflow-hidden rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,_rgba(20,23,31,0.96),_rgba(10,12,18,0.98))] p-5 shadow-[0_24px_80px_rgba(0,0,0,0.58)]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-4">
@@ -142,336 +146,338 @@ export function AccountModal({
</button>
</div>
<div className="mt-5 grid gap-3 text-sm text-zinc-200">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{resolveLoginMethodLabel(user.loginMethod)}
</div>
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.phoneNumberMasked || '未绑定'}
</div>
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.wechatBound ? '已绑定' : '未绑定'}
</div>
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.bindingStatus === 'pending_bind_phone'
? ' 待绑定手机号'
: ' 已激活'}
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
<div className="mt-5 min-h-0 flex-1 overflow-y-auto pr-1">
<div className="grid gap-3 text-sm text-zinc-200">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{resolveLoginMethodLabel(user.loginMethod)}
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void onRefreshRiskBlocks();
}}
>
</button>
</div>
<div className="mt-3 grid gap-3">
{loadingRiskBlocks ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
) : riskBlocks.length > 0 ? (
riskBlocks.map((block) => (
<div
key={`${block.scopeType}:${block.expiresAt}`}
className="rounded-2xl border border-amber-300/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-50"
>
<div className="flex items-center justify-between gap-3">
<span>{block.title}</span>
<span className="text-xs text-amber-100/75">
{Math.max(1, Math.ceil(block.remainingSeconds / 60))}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-amber-100/85">
{block.detail}
</div>
<button
type="button"
className="mt-3 h-9 rounded-2xl border border-emerald-300/20 px-3 text-xs text-emerald-50 transition hover:border-emerald-300/45 hover:bg-emerald-400/10"
onClick={() => {
void onLiftRiskBlock(block.scopeType);
}}
>
</button>
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.phoneNumberMasked || '未绑定'}
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void onRefreshSessions();
}}
>
</button>
</div>
<div className="mt-3 grid gap-3">
{loadingSessions ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
) : sessions.length > 0 ? (
sessions.map((session) => (
<div
key={session.sessionId}
className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-200"
>
<div className="flex items-center justify-between gap-3">
<span>{session.clientLabel}</span>
<span className="text-xs text-emerald-200/85">
{session.isCurrent ? '当前设备' : '已登录'}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-zinc-400">
{formatSessionTime(session.lastSeenAt)}
</div>
<div className="text-xs leading-5 text-zinc-500">
{formatSessionTime(session.expiresAt)}
</div>
{session.ipMasked ? (
<div className="text-xs leading-5 text-zinc-500">
IP{session.ipMasked}
</div>
) : null}
{!session.isCurrent ? (
<button
type="button"
className="mt-3 h-9 rounded-2xl border border-rose-400/20 px-3 text-xs text-rose-100 transition hover:border-rose-400/45 hover:bg-rose-500/10"
onClick={() => {
void onRevokeSession(session.sessionId);
}}
>
线
</button>
) : null}
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.wechatBound ? '已绑定' : '未绑定'}
</div>
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
{user.bindingStatus === 'pending_bind_phone'
? ' 待绑定手机号'
: ' 已激活'}
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
setEditingPhone((current) => !current);
setChangePhoneError('');
setChangePhoneHint('');
}}
>
{editingPhone ? '收起' : '修改'}
</button>
</div>
{editingPhone ? (
<div className="mt-3 grid gap-3 rounded-2xl border border-white/8 bg-white/5 px-4 py-4">
<label className="grid gap-2 text-sm text-zinc-200">
<span></span>
<input
className="h-11 rounded-2xl border border-white/10 bg-black/30 px-4 text-base text-zinc-100 outline-none transition focus:border-amber-300/40 focus:bg-black/40"
value={phone}
inputMode="numeric"
placeholder="13800000000"
onChange={(event) => setPhone(event.target.value)}
/>
</label>
<label className="grid gap-2 text-sm text-zinc-200">
<span></span>
<div className="flex gap-3">
<input
className="h-11 min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/30 px-4 text-base text-zinc-100 outline-none transition focus:border-amber-300/40 focus:bg-black/40"
value={code}
inputMode="numeric"
placeholder="输入验证码"
onChange={(event) => setCode(event.target.value)}
/>
<button
type="button"
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
className="h-11 shrink-0 rounded-2xl border border-amber-300/25 px-4 text-sm font-medium text-amber-100 transition hover:border-amber-300/55 hover:bg-amber-300/10 disabled:cursor-not-allowed disabled:opacity-55"
onClick={() => {
void (async () => {
setSendingCode(true);
setChangePhoneError('');
try {
const result = await onSendChangePhoneCode(phone, {
challengeId: changePhoneCaptchaChallenge?.challengeId,
answer: captchaAnswer,
});
setCooldownSeconds(result.cooldownSeconds);
setChangePhoneHint(
`验证码已发送,有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
);
setCaptchaAnswer('');
} catch (error) {
setChangePhoneError(
error instanceof Error
? error.message
: '发送验证码失败,请稍后再试。',
);
setChangePhoneHint('');
} finally {
setSendingCode(false);
}
})();
}}
>
{sendingCode
? '发送中...'
: cooldownSeconds > 0
? `${cooldownSeconds}s`
: '获取验证码'}
</button>
</div>
</label>
{changePhoneHint ? (
<div className="rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100">
{changePhoneHint}
</div>
) : null}
<CaptchaChallengeField
challenge={changePhoneCaptchaChallenge}
answer={captchaAnswer}
onAnswerChange={setCaptchaAnswer}
/>
{changePhoneError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">
{changePhoneError}
</div>
) : null}
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
</div>
<button
type="button"
disabled={changingPhone || !phone.trim() || !code.trim()}
className="h-11 rounded-2xl border border-sky-300/20 px-4 text-sm font-medium text-sky-100 transition hover:border-sky-300/45 hover:bg-sky-500/10 disabled:cursor-not-allowed disabled:opacity-60"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void (async () => {
setChangingPhone(true);
setChangePhoneError('');
try {
await onChangePhone(phone, code);
setChangePhoneHint('手机号已更新。');
setPhone('');
setCode('');
} catch (error) {
setChangePhoneError(
error instanceof Error
? error.message
: '更换手机号失败,请稍后再试。',
);
} finally {
setChangingPhone(false);
}
})();
void onRefreshRiskBlocks();
}}
>
{changingPhone ? '提交中...' : '确认更换手机号'}
</button>
</div>
) : null}
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void onRefreshAuditLogs();
}}
>
</button>
</div>
<div className="mt-3 grid gap-3">
{loadingAuditLogs ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
) : auditLogs.length > 0 ? (
auditLogs.map((log) => (
<div
key={log.id}
className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-200"
>
<div className="flex items-center justify-between gap-3">
<span>{log.title}</span>
<span className="text-xs text-zinc-500">
{formatSessionTime(log.createdAt)}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-zinc-400">
{log.detail}
</div>
{log.ipMasked ? (
<div className="text-xs leading-5 text-zinc-500">
IP{log.ipMasked}
</div>
) : null}
<div className="mt-3 grid gap-3">
{loadingRiskBlocks ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
) : riskBlocks.length > 0 ? (
riskBlocks.map((block) => (
<div
key={`${block.scopeType}:${block.expiresAt}`}
className="rounded-2xl border border-amber-300/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-50"
>
<div className="flex items-center justify-between gap-3">
<span>{block.title}</span>
<span className="text-xs text-amber-100/75">
{Math.max(1, Math.ceil(block.remainingSeconds / 60))}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-amber-100/85">
{block.detail}
</div>
<button
type="button"
className="mt-3 h-9 rounded-2xl border border-emerald-300/20 px-3 text-xs text-emerald-50 transition hover:border-emerald-300/45 hover:bg-emerald-400/10"
onClick={() => {
void onLiftRiskBlock(block.scopeType);
}}
>
</button>
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void onRefreshSessions();
}}
>
</button>
</div>
<div className="mt-3 grid gap-3">
{loadingSessions ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
) : sessions.length > 0 ? (
sessions.map((session) => (
<div
key={session.sessionId}
className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-200"
>
<div className="flex items-center justify-between gap-3">
<span>{session.clientLabel}</span>
<span className="text-xs text-emerald-200/85">
{session.isCurrent ? '当前设备' : '已登录'}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-zinc-400">
{formatSessionTime(session.lastSeenAt)}
</div>
<div className="text-xs leading-5 text-zinc-500">
{formatSessionTime(session.expiresAt)}
</div>
{session.ipMasked ? (
<div className="text-xs leading-5 text-zinc-500">
IP{session.ipMasked}
</div>
) : null}
{!session.isCurrent ? (
<button
type="button"
className="mt-3 h-9 rounded-2xl border border-rose-400/20 px-3 text-xs text-rose-100 transition hover:border-rose-400/45 hover:bg-rose-500/10"
onClick={() => {
void onRevokeSession(session.sessionId);
}}
>
线
</button>
) : null}
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
setEditingPhone((current) => !current);
setChangePhoneError('');
setChangePhoneHint('');
}}
>
{editingPhone ? '收起' : '修改'}
</button>
</div>
{editingPhone ? (
<div className="mt-3 grid gap-3 rounded-2xl border border-white/8 bg-white/5 px-4 py-4">
<label className="grid gap-2 text-sm text-zinc-200">
<span></span>
<input
className="h-11 rounded-2xl border border-white/10 bg-black/30 px-4 text-base text-zinc-100 outline-none transition focus:border-amber-300/40 focus:bg-black/40"
value={phone}
inputMode="numeric"
placeholder="13800000000"
onChange={(event) => setPhone(event.target.value)}
/>
</label>
<label className="grid gap-2 text-sm text-zinc-200">
<span></span>
<div className="flex gap-3">
<input
className="h-11 min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/30 px-4 text-base text-zinc-100 outline-none transition focus:border-amber-300/40 focus:bg-black/40"
value={code}
inputMode="numeric"
placeholder="输入验证码"
onChange={(event) => setCode(event.target.value)}
/>
<button
type="button"
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
className="h-11 shrink-0 rounded-2xl border border-amber-300/25 px-4 text-sm font-medium text-amber-100 transition hover:border-amber-300/55 hover:bg-amber-300/10 disabled:cursor-not-allowed disabled:opacity-55"
onClick={() => {
void (async () => {
setSendingCode(true);
setChangePhoneError('');
try {
const result = await onSendChangePhoneCode(phone, {
challengeId: changePhoneCaptchaChallenge?.challengeId,
answer: captchaAnswer,
});
setCooldownSeconds(result.cooldownSeconds);
setChangePhoneHint(
`验证码已发送,有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
);
setCaptchaAnswer('');
} catch (error) {
setChangePhoneError(
error instanceof Error
? error.message
: '发送验证码失败,请稍后再试。',
);
setChangePhoneHint('');
} finally {
setSendingCode(false);
}
})();
}}
>
{sendingCode
? '发送中...'
: cooldownSeconds > 0
? `${cooldownSeconds}s`
: '获取验证码'}
</button>
</div>
</label>
{changePhoneHint ? (
<div className="rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100">
{changePhoneHint}
</div>
) : null}
<CaptchaChallengeField
challenge={changePhoneCaptchaChallenge}
answer={captchaAnswer}
onAnswerChange={setCaptchaAnswer}
/>
{changePhoneError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">
{changePhoneError}
</div>
) : null}
<button
type="button"
disabled={changingPhone || !phone.trim() || !code.trim()}
className="h-11 rounded-2xl border border-sky-300/20 px-4 text-sm font-medium text-sky-100 transition hover:border-sky-300/45 hover:bg-sky-500/10 disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => {
void (async () => {
setChangingPhone(true);
setChangePhoneError('');
try {
await onChangePhone(phone, code);
setChangePhoneHint('手机号已更新。');
setPhone('');
setCode('');
} catch (error) {
setChangePhoneError(
error instanceof Error
? error.message
: '更换手机号失败,请稍后再试。',
);
} finally {
setChangingPhone(false);
}
})();
}}
>
{changingPhone ? '提交中...' : '确认更换手机号'}
</button>
</div>
) : null}
</div>
<div className="mt-5">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-[0.24em] text-zinc-400">
</div>
<button
type="button"
className="rounded-full border border-white/10 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white"
onClick={() => {
void onRefreshAuditLogs();
}}
>
</button>
</div>
<div className="mt-3 grid gap-3">
{loadingAuditLogs ? (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
...
</div>
) : auditLogs.length > 0 ? (
auditLogs.map((log) => (
<div
key={log.id}
className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-200"
>
<div className="flex items-center justify-between gap-3">
<span>{log.title}</span>
<span className="text-xs text-zinc-500">
{formatSessionTime(log.createdAt)}
</span>
</div>
<div className="mt-2 text-xs leading-5 text-zinc-400">
{log.detail}
</div>
{log.ipMasked ? (
<div className="text-xs leading-5 text-zinc-500">
IP{log.ipMasked}
</div>
) : null}
</div>
))
) : (
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-zinc-400">
</div>
)}
</div>
</div>
<button
type="button"
className="mt-5 h-11 w-full rounded-2xl border border-amber-300/25 px-4 text-sm font-medium text-amber-100 transition hover:border-amber-300/55 hover:bg-amber-300/10"
onClick={() => {
void onLogout();
}}
>
退
</button>
<button
type="button"
className="mt-3 h-11 w-full rounded-2xl border border-rose-400/20 px-4 text-sm font-medium text-rose-100 transition hover:border-rose-400/45 hover:bg-rose-500/10"
onClick={() => {
void onLogoutAll();
}}
>
退
</button>
</div>
<button
type="button"
className="mt-5 h-11 w-full rounded-2xl border border-amber-300/25 px-4 text-sm font-medium text-amber-100 transition hover:border-amber-300/55 hover:bg-amber-300/10"
onClick={() => {
void onLogout();
}}
>
退
</button>
<button
type="button"
className="mt-3 h-11 w-full rounded-2xl border border-rose-400/20 px-4 text-sm font-medium text-rose-100 transition hover:border-rose-400/45 hover:bg-rose-500/10"
onClick={() => {
void onLogoutAll();
}}
>
退
</button>
</div>
</div>
);