diff --git a/src/components/admin/ChangePasswordDialog.tsx b/src/components/admin/ChangePasswordDialog.tsx new file mode 100644 index 0000000..471df04 --- /dev/null +++ b/src/components/admin/ChangePasswordDialog.tsx @@ -0,0 +1,204 @@ +import { useState } from "react" +import { useForm } from "react-hook-form" +import { z } from "zod/v4" +import { zodResolver } from "@hookform/resolvers/zod" +import { Eye, EyeOff } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { changePassword } from "@/lib/api/admin-api" + +const schema = z + .object({ + currentPassword: z.string().min(1, "请输入当前密码"), + newPassword: z.string().min(8, "新密码至少8个字符").max(128, "密码不超过128个字符"), + confirmPassword: z.string().min(1, "请确认新密码"), + }) + .refine((d) => d.newPassword === d.confirmPassword, { + message: "两次输入的密码不一致", + path: ["confirmPassword"], + }) + .refine((d) => d.currentPassword !== d.newPassword, { + message: "新密码不能与当前密码相同", + path: ["newPassword"], + }) + +type FormValues = z.infer + +interface ChangePasswordDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ChangePasswordDialog({ open, onOpenChange }: ChangePasswordDialogProps) { + const [submitting, setSubmitting] = useState(false) + const [serverError, setServerError] = useState("") + const [success, setSuccess] = useState(false) + const [showCurrent, setShowCurrent] = useState(false) + const [showNew, setShowNew] = useState(false) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" }, + }) + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + setServerError("") + setSuccess(false) + } + onOpenChange(nextOpen) + } + + async function handleSubmit(data: FormValues) { + setServerError("") + setSubmitting(true) + try { + const res = await changePassword({ + currentPassword: data.currentPassword, + newPassword: data.newPassword, + }) + if (res.success) { + setSuccess(true) + } else { + const code = res.error?.code ?? "" + if (code === "UNAUTHORIZED") { + setServerError("当前密码错误") + } else if (code === "VALIDATION_ERROR") { + setServerError(res.error?.message ?? "参数校验失败") + } else if (code === "FORBIDDEN") { + setServerError("账户已被禁用") + } else { + setServerError(res.error?.message ?? "修改失败") + } + } + } catch { + setServerError("网络错误,请稍后重试") + } finally { + setSubmitting(false) + } + } + + return ( + + + + 修改密码 + + {success ? "密码修改成功" : "修改当前账号的登录密码"} + + + + {success ? ( +
+ 密码已成功修改,下次登录请使用新密码。 +
+ ) : ( +
+ {serverError && ( +
+ {serverError} +
+ )} + +
+ +
+ + +
+ {form.formState.errors.currentPassword && ( +

+ {form.formState.errors.currentPassword.message} +

+ )} +
+ +
+ +
+ + +
+ {form.formState.errors.newPassword && ( +

+ {form.formState.errors.newPassword.message} +

+ )} +
+ +
+ + + {form.formState.errors.confirmPassword && ( +

+ {form.formState.errors.confirmPassword.message} +

+ )} +
+ + + + + +
+ )} + + {success && ( + + + + )} +
+
+ ) +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 9dc967a..6b9dd50 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useState } from "react" import { NavLink, useNavigate } from "react-router" import { LayoutDashboard, @@ -12,9 +13,11 @@ import { AlertCircle, Shield, BookMarked, + KeyRound, } from "lucide-react" import { cn } from "@/lib/utils" import { useAuth } from "@/hooks/use-auth" +import { ChangePasswordDialog } from "@/components/admin/ChangePasswordDialog" const navItems = [ { to: "/", label: "数据看板", icon: LayoutDashboard, end: true }, @@ -33,6 +36,7 @@ const navItems = [ export function Sidebar() { const { logout } = useAuth() const navigate = useNavigate() + const [changePasswordOpen, setChangePasswordOpen] = useState(false) function handleLogout() { logout() @@ -40,41 +44,52 @@ export function Sidebar() { } return ( - + + ) } diff --git a/src/lib/api/admin-api.ts b/src/lib/api/admin-api.ts index 7b978bd..fe7df9c 100644 --- a/src/lib/api/admin-api.ts +++ b/src/lib/api/admin-api.ts @@ -44,6 +44,24 @@ export async function fetchMe(): Promise> { return apiClient.get("auth/me").json>() } +// ==================== 修改密码 ==================== + +/** 修改密码请求 */ +export interface ChangePasswordRequest { + currentPassword: string + newPassword: string +} + +/** + * 修改当前管理员密码 + * PUT /admin/change-password + */ +export async function changePassword( + data: ChangePasswordRequest +): Promise> { + return apiClient.put("change-password", { json: data }).json>() +} + // ==================== 管理员管理 ==================== /** fetchAdmins 的查询参数 */