flnxi-Wu/components/ChangePasswordModal.tsx
2026-06-01 17:00:05 +08:00

247 lines
6.7 KiB
TypeScript
Raw Permalink 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.

/**
* ChangePasswordModal 组件
*
* 修改密码模态框组件
* 支持首次登录强制修改密码和普通修改密码
* 使用统一的 Modal 和 Input 组件,遵循三层架构
*
* @created 2025-01-XX
* @component
*/
'use client'
import { useState } from 'react'
import { useSession } from 'next-auth/react'
import { useDialog } from '@/components/ui/DialogProvider'
import Modal from '@/components/ui/Modal'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import { PASSWORD_POLICY_DESCRIPTION, validatePasswordStrength } from '@/lib/password-policy'
interface ChangePasswordModalProps {
isOpen: boolean
onClose: () => void
isRequired?: boolean // 是否强制修改(首次登录)
onSuccess?: () => void
}
export default function ChangePasswordModal({
isOpen,
onClose,
isRequired = false,
onSuccess
}: ChangePasswordModalProps) {
const { alert } = useDialog()
const { update } = useSession()
const styles = useThemeStyles()
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const resetForm = () => {
setOldPassword('')
setNewPassword('')
setConfirmPassword('')
setError('')
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!newPassword || !confirmPassword) {
setError('请填写所有字段')
return
}
const passwordValidation = validatePasswordStrength(newPassword)
if (!passwordValidation.valid) {
setError(passwordValidation.error || '新密码不符合安全要求')
return
}
if (newPassword !== confirmPassword) {
setError('两次输入的密码不一致')
return
}
setLoading(true)
try {
const payload = isRequired
? { newPassword }
: { oldPassword, newPassword }
const response = await fetch('/api/user/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await response.json()
if (response.ok) {
resetForm()
try {
await update()
} catch (error) {
console.warn('刷新会话状态失败:', error)
}
if (onSuccess) {
onSuccess()
} else {
onClose()
}
// 显示成功消息
alert({
title: '成功',
message: '密码修改成功!',
variant: 'success',
})
} else {
setError(data.error || '修改密码失败')
}
} catch (error) {
console.error('修改密码错误:', error)
setError('修改密码失败,请稍后重试')
} finally {
setLoading(false)
}
}
const formContent = (
<form onSubmit={handleSubmit} className="space-y-4">
{!isRequired && (
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
旧密码
</label>
<Input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
placeholder="请输入旧密码"
required={!isRequired}
fullWidth
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
新密码
</label>
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码"
required
minLength={10}
fullWidth
/>
<p className="mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400">
{PASSWORD_POLICY_DESCRIPTION}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
确认新密码
</label>
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入新密码"
required
minLength={10}
fullWidth
/>
</div>
{error && (
<div className={cn(
'p-3 rounded-md border',
'bg-red-50 dark:bg-red-900/20',
'border-red-200 dark:border-red-800'
)}>
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="flex gap-3 justify-end">
{!isRequired && (
<Button
type="button"
variant="secondary"
onClick={onClose}
>
取消
</Button>
)}
<Button
type="submit"
variant="primary"
disabled={loading}
loading={loading}
className="min-w-24 justify-center"
>
{loading ? '修改中...' : '确认修改'}
</Button>
</div>
</form>
)
if (!isOpen) return null
if (isRequired) {
return (
<section className="mx-auto flex min-h-[calc(100vh-6rem)] w-full max-w-md items-center justify-center px-4 py-10">
<div className="w-full rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-800">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400">账号安全</p>
<h1 className="mt-2 text-xl font-semibold text-slate-950 dark:text-white">
设置新密码
</h1>
<p className="mt-2 text-sm leading-6 text-slate-500 dark:text-slate-400">
首次登录需要先更换初始密码,完成后才能进入素材库。
</p>
</div>
<div className="space-y-5 px-6 py-5">
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2.5 dark:border-amber-900/60 dark:bg-amber-950/30">
<p className="text-sm leading-6 text-amber-800 dark:text-amber-200">
当前账号仍在使用系统初始化密码,请设置一个只由您掌握的新密码。
</p>
</div>
{formContent}
</div>
</div>
</section>
)
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="修改密码"
showCloseButton
closeOnOverlayClick
closeOnEscape
>
{formContent}
</Modal>
)
}