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 ( 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