Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user