import { useMemo, useState } from "react"; import { ArrowLeft, Check, KeyRound, ShieldCheck } from "lucide-react"; import { Alert, Button, Form } from "tdesign-react"; import { changePasswordThunk, clearSessionError, useAppDispatch, useAppSelector } from "../../store"; import type { Admin } from "../../types/auth"; import { StarterPasswordInput } from "./StarterAuth"; import { Page } from "../common"; import { useUnsavedChanges } from "../../contexts/UnsavedChanges"; export interface ChangePasswordPageProps { admin?: Admin | null; onSuccess?: (admin: Admin) => void; onCancel?: () => void; returnLabel?: string; firstLogin?: boolean; } export default function ChangePasswordPage({ admin, onSuccess, onCancel, returnLabel = "返回当前页面", firstLogin = false }: ChangePasswordPageProps) { const dispatch = useAppDispatch(); const sessionAdmin = useAppSelector((state) => state.session.admin); const { status, error } = useAppSelector((state) => state.session); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [formError, setFormError] = useState(null); const [fieldErrors, setFieldErrors] = useState<{ newPassword?: string; confirmation?: string }>({}); const busy = status === "loading"; const dirty = Boolean(currentPassword || newPassword || confirmation); useUnsavedChanges(dirty); const displayAdmin = admin ?? sessionAdmin; const isFirstLogin = firstLogin || Boolean(displayAdmin?.mustChangePassword); const displayName = displayAdmin?.displayName || displayAdmin?.username || "当前管理员"; const requirements = useMemo(() => [ { label: "至少 12 个字符", valid: newPassword.length >= 12 }, { label: "两次输入保持一致", valid: Boolean(confirmation) && newPassword === confirmation }, ], [confirmation, newPassword]); const submit = async () => { setFormError(null); setFieldErrors({}); if (newPassword.length < 12) { setFieldErrors({ newPassword: "新密码至少需要 12 位" }); focusPasswordField("新密码"); return; } if (newPassword !== confirmation) { setFieldErrors({ confirmation: "两次输入的新密码不一致" }); focusPasswordField("确认新密码"); return; } try { const updated = await dispatch(changePasswordThunk({ currentPassword, newPassword })).unwrap(); // Clear the local draft before notifying the parent. This keeps the // unsaved-changes guard correct even when a host keeps this page mounted. setCurrentPassword(""); setNewPassword(""); setConfirmation(""); setFieldErrors({}); onSuccess?.(updated); } catch { // The rejected thunk exposes its server message through session.error. } }; const form =
{ if (context.validateResult === true) void submit(); }} > { setCurrentPassword(value); setFormError(null); dispatch(clearSessionError()); }} autocomplete="current-password" disabled={busy} placeholder="请输入当前密码" /> { setNewPassword(value); setFieldErrors(current => ({ ...current, newPassword: undefined })); setFormError(null); dispatch(clearSessionError()); }} autocomplete="new-password" disabled={busy} placeholder="请输入至少 12 位的新密码" /> {fieldErrors.newPassword && {fieldErrors.newPassword}} { setConfirmation(value); setFieldErrors(current => ({ ...current, confirmation: undefined })); setFormError(null); dispatch(clearSessionError()); }} autocomplete="new-password" disabled={busy} placeholder="请再次输入新密码" /> {fieldErrors.confirmation && {fieldErrors.confirmation}}
; const panel =

{isFirstLogin ? "设置登录密码" : "修改登录密码"}

{isFirstLogin ? "首次登录需要先设置一个新的登录密码。" : `当前账号:${displayName}`}

{(formError || error) &&
}
{form}
; if (!isFirstLogin) { return }>{returnLabel} : undefined} className="tn-password-page">{panel}; } return

初始安全设置

欢迎使用系统,管理员 {displayName},为保障账户安全,首次登录请先设置新密码。

{panel}
; } function focusPasswordField(label: string) { const focus = () => { const nodes = Array.from(document.querySelectorAll("[aria-invalid='true'], [aria-invalid='true'] input, [aria-invalid='true'] textarea")); const target = nodes.find(node => node.closest(".t-form__item")?.textContent?.includes(label)); target?.focus(); }; requestAnimationFrame(focus); window.setTimeout(focus, 60); }