u
This commit is contained in:
1 parent
44fb48ef6a
commit
ca2fa3543d
10 files changed
+538
-382
No files matched your search
@@ -1,9 +1,39 @@
|
|||||||
import { Text, View } from 'react-native'
|
import React, { useRef, useState } from 'react'
|
||||||
|
import { Button, ScrollView, Text, View } from 'react-native'
|
||||||
|
|
||||||
|
export default function ScrollToExample() {
|
||||||
|
const scrollRef = useRef<ScrollView>(null)
|
||||||
|
const [positions, setPositions] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
|
const handleLayout = (name: string, event: any) => {
|
||||||
|
const { y } = event.nativeEvent.layout
|
||||||
|
setPositions(prev => ({ ...prev, [name]: y }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToField = (name: string) => {
|
||||||
|
const y = positions[name]
|
||||||
|
if (typeof y === 'number' && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTo({ y, animated: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Love() {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={{ flex: 1, paddingTop: 40 }}>
|
||||||
<Text>Love</Text>
|
<Button title="滚动到 Password" onPress={() => scrollToField('password')} />
|
||||||
|
|
||||||
|
<ScrollView ref={scrollRef} style={{ marginTop: 20 }}>
|
||||||
|
<View onLayout={e => handleLayout('username', e)} style={{ height: 300, backgroundColor: '#f9c2ff', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Text>Username 区域</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View onLayout={e => handleLayout('email', e)} style={{ height: 300, backgroundColor: '#c2f9ff', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Text>Email 区域</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View onLayout={e => handleLayout('password', e)} style={{ height: 300, backgroundColor: '#ffc2c2', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Text>Password 区域</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { FormWrapperRef } from '@/components/common/FormWrapper'
|
import type { AFormRef } from '@/components/common/ao'
|
||||||
import { Lock, UserRound } from 'lucide-react-native'
|
import { Lock, UserRound } from 'lucide-react-native'
|
||||||
import { useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Image, View } from 'react-native'
|
import { Image, ScrollView, View } from 'react-native'
|
||||||
import FormWrapper from '@/components/common/FormWrapper'
|
|
||||||
|
import { AForm, AFormItem, AInput } from '@/components/common/ao'
|
||||||
import { Button, ButtonSpinner, ButtonText } from '@/components/ui/button'
|
import { Button, ButtonSpinner, ButtonText } from '@/components/ui/button'
|
||||||
import { useToast } from '@/utils/toast'
|
import { useToast } from '@/utils/toast'
|
||||||
|
|
||||||
@@ -13,9 +14,14 @@ interface RegisterForm {
|
|||||||
a: string
|
a: string
|
||||||
b: string
|
b: string
|
||||||
c: string
|
c: string
|
||||||
|
d: string
|
||||||
|
e: string
|
||||||
|
f: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthLogin() {
|
export default function AuthLogin() {
|
||||||
|
const scrollRef = useRef<ScrollView>(null)
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [form, setForm] = useState<RegisterForm>({
|
const [form, setForm] = useState<RegisterForm>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -24,9 +30,12 @@ export default function AuthLogin() {
|
|||||||
a: '',
|
a: '',
|
||||||
b: '',
|
b: '',
|
||||||
c: '',
|
c: '',
|
||||||
|
d: '',
|
||||||
|
e: '',
|
||||||
|
f: '',
|
||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const formRef = useRef<FormWrapperRef>(null)
|
const formRef = useRef<AFormRef>(null)
|
||||||
|
|
||||||
const handleInputChange = (field: keyof RegisterForm, value: string) => {
|
const handleInputChange = (field: keyof RegisterForm, value: string) => {
|
||||||
setForm(prev => ({ ...prev, [field]: value }))
|
setForm(prev => ({ ...prev, [field]: value }))
|
||||||
@@ -44,106 +53,80 @@ export default function AuthLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col items-center px-6 h-full mb-4">
|
<ScrollView ref={scrollRef} style={{ flex: 1 }}>
|
||||||
<Image source={require('@/static/images/logo.png')} style={{ width: 120, height: 120 }} className="my-12" />
|
<View className="flex flex-col items-center px-6">
|
||||||
|
<Image source={require('@/static/images/logo.png')} style={{ width: 120, height: 120 }} className="my-12" />
|
||||||
|
|
||||||
<FormWrapper ref={formRef} form={form}>
|
<AForm ref={formRef} form={form} scrollRef={scrollRef}>
|
||||||
<FormWrapper.FormItem
|
<AFormItem name="username" label="账号" isRequired>
|
||||||
name="username"
|
<AInput
|
||||||
label="账号"
|
placeholder="请输入账号"
|
||||||
required
|
leftIcon={UserRound}
|
||||||
>
|
value={form.username}
|
||||||
<FormWrapper.Input
|
onChange={text => handleInputChange('username', text)}
|
||||||
placeholder="请输入账号"
|
/>
|
||||||
leftIcon={UserRound}
|
</AFormItem>
|
||||||
value={form.username}
|
|
||||||
onChangeText={text => handleInputChange('username', text)}
|
|
||||||
/>
|
|
||||||
</FormWrapper.FormItem>
|
|
||||||
|
|
||||||
<FormWrapper.FormItem
|
<AFormItem
|
||||||
name="password"
|
name="password"
|
||||||
label="密码"
|
label="密码"
|
||||||
required
|
isRequired
|
||||||
rules={[
|
rules={[
|
||||||
(val: any) => val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null,
|
(val: any) => val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormWrapper.Input
|
<AInput
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
leftIcon={Lock}
|
leftIcon={Lock}
|
||||||
isPassword
|
isPassword
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChangeText={text => handleInputChange('password', text)}
|
onChange={text => handleInputChange('password', text)}
|
||||||
/>
|
/>
|
||||||
</FormWrapper.FormItem>
|
</AFormItem>
|
||||||
|
|
||||||
<FormWrapper.FormItem
|
<AFormItem
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
label="确认密码"
|
label="确认密码"
|
||||||
required
|
isRequired
|
||||||
rules={[
|
rules={[
|
||||||
(val: any) => val !== form.password ? '两次密码不一致' : null,
|
(val: any) => val !== form.password ? '两次密码不一致' : null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormWrapper.Input
|
<AInput
|
||||||
placeholder="请再次输入密码"
|
placeholder="请再次输入密码"
|
||||||
leftIcon={Lock}
|
leftIcon={Lock}
|
||||||
isPassword
|
isPassword
|
||||||
value={form.confirmPassword}
|
value={form.confirmPassword}
|
||||||
onChangeText={text => handleInputChange('confirmPassword', text)}
|
onChange={text => handleInputChange('confirmPassword', text)}
|
||||||
/>
|
/>
|
||||||
</FormWrapper.FormItem>
|
</AFormItem>
|
||||||
<FormWrapper.FormItem
|
|
||||||
name="a"
|
|
||||||
label="a"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<FormWrapper.Input
|
|
||||||
placeholder="请再次输入密码"
|
|
||||||
isPassword
|
|
||||||
value={form.a}
|
|
||||||
onChangeText={text => handleInputChange('a', text)}
|
|
||||||
/>
|
|
||||||
</FormWrapper.FormItem>
|
|
||||||
<FormWrapper.FormItem
|
|
||||||
name="b"
|
|
||||||
label="b"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<FormWrapper.Input
|
|
||||||
placeholder="请再次输入密码"
|
|
||||||
isPassword
|
|
||||||
value={form.a}
|
|
||||||
onChangeText={text => handleInputChange('b', text)}
|
|
||||||
/>
|
|
||||||
</FormWrapper.FormItem>
|
|
||||||
<FormWrapper.FormItem
|
|
||||||
name="c"
|
|
||||||
label="c"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<FormWrapper.Input
|
|
||||||
placeholder="请再次输入密码"
|
|
||||||
isPassword
|
|
||||||
value={form.c}
|
|
||||||
onChangeText={text => handleInputChange('c', text)}
|
|
||||||
/>
|
|
||||||
</FormWrapper.FormItem>
|
|
||||||
</FormWrapper>
|
|
||||||
|
|
||||||
<Button className="w-full mt-4" onPress={defaultLogin} disabled={loading}>
|
{/* 其他表单项 */}
|
||||||
{loading
|
{['a', 'b', 'c', 'd', 'e', 'f'].map(field => (
|
||||||
? (
|
<AFormItem key={field} name={field} label={field} isRequired>
|
||||||
<>
|
<AInput
|
||||||
<ButtonSpinner color="gray" />
|
placeholder="请输入"
|
||||||
<ButtonText>请稍等 . . .</ButtonText>
|
isPassword
|
||||||
</>
|
value={form[field]}
|
||||||
)
|
onChange={text => handleInputChange(field, text)}
|
||||||
: (
|
/>
|
||||||
<ButtonText>注册</ButtonText>
|
</AFormItem>
|
||||||
)}
|
))}
|
||||||
</Button>
|
</AForm>
|
||||||
</View>
|
|
||||||
|
<Button className="w-full mb-4" onPress={defaultLogin} disabled={loading}>
|
||||||
|
{loading
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<ButtonSpinner color="gray" />
|
||||||
|
<ButtonText>请稍等 . . .</ButtonText>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ButtonText>注册</ButtonText>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
// FormWrapper.tsx
|
|
||||||
import React, { createContext, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormControlError,
|
|
||||||
FormControlErrorIcon,
|
|
||||||
FormControlErrorText,
|
|
||||||
FormControlLabel,
|
|
||||||
FormControlLabelText,
|
|
||||||
} from '@/components/ui/form-control'
|
|
||||||
import { AlertCircleIcon, EyeIcon, EyeOffIcon } from '@/components/ui/icon'
|
|
||||||
import { InputField, InputIcon, InputSlot, Input as NativeBaseInput } from '@/components/ui/input'
|
|
||||||
|
|
||||||
export interface FormWrapperRef {
|
|
||||||
validate: () => boolean
|
|
||||||
getValues: () => Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 FormContext 来传递表单数据和验证器注册函数
|
|
||||||
interface FormContextType<T = any> {
|
|
||||||
form: T
|
|
||||||
registerValidator: (name: string, validator: () => boolean) => void
|
|
||||||
unregisterValidator: (name: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormContext = createContext<FormContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
// ---------------- FormItem ----------------
|
|
||||||
export interface FormItemProps<T extends Record<string, any> = any> {
|
|
||||||
name: keyof T
|
|
||||||
label?: string
|
|
||||||
required?: boolean
|
|
||||||
rules?: ((val: any) => string | null)[]
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FormItem<T extends Record<string, any>>({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
rules,
|
|
||||||
children,
|
|
||||||
}: FormItemProps<T>) {
|
|
||||||
const formContext = useContext(FormContext)
|
|
||||||
if (!formContext) {
|
|
||||||
throw new Error('FormItem must be used within FormWrapper')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { form, registerValidator, unregisterValidator } = formContext
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
// 从 form 中根据 name 获取对应的值
|
|
||||||
const value = form[name]
|
|
||||||
|
|
||||||
const validate = (): boolean => {
|
|
||||||
let err = ''
|
|
||||||
|
|
||||||
if (required && (value === undefined || value === '' || value === null)) {
|
|
||||||
err = `${label || String(name)} 不能为空`
|
|
||||||
}
|
|
||||||
else if (rules) {
|
|
||||||
for (const rule of rules) {
|
|
||||||
const msg = rule(value)
|
|
||||||
if (msg) {
|
|
||||||
err = msg
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setError(err)
|
|
||||||
return !err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册验证函数到父组件
|
|
||||||
useEffect(() => {
|
|
||||||
registerValidator(String(name), validate)
|
|
||||||
return () => unregisterValidator(String(name))
|
|
||||||
}, [validate, registerValidator, unregisterValidator, name])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl size="md" className="w-full mb-6" isInvalid={!!error}>
|
|
||||||
{label && (
|
|
||||||
<FormControlLabel>
|
|
||||||
<FormControlLabelText>{label}</FormControlLabelText>
|
|
||||||
</FormControlLabel>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
{error && (
|
|
||||||
<FormControlError>
|
|
||||||
<FormControlErrorIcon as={AlertCircleIcon} className="text-red-500" />
|
|
||||||
<FormControlErrorText className="text-red-500">{error}</FormControlErrorText>
|
|
||||||
</FormControlError>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- FormInput ----------------
|
|
||||||
export interface FormInputProps {
|
|
||||||
placeholder?: string
|
|
||||||
leftIcon?: React.ComponentType<any>
|
|
||||||
isPassword?: boolean
|
|
||||||
value?: string
|
|
||||||
onChangeText?: (text: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FormInput({
|
|
||||||
placeholder,
|
|
||||||
leftIcon,
|
|
||||||
isPassword,
|
|
||||||
value,
|
|
||||||
onChangeText,
|
|
||||||
}: FormInputProps) {
|
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NativeBaseInput size="md">
|
|
||||||
{leftIcon && (
|
|
||||||
<InputSlot className="pl-3">
|
|
||||||
<InputIcon as={leftIcon} />
|
|
||||||
</InputSlot>
|
|
||||||
)}
|
|
||||||
<InputField
|
|
||||||
placeholder={placeholder}
|
|
||||||
type={isPassword ? (showPassword ? 'text' : 'password') : 'text'}
|
|
||||||
value={value}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
/>
|
|
||||||
{isPassword && (
|
|
||||||
<InputSlot className="pr-3" onPress={() => setShowPassword(prev => !prev)}>
|
|
||||||
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
|
|
||||||
</InputSlot>
|
|
||||||
)}
|
|
||||||
</NativeBaseInput>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- FormWrapper ----------------
|
|
||||||
interface FormWrapperProps<T extends Record<string, any>> {
|
|
||||||
children: React.ReactNode
|
|
||||||
form: T
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义组件接口
|
|
||||||
interface FormWrapperComponent extends React.ForwardRefExoticComponent<
|
|
||||||
FormWrapperProps<any> & React.RefAttributes<FormWrapperRef>
|
|
||||||
> {
|
|
||||||
FormItem: typeof FormItem
|
|
||||||
Input: typeof FormInput
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内部组件实现
|
|
||||||
function FormWrapperInner<T extends Record<string, any>>(
|
|
||||||
props: FormWrapperProps<T>,
|
|
||||||
ref: React.Ref<FormWrapperRef>,
|
|
||||||
) {
|
|
||||||
const { children, form } = props
|
|
||||||
const validators = useRef<Map<string, () => boolean>>(new Map())
|
|
||||||
|
|
||||||
const registerValidator = (name: string, validator: () => boolean) => {
|
|
||||||
validators.current.set(name, validator)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unregisterValidator = (name: string) => {
|
|
||||||
validators.current.delete(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = (): boolean => {
|
|
||||||
let isValid = true
|
|
||||||
|
|
||||||
// 然后调用所有验证函数
|
|
||||||
validators.current.forEach((validator) => {
|
|
||||||
if (!validator()) {
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValues = (): Record<string, any> => form
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({ validate, getValues }))
|
|
||||||
|
|
||||||
const contextValue: FormContextType<T> = {
|
|
||||||
form,
|
|
||||||
registerValidator,
|
|
||||||
unregisterValidator,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</FormContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建基础组件
|
|
||||||
const BaseFormWrapper = forwardRef(FormWrapperInner) as <T extends Record<string, any>>(
|
|
||||||
props: FormWrapperProps<T> & { ref?: React.Ref<FormWrapperRef> },
|
|
||||||
) => React.JSX.Element
|
|
||||||
|
|
||||||
// 创建带有静态属性的组件
|
|
||||||
const FormWrapper = BaseFormWrapper as FormWrapperComponent
|
|
||||||
|
|
||||||
// 添加静态属性
|
|
||||||
FormWrapper.FormItem = FormItem
|
|
||||||
FormWrapper.Input = FormInput
|
|
||||||
|
|
||||||
export default FormWrapper
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { ScrollView } from 'react-native'
|
||||||
|
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
|
||||||
|
|
||||||
|
export interface FormRef {
|
||||||
|
validate: () => boolean
|
||||||
|
scrollToField: (name: string) => void // 新增的滚动方法
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormProps<T extends Record<string, any>> {
|
||||||
|
children: React.ReactNode
|
||||||
|
form: T
|
||||||
|
scrollRef: React.RefObject<ScrollView>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormContextType<T = any> {
|
||||||
|
form: T
|
||||||
|
registerValidator: (name: string, validator: () => boolean) => void
|
||||||
|
unregisterValidator: (name: string) => void
|
||||||
|
registerFieldPosition: (name: string, y: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormContext = createContext<FormContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
const Form = forwardRef<FormRef, FormProps<any>>(({ children, form, scrollRef }, ref) => {
|
||||||
|
// 使用 Array 来代替 Map
|
||||||
|
const validatorsRef = useRef<Array<{ name: string, validator: () => boolean }>>([]) // 持有验证器顺序
|
||||||
|
const fieldPositions = useRef<Map<string, number>>(new Map()) // 用来存储每个字段的 Y 坐标
|
||||||
|
|
||||||
|
const registerValidator = useCallback((name: string, validator: () => boolean) => {
|
||||||
|
console.log(`注册:${name}`)
|
||||||
|
validatorsRef.current.push({ name, validator })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const unregisterValidator = useCallback((name: string) => {
|
||||||
|
console.log(`移除:${name}`)
|
||||||
|
validatorsRef.current = validatorsRef.current.filter(validator => validator.name !== name)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const registerFieldPosition = useCallback((name: string, y: number) => {
|
||||||
|
fieldPositions.current.set(name, y)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
let firstErrorField: string | null = null
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
// 按顺序遍历所有的验证器
|
||||||
|
for (const { name, validator } of validatorsRef.current) {
|
||||||
|
const ok = validator()
|
||||||
|
|
||||||
|
if (!ok && !firstErrorField) {
|
||||||
|
// 如果遇到第一个验证失败的字段,记录下来
|
||||||
|
firstErrorField = name
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
return true
|
||||||
|
|
||||||
|
// 滚动到第一个错误字段
|
||||||
|
if (firstErrorField) {
|
||||||
|
scrollToField(firstErrorField)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToField = (name: string) => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
const y = fieldPositions.current.get(name)
|
||||||
|
if (y !== undefined && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTo({ y, animated: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({ validate, scrollToField }))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormContext.Provider
|
||||||
|
value={{
|
||||||
|
form,
|
||||||
|
registerValidator,
|
||||||
|
unregisterValidator,
|
||||||
|
registerFieldPosition,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FormContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Form
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlError,
|
||||||
|
FormControlErrorIcon,
|
||||||
|
FormControlErrorText,
|
||||||
|
FormControlLabel,
|
||||||
|
FormControlLabelText,
|
||||||
|
} from '@/components/ui/form-control'
|
||||||
|
import { AlertCircleIcon } from '@/components/ui/icon'
|
||||||
|
import { FormContext } from './Form'
|
||||||
|
|
||||||
|
interface FormItemProps<T> {
|
||||||
|
name: keyof T
|
||||||
|
label?: string
|
||||||
|
rules?: ((val: any) => string | null)[]
|
||||||
|
children: React.ReactNode
|
||||||
|
isRequired?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormItem<T extends Record<string, any>>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
rules = [],
|
||||||
|
children,
|
||||||
|
isRequired = false,
|
||||||
|
}: FormItemProps<T>) {
|
||||||
|
const context = useContext(FormContext)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!context)
|
||||||
|
return
|
||||||
|
|
||||||
|
const { registerValidator, unregisterValidator } = context
|
||||||
|
|
||||||
|
const validator = () => {
|
||||||
|
const value = context.form[name]
|
||||||
|
|
||||||
|
if (isRequired && (value === undefined || value === null || value === '' || value.length === 0)) {
|
||||||
|
setError(`${String(label || name)} 是必填项`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const msg = rule(value)
|
||||||
|
if (msg) {
|
||||||
|
setError(msg)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
registerValidator(String(name), validator)
|
||||||
|
return () => unregisterValidator(String(name))
|
||||||
|
}, [name])
|
||||||
|
|
||||||
|
const handleLayout = (event: any) => {
|
||||||
|
const { y } = event.nativeEvent.layout
|
||||||
|
context?.registerFieldPosition(String(name), y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isInvalid={!!error} isRequired={isRequired} className="w-full mb-4" onLayout={handleLayout}>
|
||||||
|
{label && (
|
||||||
|
<FormControlLabel>
|
||||||
|
<FormControlLabelText>{label}</FormControlLabelText>
|
||||||
|
</FormControlLabel>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{error && (
|
||||||
|
<FormControlError>
|
||||||
|
<FormControlErrorIcon as={AlertCircleIcon} className="text-red-500" />
|
||||||
|
<FormControlErrorText className="text-red-500">{error}</FormControlErrorText>
|
||||||
|
</FormControlError>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormItem
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ComponentType } from 'react'
|
||||||
|
import type { StyleProp, ViewStyle } from 'react-native'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { EyeIcon, EyeOffIcon } from '@/components/ui/icon'
|
||||||
|
import { InputField, InputIcon, InputSlot, Input as NativeInput } from '@/components/ui/input'
|
||||||
|
|
||||||
|
export interface AInputProps {
|
||||||
|
value: string
|
||||||
|
onChange?: (text: string) => void
|
||||||
|
isPassword?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
isReadOnly?: boolean
|
||||||
|
isInvalid?: boolean
|
||||||
|
isHovered?: boolean
|
||||||
|
isFocused?: boolean
|
||||||
|
variant?: 'outline' | 'rounded' | 'underlined'
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
placeholder?: string
|
||||||
|
leftIcon?: ComponentType
|
||||||
|
className?: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
isPassword = false,
|
||||||
|
isDisabled = false,
|
||||||
|
isReadOnly = false,
|
||||||
|
isInvalid = false,
|
||||||
|
isHovered = false,
|
||||||
|
isFocused = false,
|
||||||
|
variant = 'outline',
|
||||||
|
size = 'md',
|
||||||
|
placeholder,
|
||||||
|
leftIcon,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}: AInputProps) {
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NativeInput
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
isHovered={isHovered}
|
||||||
|
isFocused={isFocused}
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{leftIcon
|
||||||
|
? (
|
||||||
|
<InputSlot className="pl-3">
|
||||||
|
<InputIcon as={leftIcon} />
|
||||||
|
</InputSlot>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
<InputField
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={isPassword ? 'password' : 'text'}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{isPassword && (
|
||||||
|
<InputSlot className="pr-3" onPress={() => setShowPassword(prev => !prev)}>
|
||||||
|
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
|
||||||
|
</InputSlot>
|
||||||
|
)}
|
||||||
|
</NativeInput>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Form, { FormRef } from './Form'
|
||||||
|
import FormItem from './FormItem'
|
||||||
|
import Input from './Input'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Form as AForm,
|
||||||
|
FormItem as AFormItem,
|
||||||
|
FormRef as AFormRef,
|
||||||
|
Input as AInput,
|
||||||
|
}
|
||||||
@@ -65,14 +65,7 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollView
|
<View style={{ flex: 1 }}>{children}</View>
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
keyboardShouldPersistTaps="handled"
|
|
||||||
>
|
|
||||||
<View style={{ flex: 1 }}>{children}</View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
'use client';
|
'use client'
|
||||||
import React from 'react';
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
|
||||||
import { createInput } from '@gluestack-ui/core/input/creator';
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'
|
||||||
import { View, Pressable, TextInput } from 'react-native';
|
import { createInput } from '@gluestack-ui/core/input/creator'
|
||||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'
|
||||||
import {
|
|
||||||
withStyleContext,
|
|
||||||
useStyleContext,
|
|
||||||
} from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { cssInterop } from 'nativewind';
|
|
||||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
|
||||||
|
|
||||||
const SCOPE = 'INPUT';
|
import { cssInterop } from 'nativewind'
|
||||||
|
import React from 'react'
|
||||||
|
import { Pressable, TextInput, View } from 'react-native'
|
||||||
|
|
||||||
|
const SCOPE = 'INPUT'
|
||||||
|
|
||||||
const UIInput = createInput({
|
const UIInput = createInput({
|
||||||
Root: withStyleContext(View, SCOPE),
|
Root: withStyleContext(View, SCOPE),
|
||||||
Icon: UIIcon,
|
Icon: UIIcon,
|
||||||
Slot: Pressable,
|
Slot: Pressable,
|
||||||
Input: TextInput,
|
Input: TextInput,
|
||||||
});
|
})
|
||||||
|
|
||||||
cssInterop(PrimitiveIcon, {
|
cssInterop(PrimitiveIcon, {
|
||||||
className: {
|
className: {
|
||||||
@@ -31,7 +28,7 @@ cssInterop(PrimitiveIcon, {
|
|||||||
stroke: true,
|
stroke: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const inputStyle = tva({
|
const inputStyle = tva({
|
||||||
base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center',
|
base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center',
|
||||||
@@ -55,7 +52,7 @@ const inputStyle = tva({
|
|||||||
'rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
|
'rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const inputIconStyle = tva({
|
const inputIconStyle = tva({
|
||||||
base: 'justify-center items-center text-typography-400 fill-none',
|
base: 'justify-center items-center text-typography-400 fill-none',
|
||||||
@@ -69,11 +66,11 @@ const inputIconStyle = tva({
|
|||||||
'xl': 'h-6 w-6',
|
'xl': 'h-6 w-6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const inputSlotStyle = tva({
|
const inputSlotStyle = tva({
|
||||||
base: 'justify-center items-center web:disabled:cursor-not-allowed',
|
base: 'justify-center items-center web:disabled:cursor-not-allowed',
|
||||||
});
|
})
|
||||||
|
|
||||||
const inputFieldStyle = tva({
|
const inputFieldStyle = tva({
|
||||||
base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
|
base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
|
||||||
@@ -99,15 +96,15 @@ const inputFieldStyle = tva({
|
|||||||
'6xl': 'text-6xl',
|
'6xl': 'text-6xl',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
type IInputProps = React.ComponentProps<typeof UIInput> &
|
type IInputProps = React.ComponentProps<typeof UIInput>
|
||||||
VariantProps<typeof inputStyle> & { className?: string };
|
& VariantProps<typeof inputStyle> & { className?: string }
|
||||||
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
|
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
|
||||||
function Input(
|
(
|
||||||
{ className, variant = 'outline', size = 'md', ...props },
|
{ className, variant = 'outline', size = 'md', ...props },
|
||||||
ref
|
ref,
|
||||||
) {
|
) => {
|
||||||
return (
|
return (
|
||||||
<UIInput
|
<UIInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -115,22 +112,22 @@ const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
|
|||||||
className={inputStyle({ variant, size, class: className })}
|
className={inputStyle({ variant, size, class: className })}
|
||||||
context={{ variant, size }}
|
context={{ variant, size }}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> &
|
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon>
|
||||||
VariantProps<typeof inputIconStyle> & {
|
& VariantProps<typeof inputIconStyle> & {
|
||||||
className?: string;
|
className?: string
|
||||||
height?: number;
|
height?: number
|
||||||
width?: number;
|
width?: number
|
||||||
};
|
}
|
||||||
|
|
||||||
const InputIcon = React.forwardRef<
|
const InputIcon = React.forwardRef<
|
||||||
React.ComponentRef<typeof UIInput.Icon>,
|
React.ComponentRef<typeof UIInput.Icon>,
|
||||||
IInputIconProps
|
IInputIconProps
|
||||||
>(function InputIcon({ className, size, ...props }, ref) {
|
>(({ className, size, ...props }, ref) => {
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
const { size: parentSize } = useStyleContext(SCOPE)
|
||||||
|
|
||||||
if (typeof size === 'number') {
|
if (typeof size === 'number') {
|
||||||
return (
|
return (
|
||||||
@@ -140,10 +137,11 @@ const InputIcon = React.forwardRef<
|
|||||||
className={inputIconStyle({ class: className })}
|
className={inputIconStyle({ class: className })}
|
||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
} else if (
|
}
|
||||||
(props.height !== undefined || props.width !== undefined) &&
|
else if (
|
||||||
size === undefined
|
(props.height !== undefined || props.width !== undefined)
|
||||||
|
&& size === undefined
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<UIInput.Icon
|
<UIInput.Icon
|
||||||
@@ -151,7 +149,7 @@ const InputIcon = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
className={inputIconStyle({ class: className })}
|
className={inputIconStyle({ class: className })}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<UIInput.Icon
|
<UIInput.Icon
|
||||||
@@ -164,16 +162,16 @@ const InputIcon = React.forwardRef<
|
|||||||
class: className,
|
class: className,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> &
|
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot>
|
||||||
VariantProps<typeof inputSlotStyle> & { className?: string };
|
& VariantProps<typeof inputSlotStyle> & { className?: string }
|
||||||
|
|
||||||
const InputSlot = React.forwardRef<
|
const InputSlot = React.forwardRef<
|
||||||
React.ComponentRef<typeof UIInput.Slot>,
|
React.ComponentRef<typeof UIInput.Slot>,
|
||||||
IInputSlotProps
|
IInputSlotProps
|
||||||
>(function InputSlot({ className, ...props }, ref) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<UIInput.Slot
|
<UIInput.Slot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -182,17 +180,17 @@ const InputSlot = React.forwardRef<
|
|||||||
class: className,
|
class: className,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> &
|
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input>
|
||||||
VariantProps<typeof inputFieldStyle> & { className?: string };
|
& VariantProps<typeof inputFieldStyle> & { className?: string }
|
||||||
|
|
||||||
const InputField = React.forwardRef<
|
const InputField = React.forwardRef<
|
||||||
React.ComponentRef<typeof UIInput.Input>,
|
React.ComponentRef<typeof UIInput.Input>,
|
||||||
IInputFieldProps
|
IInputFieldProps
|
||||||
>(function InputField({ className, ...props }, ref) {
|
>(({ className, ...props }, ref) => {
|
||||||
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE);
|
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UIInput.Input
|
<UIInput.Input
|
||||||
@@ -206,12 +204,12 @@ const InputField = React.forwardRef<
|
|||||||
class: className,
|
class: className,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
Input.displayName = 'Input';
|
Input.displayName = 'Input'
|
||||||
InputIcon.displayName = 'InputIcon';
|
InputIcon.displayName = 'InputIcon'
|
||||||
InputSlot.displayName = 'InputSlot';
|
InputSlot.displayName = 'InputSlot'
|
||||||
InputField.displayName = 'InputField';
|
InputField.displayName = 'InputField'
|
||||||
|
|
||||||
export { Input, InputField, InputIcon, InputSlot };
|
export { Input, InputField, InputIcon, InputSlot }
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
// 定义表单字段的验证规则类型
|
||||||
|
interface ValidationRule {
|
||||||
|
required?: boolean
|
||||||
|
pattern?: RegExp
|
||||||
|
minLength?: number
|
||||||
|
maxLength?: number
|
||||||
|
validator?: (value: string) => boolean // 自定义验证规则
|
||||||
|
message?: string // 自定义消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义返回的数据结构
|
||||||
|
interface FormValidationResult {
|
||||||
|
[key: string]: {
|
||||||
|
isValid: boolean
|
||||||
|
errorMessage: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFormValidation(values: Record<string, string>, rules: Record<string, ValidationRule>, autoValidate = false) {
|
||||||
|
const [validationResults, setValidationResults] = useState<FormValidationResult>({})
|
||||||
|
const [isValid, setIsValid] = useState<boolean>(true) // 用于保存整体表单是否有效
|
||||||
|
|
||||||
|
// 验证逻辑
|
||||||
|
const validate = () => {
|
||||||
|
const newValidationResults: FormValidationResult = {}
|
||||||
|
|
||||||
|
for (const field in rules) {
|
||||||
|
const value = values[field] || ''
|
||||||
|
const fieldRules = rules[field]
|
||||||
|
let isValid = true
|
||||||
|
let errorMessage = ''
|
||||||
|
|
||||||
|
// 默认错误消息的函数
|
||||||
|
const getDefaultMessage = (type: string) => {
|
||||||
|
return `${field} 是${type}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 required
|
||||||
|
if (fieldRules.required && !value) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = fieldRules.message || getDefaultMessage('必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 pattern
|
||||||
|
if (isValid && fieldRules.pattern && !fieldRules.pattern.test(value)) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = fieldRules.message || '格式不正确'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 minLength
|
||||||
|
if (isValid && fieldRules.minLength && value.length < fieldRules.minLength) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = fieldRules.message || `最少输入 ${fieldRules.minLength} 个字符`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 maxLength
|
||||||
|
if (isValid && fieldRules.maxLength && value.length > fieldRules.maxLength) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = fieldRules.message || `最多输入 ${fieldRules.maxLength} 个字符`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义验证
|
||||||
|
if (isValid && fieldRules.validator && !fieldRules.validator(value)) {
|
||||||
|
isValid = false
|
||||||
|
errorMessage = fieldRules.message || '自定义验证未通过'
|
||||||
|
}
|
||||||
|
|
||||||
|
newValidationResults[field] = { isValid, errorMessage }
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationResults(newValidationResults)
|
||||||
|
|
||||||
|
// 更新整体表单是否有效
|
||||||
|
const allValid = !Object.values(newValidationResults).some(result => !result.isValid)
|
||||||
|
setIsValid(allValid)
|
||||||
|
|
||||||
|
// 返回是否验证通过(返回true或false)
|
||||||
|
return allValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果开启自动验证
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoValidate) {
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
}, [values, autoValidate])
|
||||||
|
|
||||||
|
return {
|
||||||
|
validationResults,
|
||||||
|
isValid,
|
||||||
|
validate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFormValidation
|
||||||
Reference in new issue
Block a user