迁移后端认证与拆分 Spacetime 客户端

This commit is contained in:
2026-04-24 14:10:11 +08:00
parent ef53028be5
commit 4f369617c7
55 changed files with 9206 additions and 343 deletions

View File

@@ -52,6 +52,10 @@ type AccountModalProps = {
expiresInSeconds: number;
}>;
onChangePhone: (phone: string, code: string) => Promise<void>;
onChangePassword: (
currentPassword: string,
newPassword: string,
) => Promise<void>;
};
const SETTINGS_SECTIONS: Array<{
@@ -285,24 +289,31 @@ export function AccountModal({
changePhoneCaptchaChallenge,
onSendChangePhoneCode,
onChangePhone,
onChangePassword,
}: AccountModalProps) {
const [activeSection, setActiveSection] =
useState<PrimarySettingsSection | null>(
normalizeSettingsSection(initialSection),
);
const [isChangePhonePanelOpen, setIsChangePhonePanelOpen] = useState(false);
const [isPasswordPanelOpen, setIsPasswordPanelOpen] = useState(false);
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [captchaAnswer, setCaptchaAnswer] = useState('');
const [changePhoneError, setChangePhoneError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [changePhoneHint, setChangePhoneHint] = useState('');
const [accountNotice, setAccountNotice] = useState('');
const [sendingCode, setSendingCode] = useState(false);
const [changingPhone, setChangingPhone] = useState(false);
const [changingPassword, setChangingPassword] = useState(false);
const [cooldownSeconds, setCooldownSeconds] = useState(0);
const settingsHomeRef = useRef<HTMLDivElement | null>(null);
const sectionTriggerRef = useRef<HTMLButtonElement | null>(null);
const changePhoneTriggerRef = useRef<HTMLButtonElement | null>(null);
const passwordTriggerRef = useRef<HTMLButtonElement | null>(null);
const focusAfterNextPaint = useCallback((element: HTMLElement | null) => {
if (!element) {
@@ -325,6 +336,12 @@ export function AccountModal({
setCooldownSeconds(0);
}, []);
const resetPasswordDraft = useCallback(() => {
setCurrentPassword('');
setNewPassword('');
setPasswordError('');
}, []);
useEffect(() => {
if (!isOpen) {
return;
@@ -332,11 +349,14 @@ export function AccountModal({
setActiveSection(normalizeSettingsSection(initialSection));
setIsChangePhonePanelOpen(false);
setIsPasswordPanelOpen(false);
setAccountNotice('');
sectionTriggerRef.current = null;
changePhoneTriggerRef.current = null;
passwordTriggerRef.current = null;
resetChangePhoneDraft();
}, [initialSection, isOpen, resetChangePhoneDraft]);
resetPasswordDraft();
}, [initialSection, isOpen, resetChangePhoneDraft, resetPasswordDraft]);
useEffect(() => {
const settingsHome = settingsHomeRef.current;
@@ -368,10 +388,12 @@ export function AccountModal({
const closeSectionPanel = useCallback(() => {
const sectionTrigger = sectionTriggerRef.current;
setIsChangePhonePanelOpen(false);
setIsPasswordPanelOpen(false);
setActiveSection(null);
resetChangePhoneDraft();
resetPasswordDraft();
focusAfterNextPaint(sectionTrigger);
}, [focusAfterNextPaint, resetChangePhoneDraft]);
}, [focusAfterNextPaint, resetChangePhoneDraft, resetPasswordDraft]);
const closeChangePhonePanel = useCallback(() => {
const changePhoneTrigger = changePhoneTriggerRef.current;
@@ -380,6 +402,13 @@ export function AccountModal({
focusAfterNextPaint(changePhoneTrigger);
}, [focusAfterNextPaint, resetChangePhoneDraft]);
const closePasswordPanel = useCallback(() => {
const passwordTrigger = passwordTriggerRef.current;
setIsPasswordPanelOpen(false);
resetPasswordDraft();
focusAfterNextPaint(passwordTrigger);
}, [focusAfterNextPaint, resetPasswordDraft]);
if (!isOpen) {
return null;
}
@@ -556,6 +585,31 @@ export function AccountModal({
</button>
</div>
<div className="platform-subpanel rounded-2xl px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<div className="mt-1 text-sm text-[var(--platform-text-base)]">
</div>
</div>
<button
type="button"
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-[11px]"
onClick={(event) => {
passwordTriggerRef.current = event.currentTarget;
setAccountNotice('');
resetPasswordDraft();
setIsPasswordPanelOpen(true);
}}
>
</button>
</div>
</div>
<div className="platform-subpanel rounded-2xl px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
@@ -893,6 +947,74 @@ export function AccountModal({
</div>
</OverlayPanel>
) : null}
{isPasswordPanelOpen ? (
<OverlayPanel
eyebrow="账号安全"
title="修改登录密码"
description="输入当前密码与新密码。首次设置密码时当前密码可留空。"
onBack={closePasswordPanel}
onClose={onClose}
>
<div className="grid gap-3">
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
<span></span>
<input
className="platform-input h-11"
value={currentPassword}
type="password"
autoComplete="current-password"
placeholder="首次设置可留空"
onChange={(event) => setCurrentPassword(event.target.value)}
/>
</label>
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
<span></span>
<input
className="platform-input h-11"
value={newPassword}
type="password"
autoComplete="new-password"
placeholder="设置新密码"
onChange={(event) => setNewPassword(event.target.value)}
/>
</label>
{passwordError ? (
<div className="platform-banner platform-banner--danger text-sm">
{passwordError}
</div>
) : null}
<button
type="button"
disabled={changingPassword || !newPassword.trim()}
className="platform-button platform-button--primary h-11 w-full text-sm disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => {
void (async () => {
setChangingPassword(true);
setPasswordError('');
try {
await onChangePassword(currentPassword, newPassword);
setAccountNotice('密码已更新。');
closePasswordPanel();
} catch (error) {
setPasswordError(
error instanceof Error
? error.message
: '修改密码失败,请稍后再试。',
);
} finally {
setChangingPassword(false);
}
})();
}}
>
{changingPassword ? '提交中...' : '确认修改密码'}
</button>
</div>
</OverlayPanel>
) : null}
</OverlayPanel>
) : null}
</div>