Files
web/src/components/composites/dialogs/change-password-dialog.tsx

161 lines
4.8 KiB
TypeScript
Raw Normal View History

'use client'
2025-12-11 14:10:52 +08:00
import {useState} from 'react'
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
2025-12-11 14:10:52 +08:00
import {useForm, useFormContext} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import * as z from 'zod'
import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import {updatePassword} from '@/actions/user'
2025-12-11 14:10:52 +08:00
import SendMsg from '@/components/send-msg'
// 表单验证规则
const schema = z.object({
phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`),
captcha: z.string().nonempty('请输入验证码'),
code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`),
password: z.string().min(6, `密码至少6位`),
confirm_password: z.string(),
}).refine(data => data.password === data.confirm_password, {
message: '两次输入的密码不一致',
path: ['confirm_password'],
})
type Schema = z.infer<typeof schema>
interface ChangePasswordDialogProps {
triggerClassName?: string
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
onSuccess?: () => void
}
export function ChangePasswordDialog({
triggerClassName,
open,
defaultOpen,
onOpenChange,
onSuccess,
}: ChangePasswordDialogProps) {
const [internalOpen, setInternalOpen] = useState(defaultOpen || false)
const router = useRouter()
const actualOpen = open !== undefined ? open : internalOpen
const actualOnOpenChange = onOpenChange || setInternalOpen
// 表单初始化
const form = useForm<Schema>({
resolver: zodResolver(
schema.refine(
data => /^(?=.*[a-zA-Z])(?=.*\d).{6,}$/.test(data.password),
{
message: '密码需包含字母和数字且不少于6位',
path: ['password'],
},
),
),
defaultValues: {
phone: '',
captcha: '',
code: '',
password: '',
confirm_password: '',
},
})
// 提交处理
const handler = form.handleSubmit(async (value) => {
try {
const resp = await updatePassword({
phone: value.phone,
code: value.code,
password: value.password,
})
if (!resp.success) {
throw new Error(resp.message)
}
toast.success(`保存成功,请重新登录`)
onSuccess?.()
actualOnOpenChange(false)
router.replace('/login')
}
catch (e) {
console.error(e)
toast.error(`保存失败`, {
description: e instanceof Error ? e.message : String(e),
})
}
})
return (
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
<DialogTrigger asChild>
<Button theme="outline" className={triggerClassName || 'w-24 h-9'}></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<Form form={form} handler={handler} className="flex flex-col gap-4 mt-4">
{/* 手机号输入 */}
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入手机号" autoComplete="tel-national"/>
)}
</FormField>
{/* 短信验证码 */}
2025-12-11 14:10:52 +08:00
<div className="flex gap-4 items-end">
<FormField<Schema> name="code" label="验证码" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
2025-12-11 14:10:52 +08:00
)}
</FormField>
<SendMsgByPhone/>
</div>
{/* 新密码 */}
<FormField<Schema> name="password" label="新密码" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入新密码" type="password" autoComplete="new-password"/>
)}
</FormField>
{/* 确认密码 */}
<FormField<Schema> name="confirm_password" label="确认密码" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请再次输入新密码" type="password" autoComplete="new-password"/>
)}
</FormField>
</Form>
<DialogFooter>
<Button
theme="outline"
type="button"
onClick={() => {
actualOnOpenChange(false)
form.reset()
}}>
</Button>
<Button onClick={handler}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
2025-12-11 14:10:52 +08:00
function SendMsgByPhone() {
const form = useFormContext<Schema>()
const phone = form.watch('phone')
return <SendMsg phone={phone}/>
}