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

167 lines
5.0 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.
'use client'
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'
import {useForm, useFormContext, useWatch} 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'
import dynamic from 'next/dynamic'
// 表单验证规则
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 = async (value: Schema) => {
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>
<Form
form={form}
handler={async () => {
const data = form.getValues()
await handler(data)
}}
className="flex flex-col gap-4 mt-4">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{/* 手机号输入 */}
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入手机号" autoComplete="tel-national"/>
)}
</FormField>
{/* 短信验证码 */}
<div className="flex gap-4 items-end">
<FormField<Schema> name="code" label="验证码" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
)}
</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>
<DialogFooter>
<Button
theme="outline"
type="button"
onClick={() => {
actualOnOpenChange(false)
form.reset()
}}>
</Button>
<Button type="submit"></Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
)
}
function SendMsgByPhone() {
const {control} = useFormContext<Schema>()
const phone = useWatch({control, name: 'phone'})
return <SendMsg phone={phone}/>
}
const SendMsg = dynamic(() => import('@/components/send-msg'), {ssr: false})