duoqi-admin/src/components/admin/ChangePasswordDialog.tsx
Wang Zhuoxuan 165625f362
All checks were successful
Build & Deploy Admin / deploy (push) Successful in 28s
feat: 添加当前管理员修改密码功能
在侧边栏底部新增"修改密码"入口,支持输入当前密码和新密码修改自己的登录密码。
2026-04-23 12:34:45 +08:00

205 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<typeof schema>
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<FormValues>({
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 (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{success ? "密码修改成功" : "修改当前账号的登录密码"}
</DialogDescription>
</DialogHeader>
{success ? (
<div className="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">
使
</div>
) : (
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
{serverError && (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
{serverError}
</div>
)}
<div className="space-y-2">
<Label htmlFor="currentPassword"></Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrent ? "text" : "password"}
autoComplete="current-password"
{...form.register("currentPassword")}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2"
onClick={() => setShowCurrent(!showCurrent)}
>
{showCurrent ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</Button>
</div>
{form.formState.errors.currentPassword && (
<p className="text-sm text-destructive">
{form.formState.errors.currentPassword.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="newPassword"></Label>
<div className="relative">
<Input
id="newPassword"
type={showNew ? "text" : "password"}
autoComplete="new-password"
{...form.register("newPassword")}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2"
onClick={() => setShowNew(!showNew)}
>
{showNew ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</Button>
</div>
{form.formState.errors.newPassword && (
<p className="text-sm text-destructive">
{form.formState.errors.newPassword.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"></Label>
<Input
id="confirmPassword"
type="password"
autoComplete="new-password"
{...form.register("confirmPassword")}
/>
{form.formState.errors.confirmPassword && (
<p className="text-sm text-destructive">
{form.formState.errors.confirmPassword.message}
</p>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={submitting}
>
</Button>
<Button type="submit" disabled={submitting}>
{submitting ? "提交中..." : "确认修改"}
</Button>
</DialogFooter>
</form>
)}
{success && (
<DialogFooter>
<Button onClick={() => handleOpenChange(false)}></Button>
</DialogFooter>
)}
</DialogContent>
</Dialog>
)
}