This commit is contained in:
oneao committed 2025-11-06 22:50:03 +08:00
1 parent d60be4a75b
commit be8eaae7c5
24 files changed
+595 -232

No files matched your search

@@ -1,10 +1,8 @@
import type { TabbarItem } from '@/components/custom/Tabbar'
import CustomTabbar from '@/components/custom/Tabbar'
import { useToast } from '@/utils/toast'
import { toast } from '@/utils/toast'
export default function TabsLayout() {
const toast = useToast()
const tabbarOptions: TabbarItem[] = [
{
name: 'user',
@@ -1,41 +1,39 @@
import { Lock, UserRound } from 'lucide-react-native'
import { useState } from 'react'
import { Image, Text, View } from 'react-native'
import { AForm, AFormItem, AInput } from '@/components/common/ao'
import { AButton, ACheckbox, AForm, AFormItem, AInput } from '@/components/common/ao'
import { IconQq, IconWeixin, IconZhiwen } from '@/components/iconfont'
import { Button, ButtonText } from '@/components/ui/button'
import {
Checkbox,
CheckboxIcon,
CheckboxIndicator,
CheckboxLabel,
} from '@/components/ui/checkbox'
import { Divider } from '@/components/ui/divider'
import { CheckIcon } from '@/components/ui/icon'
import useFormValidation from '@/hooks/useFormValidation'
import alovaInstance from '@/request'
import { useToast } from '@/utils/toast'
import { toast } from '@/utils/toast'
interface LoginForm {
username: string
password: string
rememberMe?: boolean
rememberMe: boolean
socialType?: 'qq' | 'weixin' | 'fingerprint'
}
export default function AuthLogin() {
const toast = useToast()
const [form, setForm] = useState<LoginForm>({
username: '',
password: '',
rememberMe: true,
})
const handleInputChange = (field: keyof LoginForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }))
}
const defaultLogin = () => {
// 验证规则
const rules = {
username: { required: true, message: '账号是必填项' },
password: { required: true, message: '密码是必填项' },
}
const { isValid } = useFormValidation(form, rules)
const loginDefault = () => {
alovaInstance.Post('/auth/test', {
title: 'foo',
body: 'bar',
@@ -49,6 +47,16 @@ export default function AuthLogin() {
})
}
const loginQq = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
const loginWx = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
const loginZhiwen = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
return (
<View className="flex flex-col items-center px-6 h-full">
<Image
@@ -64,6 +72,7 @@ export default function AuthLogin() {
onChange={text => handleInputChange('username', text)}
/>
</AFormItem>
<AFormItem label="密码">
<AInput
isPassword
@@ -74,45 +83,34 @@ export default function AuthLogin() {
</AForm>
<View className="w-full flex-row justify-between items-center mb-6">
<Checkbox
value="rememberMe"
<ACheckbox
isChecked={form.rememberMe}
onChange={checked =>
setForm(prev => ({ ...prev, rememberMe: checked }))}
>
<CheckboxIndicator>
<CheckboxIcon as={CheckIcon} />
</CheckboxIndicator>
<CheckboxLabel>记住我</CheckboxLabel>
</Checkbox>
onChange={checked => setForm(prev => ({ ...prev, rememberMe: checked }))}
label="记住我"
/>
<Text className="text-blue-500">
忘记密码?
</Text>
<Text className="text-blue-500">忘记密码?</Text>
</View>
<Button className="w-full" onPress={() => defaultLogin()}>
<ButtonText>登录</ButtonText>
</Button>
<AButton className="w-full" onPress={loginDefault} disabled={!isValid}>
登录
</AButton>
<View className="flex-row items-center my-8 w-full">
<View className="flex-1">
<Divider />
</View>
<Text className="mx-3 text-gray-500 text-sm">社交账号</Text>
<View className="flex-1">
<Divider />
</View>
</View>
<View className="w-full flex-row justify-between items-center">
<IconQq size={32} />
<IconWeixin size={32} />
<IconZhiwen size={32} />
<IconQq size={32} onPress={loginQq} />
<IconWeixin size={32} onPress={loginWx} />
<IconZhiwen size={32} onPress={loginZhiwen} />
</View>
</View>
)
}
@@ -1,11 +1,12 @@
import type { AFormRef } from '@/components/common/ao'
import { useRouter } from 'expo-router'
import { Lock, UserRound } from 'lucide-react-native'
import { 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'
import { Image, ScrollView, View } from 'react-native'
import { AButton, AForm, AFormItem, AInput } from '@/components/common/ao'
import { registerDefaultApi } from '@/request/api/auth'
import { toast } from '@/utils/toast'
interface RegisterForm {
username: string
@@ -14,8 +15,8 @@ interface RegisterForm {
}
export default function AuthRegister() {
const router = useRouter()
const scrollRef = useRef<ScrollView>(null)
const toast = useToast()
const [form, setForm] = useState<RegisterForm>({
username: '',
@@ -29,14 +30,30 @@ export default function AuthRegister() {
setForm(prev => ({ ...prev, [field]: value }))
}
const handleRegister = () => {
const handleRegister = async () => {
if (formRef.current?.validate()) {
setLoading(true)
toast.show({ title: '校验通过,开始注册', action: 'success' })
// 模拟注册过程
setTimeout(() => {
await registerDefaultApi({
account: form.username,
password: form.password,
}).then((res) => {
if (res.isSuccess) {
console.log(res)
toast.show({
title: '注册成功',
})
// router.replace('/auth/login')
}
else {
toast.show({
title: res.message || '注册失败',
})
}
}).catch((_err) => {
}).finally(() => {
setLoading(false)
}, 2000)
})
}
}
@@ -66,10 +83,7 @@ export default function AuthRegister() {
<AFormItem
name="password"
label="密码"
rules={[
{ required: true, message: '密码是必填项' },
{ pattern: /^\d{6}$/, message: '请输入6位数字密码' },
]}
isRequired
>
<AInput
placeholder="请输入密码"
@@ -83,8 +97,8 @@ export default function AuthRegister() {
<AFormItem
name="confirmPassword"
label="确认密码"
isRequired
rules={[
{ required: true, message: '确认密码是必填项' },
{
validator: val => val === form.password,
message: '两次密码不一致',
@@ -101,18 +115,10 @@ export default function AuthRegister() {
</AFormItem>
</AForm>
<Button className="w-full mb-4" onPress={handleRegister} disabled={loading}>
{loading
? (
<>
<ButtonSpinner color="gray" />
<ButtonText>请稍等 . . .</ButtonText>
</>
)
: (
<ButtonText>注册</ButtonText>
)}
</Button>
<AButton className="w-full mb-4" onPress={handleRegister} loading={loading}>
注册
</AButton>
</View>
</ScrollView>
)
@@ -0,0 +1,54 @@
import type { ViewStyle } from 'react-native'
// components/ui/CustomButton.tsx
import React from 'react'
import {
ButtonSpinner,
ButtonText,
Button as NativeButton,
} from '@/components/ui/button'
interface CustomButtonProps {
children: React.ReactNode
onPress?: () => void
disabled?: boolean
loading?: boolean
className?: string
style?: ViewStyle
action?: 'primary' | 'secondary' | 'positive' | 'negative' | 'default'
variant?: 'solid' | 'outline' | 'link'
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
}
const Button: React.FC<CustomButtonProps> = ({
children,
onPress,
disabled = false,
loading = false,
className,
style,
action = 'primary',
variant = 'solid',
size = 'md',
}) => {
const isDisabled = disabled || loading
return (
<NativeButton
onPress={onPress}
isDisabled={isDisabled}
className={className}
style={[
style,
{ opacity: isDisabled ? 0.8 : 1 },
]}
action={action}
variant={variant}
size={size}
>
{loading && <ButtonSpinner color="#fff" className="mr-2" />}
<ButtonText>{children}</ButtonText>
</NativeButton>
)
}
export default Button
@@ -0,0 +1,50 @@
// components/ui/CustomCheckbox.tsx
import type { ViewStyle } from 'react-native'
import React from 'react'
import {
CheckboxIcon,
CheckboxIndicator,
CheckboxLabel,
Checkbox as NativeCheckbox,
} from '@/components/ui/checkbox'
import { CheckIcon } from '@/components/ui/icon'
interface CustomCheckboxProps {
value?: string
isChecked: boolean
onChange: (checked: boolean) => void
label: React.ReactNode
className?: string
style?: ViewStyle
disabled?: boolean
icon?: React.ElementType
}
const Checkbox: React.FC<CustomCheckboxProps> = ({
value,
isChecked,
onChange,
label,
className,
style,
disabled,
icon,
}) => {
return (
<NativeCheckbox
value={value ?? ''} // 避免 undefined
isChecked={isChecked}
onChange={onChange}
className={className}
style={style}
isDisabled={disabled}
>
<CheckboxIndicator>
<CheckboxIcon as={icon ?? CheckIcon} />
</CheckboxIndicator>
<CheckboxLabel>{label}</CheckboxLabel>
</NativeCheckbox>
)
}
export default Checkbox
@@ -1,8 +1,12 @@
import Button from './Button'
import Checkbox from './Checkbox'
import Form, { FormRef } from './Form'
import FormItem from './FormItem'
import Input from './Input'
export {
Button as AButton,
Checkbox as ACheckbox,
Form as AForm,
FormItem as AFormItem,
FormRef as AFormRef,
@@ -1,67 +1,61 @@
import { useEffect, useState } from 'react'
import { useMemo, useState } from 'react'
// 定义表单字段的验证规则类型
interface ValidationRule {
export interface ValidationRule {
required?: boolean
pattern?: RegExp
minLength?: number
maxLength?: number
validator?: (value: string) => boolean // 自定义验证规则
message?: string // 自定义消息
validator?: (value: any) => boolean
message?: string
}
// 定义返回的数据结构
interface FormValidationResult {
export interface FormValidationResult {
[key: string]: {
isValid: boolean
errorMessage: string
}
}
function useFormValidation(values: Record<string, string>, rules: Record<string, ValidationRule>, autoValidate = false) {
export default function useFormValidation<T extends Record<string, any>>(
values: T,
rules: { [K in keyof T]?: ValidationRule }, // <-- 可选字段,message 可选
) {
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]
const value = values[field]
const strValue = String(value ?? '')
const fieldRules = rules[field as keyof T]
if (!fieldRules)
continue
let isValid = true
let errorMessage = ''
const getDefaultMessage = (type: string) => `${field} 是${type}`
// 默认错误消息的函数
const getDefaultMessage = (type: string) => {
return `${field} 是${type}`
}
// 验证 required
if (fieldRules.required && !value) {
if (fieldRules.required && (value === undefined || value === null || strValue === '')) {
isValid = false
errorMessage = fieldRules.message || getDefaultMessage('必填项')
}
// 验证 pattern
if (isValid && fieldRules.pattern && !fieldRules.pattern.test(value)) {
if (isValid && fieldRules.pattern && !fieldRules.pattern.test(strValue)) {
isValid = false
errorMessage = fieldRules.message || '格式不正确'
}
// 验证 minLength
if (isValid && fieldRules.minLength && value.length < fieldRules.minLength) {
if (isValid && fieldRules.minLength && strValue.length < fieldRules.minLength) {
isValid = false
errorMessage = fieldRules.message || `最少输入 ${fieldRules.minLength} 个字符`
}
// 验证 maxLength
if (isValid && fieldRules.maxLength && value.length > fieldRules.maxLength) {
if (isValid && fieldRules.maxLength && strValue.length > fieldRules.maxLength) {
isValid = false
errorMessage = fieldRules.message || `最多输入 ${fieldRules.maxLength} 个字符`
}
// 自定义验证
if (isValid && fieldRules.validator && !fieldRules.validator(value)) {
isValid = false
errorMessage = fieldRules.message || '自定义验证未通过'
@@ -71,27 +65,31 @@ function useFormValidation(values: Record<string, string>, rules: Record<string,
}
setValidationResults(newValidationResults)
// 更新整体表单是否有效
const allValid = !Object.values(newValidationResults).some(result => !result.isValid)
setIsValid(allValid)
// 返回是否验证通过(返回true或false)
return allValid
return newValidationResults
}
// 如果开启自动验证
useEffect(() => {
if (autoValidate) {
validate()
}
}, [values, autoValidate])
const isValid = useMemo(() => {
return Object.keys(rules).every((field) => {
const value = values[field as keyof T]
const strValue = String(value ?? '')
const fieldRules = rules[field as keyof T]
if (!fieldRules)
return true
return {
validationResults,
isValid,
validate,
}
if (fieldRules.required && (value === undefined || value === null || strValue === ''))
return false
if (fieldRules.pattern && !fieldRules.pattern.test(strValue))
return false
if (fieldRules.minLength && strValue.length < fieldRules.minLength)
return false
if (fieldRules.maxLength && strValue.length > fieldRules.maxLength)
return false
if (fieldRules.validator && !fieldRules.validator(value))
return false
return true
})
}, [values, rules])
return { validationResults, isValid, validate }
}
export default useFormValidation
@@ -0,0 +1,10 @@
import alovaInstance from '../index'
interface RegisterDefaultParams {
account: string
password: string
}
export function registerDefaultApi(params: RegisterDefaultParams) {
return alovaInstance.Post<ResponseData<any>>('/auth/register/default', params)
}
+59 -12
View File
@@ -30,19 +30,22 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.1.136:8085/app',
baseURL: 'http://192.168.5.6:8085/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
responded: {
onSuccess: async (response, method: Method) => {
const resData = response.data as ResponseData<any>
if (resData.code === HttpCode.Success) {
return resData.data
const result = {
...resData,
isSuccess: resData.code === HttpCode.Success,
}
console.log(resData)
// Access Token 失效 → 重新登录
if (resData.code === HttpCode.Unauthorized) {
console.log('===到这一步了===')
router.replace('/auth/login')
return
}
@@ -77,18 +80,61 @@ const alovaInstance = createAlova({
return
}
catch (err) {
console.log('===到这一步了===')
router.replace('/auth/login')
return Promise.reject(err)
}
}
// 其他错误
return Promise.reject(resData.message || '请求失败')
return result
},
onError: (error: AxiosError, method) => {
console.log(error.status)
onError: (error: AxiosError, _method) => {
switch (error.status) {
case 400:
toast.show({
title: '请求错误,请检查输入参数',
})
break
case 401:
toast.show({
title: '未授权,请重新登录',
})
break
case 403:
toast.show({
title: '没有权限访问该资源',
})
break
case 404:
toast.show({
title: '请求丢失啦,请稍后再试!',
})
break
case 500:
toast.show({
title: '服务器出错啦,请稍后重试',
})
break
case 502:
toast.show({
title: '网关错误,请稍后重试',
})
break
case 503:
toast.show({
title: '服务暂不可用,请稍后再试',
})
break
case 504:
toast.show({
title: '网关超时,请稍后再试',
})
break
default:
toast.show({
title: `服务器出错啦,请稍后重试`,
})
break
}
return Promise.reject(error)
},
},
@@ -98,10 +144,11 @@ const alovaInstance = createAlova({
if (!userStore.getState().isLoggedIn()) {
toast.show({
title: '尚未登录',
title: '尚未登录,请先登录',
})
router.replace('/auth/login')
return
return Promise.reject(new Error('未登录'))
}
method.config.headers.token = userStore.getState().userInfo?.token ?? ''
@@ -27,6 +27,8 @@ const useUserStore = create<UserState>()(
},
),
)
// 在组件外使用
const userStore = useUserStore
export { userStore, useUserStore }
+1
View File
@@ -6,6 +6,7 @@ declare global {
code: number
message: string
data: T
isSuccess: boolean
}
interface UserInfo {
@@ -1,8 +0,0 @@
// utils/runOnMain.ts
// ✅ 安全封装 runOnJS,屏蔽 deprecated 警告
import { runOnJS as _runOnJS } from 'react-native-worklets'
// 屏蔽类型定义中的 deprecated 提示
export const runOnMain = ((fn: (...args: any[]) => void) => {
return _runOnJS(fn)
}) as typeof _runOnJS
+16 -66
View File
@@ -1,5 +1,5 @@
import type { IToastProps } from '@gluestack-ui/core/lib/esm/toast/creator/types'
import React, { createContext, useContext, useRef } from 'react'
import React, { useEffect, useRef } from 'react'
import { Toast, ToastDescription, ToastTitle, useToast as useGluestackToast } from '@/components/ui/toast'
type ToastVariant = 'solid' | 'outline'
@@ -9,29 +9,30 @@ interface ShowToastOptions extends IToastProps {
title?: React.ReactNode
description?: React.ReactNode
variant?: ToastVariant
action?: ToastAction
type?: ToastAction
}
// 创建全局 toast 实例
let globalToast: any = null
// 创建 Context 用于在组件内部使用
const ToastContext = createContext<ReturnType<typeof useGluestackToast> | null>(null)
// 全局 toast 实例
let globalToast: {
show: (options: ShowToastOptions) => string
close: (id: string) => void
closeAll: () => void
isActive: (id: string) => boolean
} | null = null
export function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
const toast = useGluestackToast()
const toastRef = useRef(toast)
toastRef.current = toast
// 设置全局实例
React.useEffect(() => {
useEffect(() => {
globalToast = {
show: (options: ShowToastOptions) => {
const id = Math.random().toString()
toast.show({
id,
placement: options.placement ?? 'top',
duration: options.duration !== undefined ? options.duration : 3000,
duration: options.duration ?? 3000,
avoidKeyboard: options.avoidKeyboard ?? false,
containerStyle: options.containerStyle,
onCloseComplete: options.onCloseComplete,
@@ -41,7 +42,7 @@ export function ToastProviderWrapper({ children }: { children: React.ReactNode }
<Toast
nativeID={`toast-${id}`}
variant={options.variant ?? 'solid'}
action={options.action ?? 'muted'}
action={options.type ?? 'muted'}
>
{options.title && <ToastTitle className="font-blod">{options.title}</ToastTitle>}
{options.description && <ToastDescription>{options.description}</ToastDescription>}
@@ -60,60 +61,11 @@ export function ToastProviderWrapper({ children }: { children: React.ReactNode }
}
}, [toast])
return (
<ToastContext.Provider value={toast}>
{children}
</ToastContext.Provider>
)
return <>{children}</>
}
/**
* 自定义 Hook,用于 React 组件内部
*/
export function useToast() {
const toast = useContext(ToastContext)
if (!toast) {
throw new Error('useToast must be used within a ToastProviderWrapper')
}
return {
show: (options: ShowToastOptions) => {
const id = Math.random().toString()
toast.show({
id,
placement: options.placement ?? 'top',
duration: options.duration !== undefined ? options.duration : 3000,
avoidKeyboard: options.avoidKeyboard ?? false,
containerStyle: options.containerStyle,
onCloseComplete: options.onCloseComplete,
render: options.render
? () => options.render!({ id })
: ({ id }) => (
<Toast
nativeID={`toast-${id}`}
variant={options.variant ?? 'solid'}
action={options.action ?? 'muted'}
>
{options.title && <ToastTitle className="font-blod">{options.title}</ToastTitle>}
{options.description && <ToastDescription>{options.description}</ToastDescription>}
</Toast>
),
})
return id
},
close: (id: string) => {
toast.close(id)
},
closeAll: () => {
toast.closeAll()
},
isActive: (id: string) => toast.isActive(id),
}
}
/**
* 全局 toast 实例,可在组件外部使用
* 全局 toast 对象,组件内外都可以使用
*/
export const toast = {
show: (options: ShowToastOptions) => {
@@ -124,14 +76,12 @@ export const toast = {
return globalToast.show(options)
},
close: (id: string) => {
if (globalToast) {
if (globalToast)
globalToast.close(id)
}
},
closeAll: () => {
if (globalToast) {
if (globalToast)
globalToast.closeAll()
}
},
isActive: (id: string) => {
return globalToast ? globalToast.isActive(id) : false