expo-duooomi-app/components/ChangePasswordModal.tsx

272 lines
9.2 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.

import { Ionicons } from '@expo/vector-icons'
import { Block, ConfirmModal, Input, Text } from '@share/components'
import React, { useState } from 'react'
export type ChangePasswordModalProps = {
onConfirm: (currentPassword: string, newPassword: string) => void | Promise<void>
onCancel: () => void
}
// 验证密码强度
const validatePassword = (password: string): string | null => {
if (!password) {
return '请输入新密码'
}
if (password.length < 6) {
return '密码长度至少6位'
}
// 检查是否是无效密码:全是相同字符
if (/^(.)\1+$/.test(password)) {
return '密码不能全是相同字符'
}
// 检查是否是连续数字(如 123456, 654321
const isSequentialNumbers = (str: string): boolean => {
for (let i = 0; i < str.length - 1; i++) {
const current = parseInt(str[i], 10)
const next = parseInt(str[i + 1], 10)
if (isNaN(current) || isNaN(next)) return false
if (Math.abs(current - next) !== 1) return false
}
return true
}
// 检查是否是连续字母(如 abcdef, fedcba
const isSequentialLetters = (str: string): boolean => {
const lowerStr = str.toLowerCase()
for (let i = 0; i < lowerStr.length - 1; i++) {
const current = lowerStr.charCodeAt(i)
const next = lowerStr.charCodeAt(i + 1)
if (Math.abs(current - next) !== 1) return false
}
return true
}
if (isSequentialNumbers(password) || isSequentialLetters(password)) {
return '密码不能是连续字符'
}
// 检查是否是简单密码(如 111111, 000000, aaaaaa
const commonWeakPasswords = [
'111111',
'222222',
'333333',
'444444',
'555555',
'666666',
'777777',
'888888',
'999999',
'000000',
'aaaaaa',
'bbbbbb',
'cccccc',
'dddddd',
'eeeeee',
'ffffff',
'123456',
'654321',
'abcdef',
'fedcba',
]
if (commonWeakPasswords.includes(password.toLowerCase())) {
return '密码过于简单,请使用更复杂的密码'
}
return null
}
export default function ChangePasswordModal({ onConfirm, onCancel }: ChangePasswordModalProps) {
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
const [showNewPassword, setShowNewPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<{
currentPassword?: string
newPassword?: string
confirmPassword?: string
}>({})
const validateCurrentPassword = (value: string) => {
if (!value) {
setErrors((prev) => ({ ...prev, currentPassword: '请输入当前密码' }))
} else {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors.currentPassword
return newErrors
})
}
}
const validateNewPassword = (value: string) => {
const error = validatePassword(value)
if (error) {
setErrors((prev) => ({ ...prev, newPassword: error }))
} else {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors.newPassword
return newErrors
})
// 如果确认密码已输入,重新验证确认密码
if (confirmPassword) {
validateConfirmPassword(confirmPassword, value)
}
}
}
const validateConfirmPassword = (value: string, newPwd = newPassword) => {
if (!value) {
setErrors((prev) => ({ ...prev, confirmPassword: '请再次输入新密码' }))
} else if (value !== newPwd) {
setErrors((prev) => ({ ...prev, confirmPassword: '两次密码输入不一致' }))
} else {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors.confirmPassword
return newErrors
})
}
}
const handleConfirm = async () => {
if (loading) return
// 验证所有字段并收集错误
const newErrors: typeof errors = {}
if (!currentPassword) {
newErrors.currentPassword = '请输入当前密码'
}
const newPasswordError = validatePassword(newPassword)
if (newPasswordError) {
newErrors.newPassword = newPasswordError
}
if (!confirmPassword) {
newErrors.confirmPassword = '请再次输入新密码'
} else if (confirmPassword !== newPassword) {
newErrors.confirmPassword = '两次密码输入不一致'
}
// 设置错误状态
setErrors(newErrors)
// 如果有错误,不继续提交
if (Object.keys(newErrors).length > 0) {
return
}
setLoading(true)
try {
await onConfirm(currentPassword, newPassword)
} finally {
setLoading(false)
}
}
return (
<ConfirmModal
badge="password"
cancelText="取消"
confirmText={loading ? '修改中...' : '确定'}
confirmLoading={loading}
content={
<Block className="w-full gap-[16px]">
<Block className="w-full">
<Text className="mb-[8px] text-[14px] text-black"></Text>
<Block className="relative w-full">
<Input
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
errors.currentPassword ? 'border-red-500' : 'border-black'
}`}
placeholder="请输入当前密码"
placeholderTextColor="#9CA3AF"
value={currentPassword}
onChangeText={(text) => {
setCurrentPassword(text)
if (errors.currentPassword) {
validateCurrentPassword(text)
}
}}
onBlur={() => validateCurrentPassword(currentPassword)}
secureTextEntry={!showCurrentPassword}
/>
<Block
className="absolute right-[12px] top-1/2 -translate-y-1/2"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
>
<Ionicons color="#9CA3AF" name={showCurrentPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
{errors.currentPassword && (
<Text className="mt-[4px] text-[12px] text-red-500">{errors.currentPassword}</Text>
)}
</Block>
<Block className="w-full">
<Text className="mb-[8px] text-[14px] text-black"></Text>
<Block className="relative w-full">
<Input
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
errors.newPassword ? 'border-red-500' : 'border-black'
}`}
placeholder="请输入新密码至少6位"
placeholderTextColor="#9CA3AF"
value={newPassword}
onChangeText={(text) => {
setNewPassword(text)
if (errors.newPassword) {
validateNewPassword(text)
}
}}
onBlur={() => validateNewPassword(newPassword)}
secureTextEntry={!showNewPassword}
/>
<Block
className="absolute right-[12px] top-1/2 -translate-y-1/2"
onClick={() => setShowNewPassword(!showNewPassword)}
>
<Ionicons color="#9CA3AF" name={showNewPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
{errors.newPassword && <Text className="mt-[4px] text-[12px] text-red-500">{errors.newPassword}</Text>}
</Block>
<Block className="w-full">
<Text className="mb-[8px] text-[14px] text-black"></Text>
<Block className="relative w-full">
<Input
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
errors.confirmPassword ? 'border-red-500' : 'border-black'
}`}
placeholder="请再次输入新密码"
placeholderTextColor="#9CA3AF"
value={confirmPassword}
onChangeText={(text) => {
setConfirmPassword(text)
if (errors.confirmPassword) {
validateConfirmPassword(text)
}
}}
onBlur={() => validateConfirmPassword(confirmPassword)}
secureTextEntry={!showConfirmPassword}
/>
<Block
className="absolute right-[12px] top-1/2 -translate-y-1/2"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
<Ionicons color="#9CA3AF" name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
{errors.confirmPassword && (
<Text className="mt-[4px] text-[12px] text-red-500">{errors.confirmPassword}</Text>
)}
</Block>
</Block>
}
title="修改密码"
onCancel={onCancel}
onConfirm={handleConfirm}
/>
)
}