This commit is contained in:
oneao committed 2025-11-05 15:57:11 +08:00
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 (
<View>
<Text>Love</Text>
<View style={{ flex: 1, paddingTop: 40 }}>
<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>
)
}
@@ -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 { useRef, useState } from 'react'
import { Image, View } from 'react-native'
import FormWrapper from '@/components/common/FormWrapper'
import { useEffect, useRef, useState } from 'react'
import { Image, ScrollView, View } from 'react-native'
import { AForm, AFormItem, AInput } from '@/components/common/ao'
import { Button, ButtonSpinner, ButtonText } from '@/components/ui/button'
import { useToast } from '@/utils/toast'
@@ -13,9 +14,14 @@ interface RegisterForm {
a: string
b: string
c: string
d: string
e: string
f: string
}
export default function AuthLogin() {
const scrollRef = useRef<ScrollView>(null)
const toast = useToast()
const [form, setForm] = useState<RegisterForm>({
username: '',
@@ -24,9 +30,12 @@ export default function AuthLogin() {
a: '',
b: '',
c: '',
d: '',
e: '',
f: '',
})
const [loading, setLoading] = useState(false)
const formRef = useRef<FormWrapperRef>(null)
const formRef = useRef<AFormRef>(null)
const handleInputChange = (field: keyof RegisterForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }))
@@ -44,106 +53,80 @@ export default function AuthLogin() {
}
return (
<View className="flex flex-col items-center px-6 h-full mb-4">
<Image source={require('@/static/images/logo.png')} style={{ width: 120, height: 120 }} className="my-12" />
<ScrollView ref={scrollRef} style={{ flex: 1 }}>
<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}>
<FormWrapper.FormItem
name="username"
label="账号"
required
>
<FormWrapper.Input
placeholder="请输入账号"
leftIcon={UserRound}
value={form.username}
onChangeText={text => handleInputChange('username', text)}
/>
</FormWrapper.FormItem>
<AForm ref={formRef} form={form} scrollRef={scrollRef}>
<AFormItem name="username" label="账号" isRequired>
<AInput
placeholder="请输入账号"
leftIcon={UserRound}
value={form.username}
onChange={text => handleInputChange('username', text)}
/>
</AFormItem>
<FormWrapper.FormItem
name="password"
label="密码"
required
rules={[
(val: any) => val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null,
]}
>
<FormWrapper.Input
placeholder="请输入密码"
leftIcon={Lock}
isPassword
value={form.password}
onChangeText={text => handleInputChange('password', text)}
/>
</FormWrapper.FormItem>
<AFormItem
name="password"
label="密码"
isRequired
rules={[
(val: any) => val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null,
]}
>
<AInput
placeholder="请输入密码"
leftIcon={Lock}
isPassword
value={form.password}
onChange={text => handleInputChange('password', text)}
/>
</AFormItem>
<FormWrapper.FormItem
name="confirmPassword"
label="确认密码"
required
rules={[
(val: any) => val !== form.password ? '两次密码不一致' : null,
]}
>
<FormWrapper.Input
placeholder="请再次输入密码"
leftIcon={Lock}
isPassword
value={form.confirmPassword}
onChangeText={text => handleInputChange('confirmPassword', text)}
/>
</FormWrapper.FormItem>
<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>
<AFormItem
name="confirmPassword"
label="确认密码"
isRequired
rules={[
(val: any) => val !== form.password ? '两次密码不一致' : null,
]}
>
<AInput
placeholder="请再次输入密码"
leftIcon={Lock}
isPassword
value={form.confirmPassword}
onChange={text => handleInputChange('confirmPassword', text)}
/>
</AFormItem>
<Button className="w-full mt-4" onPress={defaultLogin} disabled={loading}>
{loading
? (
<>
<ButtonSpinner color="gray" />
<ButtonText>请稍等 . . .</ButtonText>
</>
)
: (
<ButtonText>注册</ButtonText>
)}
</Button>
</View>
{/* 其他表单项 */}
{['a', 'b', 'c', 'd', 'e', 'f'].map(field => (
<AFormItem key={field} name={field} label={field} isRequired>
<AInput
placeholder="请输入"
isPassword
value={form[field]}
onChange={text => handleInputChange(field, text)}
/>
</AFormItem>
))}
</AForm>
<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>
)}
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={{ flex: 1 }}>{children}</View>
</ScrollView>
<View style={{ flex: 1 }}>{children}</View>
</Animated.View>
</SafeAreaView>
)
@@ -1,24 +1,21 @@
'use client';
import React from 'react';
import { createInput } from '@gluestack-ui/core/input/creator';
import { View, Pressable, TextInput } from 'react-native';
import { tva } 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';
'use client'
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'
import { createInput } from '@gluestack-ui/core/input/creator'
import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'
const SCOPE = 'INPUT';
import { cssInterop } from 'nativewind'
import React from 'react'
import { Pressable, TextInput, View } from 'react-native'
const SCOPE = 'INPUT'
const UIInput = createInput({
Root: withStyleContext(View, SCOPE),
Icon: UIIcon,
Slot: Pressable,
Input: TextInput,
});
})
cssInterop(PrimitiveIcon, {
className: {
@@ -31,7 +28,7 @@ cssInterop(PrimitiveIcon, {
stroke: true,
},
},
});
})
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',
@@ -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',
},
},
});
})
const inputIconStyle = tva({
base: 'justify-center items-center text-typography-400 fill-none',
@@ -69,11 +66,11 @@ const inputIconStyle = tva({
'xl': 'h-6 w-6',
},
},
});
})
const inputSlotStyle = tva({
base: 'justify-center items-center web:disabled:cursor-not-allowed',
});
})
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',
@@ -99,15 +96,15 @@ const inputFieldStyle = tva({
'6xl': 'text-6xl',
},
},
});
})
type IInputProps = React.ComponentProps<typeof UIInput> &
VariantProps<typeof inputStyle> & { className?: string };
type IInputProps = React.ComponentProps<typeof UIInput>
& VariantProps<typeof inputStyle> & { className?: string }
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
function Input(
(
{ className, variant = 'outline', size = 'md', ...props },
ref
) {
ref,
) => {
return (
<UIInput
ref={ref}
@@ -115,22 +112,22 @@ const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
className={inputStyle({ variant, size, class: className })}
context={{ variant, size }}
/>
);
}
);
)
},
)
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> &
VariantProps<typeof inputIconStyle> & {
className?: string;
height?: number;
width?: number;
};
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon>
& VariantProps<typeof inputIconStyle> & {
className?: string
height?: number
width?: number
}
const InputIcon = React.forwardRef<
React.ComponentRef<typeof UIInput.Icon>,
IInputIconProps
>(function InputIcon({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE)
if (typeof size === 'number') {
return (
@@ -140,10 +137,11 @@ const InputIcon = React.forwardRef<
className={inputIconStyle({ class: className })}
size={size}
/>
);
} else if (
(props.height !== undefined || props.width !== undefined) &&
size === undefined
)
}
else if (
(props.height !== undefined || props.width !== undefined)
&& size === undefined
) {
return (
<UIInput.Icon
@@ -151,7 +149,7 @@ const InputIcon = React.forwardRef<
{...props}
className={inputIconStyle({ class: className })}
/>
);
)
}
return (
<UIInput.Icon
@@ -164,16 +162,16 @@ const InputIcon = React.forwardRef<
class: className,
})}
/>
);
});
)
})
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> &
VariantProps<typeof inputSlotStyle> & { className?: string };
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot>
& VariantProps<typeof inputSlotStyle> & { className?: string }
const InputSlot = React.forwardRef<
React.ComponentRef<typeof UIInput.Slot>,
IInputSlotProps
>(function InputSlot({ className, ...props }, ref) {
>(({ className, ...props }, ref) => {
return (
<UIInput.Slot
ref={ref}
@@ -182,17 +180,17 @@ const InputSlot = React.forwardRef<
class: className,
})}
/>
);
});
)
})
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> &
VariantProps<typeof inputFieldStyle> & { className?: string };
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input>
& VariantProps<typeof inputFieldStyle> & { className?: string }
const InputField = React.forwardRef<
React.ComponentRef<typeof UIInput.Input>,
IInputFieldProps
>(function InputField({ className, ...props }, ref) {
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE);
>(({ className, ...props }, ref) => {
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE)
return (
<UIInput.Input
@@ -206,12 +204,12 @@ const InputField = React.forwardRef<
class: className,
})}
/>
);
});
)
})
Input.displayName = 'Input';
InputIcon.displayName = 'InputIcon';
InputSlot.displayName = 'InputSlot';
InputField.displayName = 'InputField';
Input.displayName = 'Input'
InputIcon.displayName = 'InputIcon'
InputSlot.displayName = 'InputSlot'
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