All checks were successful
Build & Deploy Admin / deploy (push) Successful in 28s
在侧边栏底部新增"修改密码"入口,支持输入当前密码和新密码修改自己的登录密码。
205 lines
6.7 KiB
TypeScript
205 lines
6.7 KiB
TypeScript
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>
|
||
)
|
||
}
|