update: 表改动 主页改动
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
victo
2026-04-14 18:58:33 +08:00
parent 0981d6ee1b
commit 3d6f31433a
37 changed files with 2594 additions and 699 deletions

View File

@@ -1,4 +1,4 @@
import { type ReactNode, useEffect, useState } from 'react';
import { type ReactNode, useEffect, useMemo, useState } from 'react';
import {
AUTH_STATE_EVENT,
@@ -30,6 +30,7 @@ import {
startWechatLogin,
} from '../../services/authService';
import { AccountModal } from './AccountModal';
import { AuthUiContext } from './AuthUiContext';
import { BindPhoneScreen } from './BindPhoneScreen';
import { LoginScreen } from './LoginScreen';
@@ -61,6 +62,7 @@ export function AuthGate({ children }: AuthGateProps) {
const [bindingPhone, setBindingPhone] = useState(false);
const [wechatLoading, setWechatLoading] = useState(false);
const [showAccountModal, setShowAccountModal] = useState(false);
const [showGlobalAccountActions, setShowGlobalAccountActions] = useState(true);
const [sessions, setSessions] = useState<AuthSessionSummary[]>([]);
const [loadingSessions, setLoadingSessions] = useState(false);
const [auditLogs, setAuditLogs] = useState<AuthAuditLogEntry[]>([]);
@@ -304,6 +306,19 @@ export function AuthGate({ children }: AuthGateProps) {
};
}, [showAccountModal, status]);
const authUiValue = useMemo(
() => ({
user,
openAccountModal: () => setShowAccountModal(true),
logout: async () => {
await logoutAuthUser();
setShowAccountModal(false);
},
setGlobalAccountActionsVisible: setShowGlobalAccountActions,
}),
[user],
);
if (status === 'checking') {
return (
<div className="flex min-h-screen items-center justify-center bg-[#090b11] text-sm text-zinc-300">
@@ -468,140 +483,144 @@ export function AuthGate({ children }: AuthGateProps) {
}
return (
<div className="relative">
<div className="pointer-events-none fixed right-3 top-3 z-50 flex justify-end">
<div className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/10 bg-black/45 px-3 py-2 text-xs text-zinc-200 backdrop-blur">
<button
type="button"
className="rounded-full border border-white/10 px-2 py-1 text-[11px] text-zinc-100 transition hover:border-white/25 hover:text-white"
onClick={() => setShowAccountModal(true)}
>
{user.displayName}
</button>
<button
type="button"
className="rounded-full border border-white/10 px-2 py-1 text-[11px] text-zinc-100 transition hover:border-amber-300/40 hover:text-amber-100"
onClick={() => {
void logoutAuthUser();
}}
>
退
</button>
</div>
</div>
<AccountModal
user={user}
isOpen={showAccountModal}
riskBlocks={riskBlocks}
sessions={sessions}
auditLogs={auditLogs}
loadingRiskBlocks={loadingRiskBlocks}
loadingSessions={loadingSessions}
loadingAuditLogs={loadingAuditLogs}
onClose={() => setShowAccountModal(false)}
onLogout={async () => {
await logoutAuthUser();
setShowAccountModal(false);
}}
onRefreshRiskBlocks={async () => {
setLoadingRiskBlocks(true);
try {
setRiskBlocks(await getAuthRiskBlocks());
} catch (blockError) {
setError(
blockError instanceof Error
? blockError.message
: '读取安全状态失败,请稍后再试。',
);
} finally {
setLoadingRiskBlocks(false);
}
}}
onLiftRiskBlock={async (scopeType) => {
try {
await liftAuthRiskBlock(scopeType);
setRiskBlocks(await getAuthRiskBlocks());
setAuditLogs(await getAuthAuditLogs());
} catch (liftError) {
setError(
liftError instanceof Error
? liftError.message
: '解除保护失败,请稍后再试。',
);
}
}}
onRefreshSessions={async () => {
setLoadingSessions(true);
try {
setSessions(await getAuthSessions());
} catch (sessionError) {
setError(
sessionError instanceof Error
? sessionError.message
: '读取登录设备失败,请稍后再试。',
);
} finally {
setLoadingSessions(false);
}
}}
onRefreshAuditLogs={async () => {
setLoadingAuditLogs(true);
try {
setAuditLogs(await getAuthAuditLogs());
} catch (auditError) {
setError(
auditError instanceof Error
? auditError.message
: '读取账号操作记录失败,请稍后再试。',
);
} finally {
setLoadingAuditLogs(false);
}
}}
onRevokeSession={async (sessionId) => {
try {
await revokeAuthSession(sessionId);
setSessions((current) =>
current.filter((session) => session.sessionId !== sessionId),
);
setAuditLogs(await getAuthAuditLogs());
} catch (revokeError) {
setError(
revokeError instanceof Error
? revokeError.message
: '移除登录设备失败,请稍后再试。',
);
}
}}
onLogoutAll={async () => {
await logoutAllAuthSessions();
setShowAccountModal(false);
}}
changePhoneCaptchaChallenge={changePhoneCaptchaChallenge}
onSendChangePhoneCode={async (phone, captcha) => {
try {
const result = await sendPhoneLoginCode(
phone,
'change_phone',
captcha,
);
setChangePhoneCaptchaChallenge(null);
return result;
} catch (sendError) {
const captchaChallenge = getCaptchaChallengeFromError(sendError);
if (captchaChallenge) {
setChangePhoneCaptchaChallenge(captchaChallenge);
<AuthUiContext.Provider value={authUiValue}>
<div className="relative">
{showGlobalAccountActions ? (
<div className="pointer-events-none fixed right-3 top-3 z-50 flex justify-end">
<div className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/10 bg-black/45 px-3 py-2 text-xs text-zinc-200 backdrop-blur">
<button
type="button"
className="rounded-full border border-white/10 px-2 py-1 text-[11px] text-zinc-100 transition hover:border-white/25 hover:text-white"
onClick={() => setShowAccountModal(true)}
>
{user.displayName}
</button>
<button
type="button"
className="rounded-full border border-white/10 px-2 py-1 text-[11px] text-zinc-100 transition hover:border-amber-300/40 hover:text-amber-100"
onClick={() => {
void logoutAuthUser();
}}
>
退
</button>
</div>
</div>
) : null}
<AccountModal
user={user}
isOpen={showAccountModal}
riskBlocks={riskBlocks}
sessions={sessions}
auditLogs={auditLogs}
loadingRiskBlocks={loadingRiskBlocks}
loadingSessions={loadingSessions}
loadingAuditLogs={loadingAuditLogs}
onClose={() => setShowAccountModal(false)}
onLogout={async () => {
await logoutAuthUser();
setShowAccountModal(false);
}}
onRefreshRiskBlocks={async () => {
setLoadingRiskBlocks(true);
try {
setRiskBlocks(await getAuthRiskBlocks());
} catch (blockError) {
setError(
blockError instanceof Error
? blockError.message
: '读取安全状态失败,请稍后再试。',
);
} finally {
setLoadingRiskBlocks(false);
}
throw sendError;
}
}}
onChangePhone={async (phone, code) => {
const nextUser = await changePhoneNumber(phone, code);
setChangePhoneCaptchaChallenge(null);
setUser(nextUser);
}}
/>
{children}
</div>
}}
onLiftRiskBlock={async (scopeType) => {
try {
await liftAuthRiskBlock(scopeType);
setRiskBlocks(await getAuthRiskBlocks());
setAuditLogs(await getAuthAuditLogs());
} catch (liftError) {
setError(
liftError instanceof Error
? liftError.message
: '解除保护失败,请稍后再试。',
);
}
}}
onRefreshSessions={async () => {
setLoadingSessions(true);
try {
setSessions(await getAuthSessions());
} catch (sessionError) {
setError(
sessionError instanceof Error
? sessionError.message
: '读取登录设备失败,请稍后再试。',
);
} finally {
setLoadingSessions(false);
}
}}
onRefreshAuditLogs={async () => {
setLoadingAuditLogs(true);
try {
setAuditLogs(await getAuthAuditLogs());
} catch (auditError) {
setError(
auditError instanceof Error
? auditError.message
: '读取账号操作记录失败,请稍后再试。',
);
} finally {
setLoadingAuditLogs(false);
}
}}
onRevokeSession={async (sessionId) => {
try {
await revokeAuthSession(sessionId);
setSessions((current) =>
current.filter((session) => session.sessionId !== sessionId),
);
setAuditLogs(await getAuthAuditLogs());
} catch (revokeError) {
setError(
revokeError instanceof Error
? revokeError.message
: '移除登录设备失败,请稍后再试。',
);
}
}}
onLogoutAll={async () => {
await logoutAllAuthSessions();
setShowAccountModal(false);
}}
changePhoneCaptchaChallenge={changePhoneCaptchaChallenge}
onSendChangePhoneCode={async (phone, captcha) => {
try {
const result = await sendPhoneLoginCode(
phone,
'change_phone',
captcha,
);
setChangePhoneCaptchaChallenge(null);
return result;
} catch (sendError) {
const captchaChallenge = getCaptchaChallengeFromError(sendError);
if (captchaChallenge) {
setChangePhoneCaptchaChallenge(captchaChallenge);
}
throw sendError;
}
}}
onChangePhone={async (phone, code) => {
const nextUser = await changePhoneNumber(phone, code);
setChangePhoneCaptchaChallenge(null);
setUser(nextUser);
}}
/>
{children}
</div>
</AuthUiContext.Provider>
);
}

View File

@@ -0,0 +1,16 @@
import { createContext, useContext } from 'react';
import type { AuthUser } from '../../services/authService';
type AuthUiContextValue = {
user: AuthUser | null;
openAccountModal: () => void;
logout: () => Promise<void>;
setGlobalAccountActionsVisible: (visible: boolean) => void;
};
export const AuthUiContext = createContext<AuthUiContextValue | null>(null);
export function useAuthUi() {
return useContext(AuthUiContext);
}