This commit is contained in:
oneao committed 2025-11-05 17:30:35 +08:00
1 parent ca2fa3543d
commit 7333e6695f
13 files changed
+211 -105

No files matched your search

@@ -17,6 +17,7 @@ import {
} from '@/components/ui/form-control'
import { CheckIcon, EyeIcon, EyeOffIcon } from '@/components/ui/icon'
import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input'
import alovaInstance from '@/request'
import { useToast } from '@/utils/toast'
interface LoginForm {
@@ -37,20 +38,13 @@ export default function AuthLogin() {
})
const defaultLogin = () => {
if (form.username === '') {
toast.show({
title: '账号不能为空!',
action: 'error',
})
return
}
if (form.password === '') {
toast.show({
title: '密码不能为空!',
action: 'error',
})
}
//
alovaInstance.Post('/auth/login/default', {
title: 'foo',
body: 'bar',
userId: 1,
}).then((res) => {
console.log(res)
})
}
return (
@@ -1,6 +1,6 @@
import type { AFormRef } from '@/components/common/ao'
import { Lock, UserRound } from 'lucide-react-native'
import { useEffect, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { Image, ScrollView, View } from 'react-native'
import { AForm, AFormItem, AInput } from '@/components/common/ao'
@@ -11,28 +11,16 @@ interface RegisterForm {
username: string
password: string
confirmPassword: string
a: string
b: string
c: string
d: string
e: string
f: string
}
export default function AuthLogin() {
export default function AuthRegister() {
const scrollRef = useRef<ScrollView>(null)
const toast = useToast()
const [form, setForm] = useState<RegisterForm>({
username: '',
password: '',
confirmPassword: '',
a: '',
b: '',
c: '',
d: '',
e: '',
f: '',
})
const [loading, setLoading] = useState(false)
const formRef = useRef<AFormRef>(null)
@@ -41,7 +29,7 @@ export default function AuthLogin() {
setForm(prev => ({ ...prev, [field]: value }))
}
const defaultLogin = () => {
const handleRegister = () => {
if (formRef.current?.validate()) {
setLoading(true)
toast.show({ title: '校验通过,开始注册', action: 'success' })
@@ -55,10 +43,18 @@ export default function AuthLogin() {
return (
<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" />
<Image
source={require('@/static/images/logo.png')}
style={{ width: 120, height: 120 }}
className="my-12"
/>
<AForm ref={formRef} form={form} scrollRef={scrollRef}>
<AFormItem name="username" label="账号" isRequired>
<AFormItem
name="username"
label="账号"
isRequired
>
<AInput
placeholder="请输入账号"
leftIcon={UserRound}
@@ -70,9 +66,9 @@ export default function AuthLogin() {
<AFormItem
name="password"
label="密码"
isRequired
rules={[
(val: any) => val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null,
{ required: true, message: '密码是必填项' },
{ pattern: /^\d{6}$/, message: '请输入6位数字密码' },
]}
>
<AInput
@@ -87,9 +83,12 @@ export default function AuthLogin() {
<AFormItem
name="confirmPassword"
label="确认密码"
isRequired
rules={[
(val: any) => val !== form.password ? '两次密码不一致' : null,
{ required: true, message: '确认密码是必填项' },
{
validator: val => val === form.password,
message: '两次密码不一致',
},
]}
>
<AInput
@@ -100,21 +99,9 @@ export default function AuthLogin() {
onChange={text => handleInputChange('confirmPassword', text)}
/>
</AFormItem>
{/* 其他表单项 */}
{['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}>
<Button className="w-full mb-4" onPress={handleRegister} disabled={loading}>
{loading
? (
<>
@@ -1,19 +1,19 @@
import type { ScrollView } from 'react-native'
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
import React, { createContext, forwardRef, useCallback, useImperativeHandle, useRef } from 'react'
export interface FormRef {
validate: () => boolean
scrollToField: (name: string) => void // 新增的滚动方法
scrollToField: (name: string) => void
}
interface FormProps<T extends Record<string, any>> {
interface FormProps {
children: React.ReactNode
form: T
scrollRef: React.RefObject<ScrollView>
form: Record<string, any>
scrollRef: React.RefObject<ScrollView | null>
}
interface FormContextType<T = any> {
form: T
interface FormContextType {
form: Record<string, any>
registerValidator: (name: string, validator: () => boolean) => void
unregisterValidator: (name: string) => void
registerFieldPosition: (name: string, y: number) => void
@@ -21,19 +21,16 @@ interface FormContextType<T = any> {
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 Form = forwardRef<FormRef, FormProps>(({ children, form, scrollRef }, ref) => {
const validatorsRef = useRef<Array<{ name: string, validator: () => boolean }>>([])
const fieldPositions = useRef<Map<string, number>>(new Map())
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)
validatorsRef.current = validatorsRef.current.filter(v => v.name !== name)
}, [])
const registerFieldPosition = useCallback((name: string, y: number) => {
@@ -44,34 +41,24 @@ const Form = forwardRef<FormRef, FormProps<any>>(({ children, form, scrollRef },
let firstErrorField: string | null = null
let isValid = true
// 按顺序遍历所有的验证器
for (const { name, validator } of validatorsRef.current) {
const ok = validator()
if (!ok && !firstErrorField) {
// 如果遇到第一个验证失败的字段,记录下来
if (!validator() && !firstErrorField) {
firstErrorField = name
isValid = false
}
}
if (isValid)
return true
// 滚动到第一个错误字段
if (firstErrorField) {
if (!isValid && firstErrorField) {
scrollToField(firstErrorField)
}
return false
return isValid
}
const scrollToField = (name: string) => {
if (scrollRef.current) {
const y = fieldPositions.current.get(name)
if (y !== undefined && scrollRef.current) {
scrollRef.current.scrollTo({ y, animated: true })
}
const y = fieldPositions.current.get(name)
if (y !== undefined && scrollRef.current) {
scrollRef.current.scrollTo({ y, animated: true })
}
}
@@ -1,5 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'
import { View } from 'react-native'
import {
FormControl,
FormControlError,
@@ -11,10 +10,21 @@ import {
import { AlertCircleIcon } from '@/components/ui/icon'
import { FormContext } from './Form'
interface RuleType {
validator?: (val: any) => boolean
pattern?: RegExp
message?: string
len?: number
min?: number
max?: number
required?: boolean
whitespace?: boolean
}
interface FormItemProps<T> {
name: keyof T
label?: string
rules?: ((val: any) => string | null)[]
rules?: RuleType[]
children: React.ReactNode
isRequired?: boolean
}
@@ -33,21 +43,87 @@ function FormItem<T extends Record<string, any>>({
if (!context)
return
const { registerValidator, unregisterValidator } = context
const currentName = String(name)
const validator = () => {
const value = context.form[name]
// 如果 isRequired 为 true,且 rules 中没有 required: true,则自动加
const hasRequiredInRules = rules.some(rule => rule.required)
const allRules = [...rules]
if (isRequired && !hasRequiredInRules) {
allRules.unshift({
required: true,
message: label ? `${label} 是必填项` : `${currentName} 是必填项`,
})
}
if (isRequired && (value === undefined || value === null || value === '' || value.length === 0)) {
setError(`${String(label || name)} 是必填项`)
return false
}
const validateFn = () => {
const value = context.form[currentName]
for (const rule of rules) {
const msg = rule(value)
if (msg) {
setError(msg)
return false
for (const rule of allRules) {
// 1. required 校验
if (rule.required) {
const empty
= value === undefined
|| value === null
|| value === ''
|| (Array.isArray(value) && value.length === 0)
|| (rule.whitespace && typeof value === 'string' && value.trim() === '')
if (empty) {
setError(rule.message || `${label || currentName} 校验不通过`)
return false
}
}
// 2. 自定义 validator 返回 true/false
if (rule.validator !== undefined) {
const ok = rule.validator(value)
if (!ok) {
setError(rule.message || `${label || currentName} 校验不通过`)
return false
}
}
// 3. pattern 正则
if (rule.pattern && typeof value === 'string') {
if (!rule.pattern.test(value)) {
setError(rule.message || `${label || currentName} 格式不正确`)
return false
}
}
// 4. len
if (rule.len !== undefined) {
if (
(typeof value === 'string' && value.length !== rule.len)
|| (typeof value === 'number' && value !== rule.len)
|| (Array.isArray(value) && value.length !== rule.len)
) {
setError(rule.message || `${label || currentName} 长度/值必须为 ${rule.len}`)
return false
}
}
// 5. min
if (rule.min !== undefined) {
if (
(typeof value === 'string' && value.length < rule.min)
|| (typeof value === 'number' && value < rule.min)
|| (Array.isArray(value) && value.length < rule.min)
) {
setError(rule.message || `${label || currentName} 最小值/长度为 ${rule.min}`)
return false
}
}
// 6. max
if (rule.max !== undefined) {
if (
(typeof value === 'string' && value.length > rule.max)
|| (typeof value === 'number' && value > rule.max)
|| (Array.isArray(value) && value.length > rule.max)
) {
setError(rule.message || `${label || currentName} 最大值/长度为 ${rule.max}`)
return false
}
}
}
@@ -55,9 +131,12 @@ function FormItem<T extends Record<string, any>>({
return true
}
registerValidator(String(name), validator)
return () => unregisterValidator(String(name))
}, [name])
context.registerValidator(currentName, validateFn)
return () => {
context.unregisterValidator(currentName)
}
}, [context, name, label, rules, isRequired])
const handleLayout = (event: any) => {
const { y } = event.nativeEvent.layout
@@ -0,0 +1,21 @@
import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client'
import adapterFetch from 'alova/fetch'
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
// ...
})
const alovaInstance = createAlova({
baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: adapterFetch(),
// ...
beforeRequest: onAuthRequired((method) => {
// ...原请求前拦截器
}),
responded: onResponseRefreshToken((response, method) => {
// ...原响应成功拦截器
return response.json()
}),
})
export default alovaInstance