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 (
|
||||
<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
|
||||
Reference in new issue
Block a user