u
This commit is contained in:
1 parent
6849579652
commit
d60be4a75b
24 files changed
+463
-179
No files matched your search
@@ -12,5 +12,6 @@ export default antfu({
|
||||
'no-console': 'off', // 关闭 console 报错
|
||||
'node/prefer-global/process': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'no-useless-catch': 'off',
|
||||
},
|
||||
})
|
||||
@@ -5,7 +5,7 @@ import { Stack, useRouter } from 'expo-router'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { RefreshCw } from 'lucide-react-native'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
|
||||
import { useUserStore } from '@/store/user'
|
||||
@@ -33,33 +33,23 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
|
||||
|
||||
export default function RootLayout() {
|
||||
const router = useRouter()
|
||||
|
||||
const token = useUserStore(state => state.token)
|
||||
const userStore = useUserStore()
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
|
||||
})
|
||||
|
||||
const [storeLoaded, setStoreLoaded] = useState(false)
|
||||
|
||||
// 等待 store 恢复数据
|
||||
useEffect(() => {
|
||||
if (token !== undefined) {
|
||||
setStoreLoaded(true)
|
||||
}
|
||||
}, [token])
|
||||
|
||||
// splash screen 隐藏 & 登录跳转
|
||||
useEffect(() => {
|
||||
if (fontsLoaded && storeLoaded) {
|
||||
if (fontsLoaded) {
|
||||
SplashScreen.hideAsync()
|
||||
|
||||
// 如果没有 token,则跳转登录页
|
||||
if (token === null) {
|
||||
if (!userStore.isLoggedIn()) {
|
||||
router.replace('/auth')
|
||||
}
|
||||
}
|
||||
}, [fontsLoaded, storeLoaded, token])
|
||||
}, [fontsLoaded])
|
||||
|
||||
return (
|
||||
<GluestackUIProvider>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { StepLayoutItem } from '@/components/custom/StepLayout'
|
||||
import { Slot } from 'expo-router'
|
||||
import { Slot, Stack, useRouter } from 'expo-router'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import StepLayout from '@/components/custom/StepLayout'
|
||||
import CustomTabbar from '@/components/custom/Tabbar'
|
||||
|
||||
const steps: StepLayoutItem[] = [
|
||||
{
|
||||
@@ -22,5 +25,6 @@ export default function AuthLayout() {
|
||||
<StepLayout steps={steps}>
|
||||
<Slot />
|
||||
</StepLayout>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -36,17 +36,20 @@ export default function AuthLogin() {
|
||||
setForm(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
const defaultLogin = () => {
|
||||
alovaInstance.Post('/auth/login/default', {
|
||||
alovaInstance.Post('/auth/test', {
|
||||
title: 'foo',
|
||||
body: 'bar',
|
||||
userId: 1,
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
.then((res) => {
|
||||
console.log('成功返回:', res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('请求失败或未授权:', err)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<View className="flex flex-col items-center px-6 h-full">
|
||||
<Image
|
||||
source={require('@/static/images/logo.png')}
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function AuthRegister() {
|
||||
|
||||
return (
|
||||
<ScrollView ref={scrollRef} style={{ flex: 1 }}>
|
||||
<View className="flex flex-col items-center px-6">
|
||||
<View className="flex flex-col items-center px-6 bg-white ">
|
||||
<Image
|
||||
source={require('@/static/images/logo.png')}
|
||||
style={{ width: 120, height: 120 }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { usePathname, useRouter } from 'expo-router'
|
||||
import { Image, Pressable, ScrollView, Text, View } from 'react-native'
|
||||
import Animated, { SlideInRight } from 'react-native-reanimated'
|
||||
import { Image, Pressable, Text, View } from 'react-native'
|
||||
import Animated, { SlideInLeft, SlideInRight } from 'react-native-reanimated'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
|
||||
export interface StepLayoutItem {
|
||||
@@ -31,11 +31,13 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
|
||||
// 是否显示header
|
||||
const headerVisible = showHearder !== false && currentStep.showHearder !== false
|
||||
|
||||
const enteringAnimation = canGoBack ? SlideInRight.duration(300) : SlideInLeft.duration(300)
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<Animated.View
|
||||
key={pathname}
|
||||
entering={SlideInRight.duration(300)}
|
||||
entering={enteringAnimation}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{headerVisible && (
|
||||
@@ -44,10 +46,7 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
|
||||
onPress={canGoBack ? (currentStep.onBack ?? (() => router.back())) : undefined}
|
||||
style={{ opacity: canGoBack ? 1 : 0 }}
|
||||
>
|
||||
<Ionicons
|
||||
name="arrow-back"
|
||||
size={22}
|
||||
/>
|
||||
<Ionicons name="arrow-back" size={22} />
|
||||
</Pressable>
|
||||
|
||||
<Text className="flex-1 text-center text-lg font-semibold">
|
||||
|
||||
Whitespace-only changes.
@@ -1,21 +1,110 @@
|
||||
import type { Method } from 'alova'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { axiosRequestAdapter } from '@alova/adapter-axios'
|
||||
import { createAlova } from 'alova'
|
||||
import { createServerTokenAuthentication } from 'alova/client'
|
||||
import adapterFetch from 'alova/fetch'
|
||||
import { useToast } from '@/utils/toast'
|
||||
import { router } from 'expo-router'
|
||||
import { userStore } from '@/store/user'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
|
||||
// ...
|
||||
})
|
||||
enum HttpCode {
|
||||
Success = 200,
|
||||
Unauthorized = 401,
|
||||
RefreshToken = 402,
|
||||
Fail = 500,
|
||||
}
|
||||
|
||||
const WhiteList = [
|
||||
'/auth/login/**',
|
||||
'/auth/register/**',
|
||||
'/auth/refresh/token',
|
||||
]
|
||||
|
||||
function isWhiteListed(url: string) {
|
||||
return WhiteList.some((pattern) => {
|
||||
if (pattern.endsWith('/**')) {
|
||||
const base = pattern.slice(0, -3)
|
||||
return url.startsWith(base)
|
||||
}
|
||||
return url === pattern
|
||||
})
|
||||
}
|
||||
|
||||
const alovaInstance = createAlova({
|
||||
// baseURL: 'http://192.168.1.136:8085/app',
|
||||
baseURL: 'http://192.168.5.6:8085/app',
|
||||
baseURL: 'http://192.168.1.136:8085/app',
|
||||
requestAdapter: axiosRequestAdapter(),
|
||||
responded(response) {
|
||||
// response自动被推断为AxiosResponse类型
|
||||
|
||||
return response.data
|
||||
responded: {
|
||||
onSuccess: async (response, method: Method) => {
|
||||
const resData = response.data as ResponseData<any>
|
||||
|
||||
if (resData.code === HttpCode.Success) {
|
||||
return resData.data
|
||||
}
|
||||
|
||||
// Access Token 失效 → 重新登录
|
||||
if (resData.code === HttpCode.Unauthorized) {
|
||||
console.log('===到这一步了===')
|
||||
router.replace('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
// Access Token 过期但 Refresh Token 有效 → 刷新 token 并重试
|
||||
if (resData.code === HttpCode.RefreshToken) {
|
||||
try {
|
||||
const refreshToken = userStore.getState().userInfo?.refreshToken
|
||||
if (!refreshToken) {
|
||||
router.replace('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用刷新接口
|
||||
const res = await alovaInstance.Get<ResponseData<string>>('/auth/refresh/token', {
|
||||
headers: {
|
||||
refreshToken,
|
||||
},
|
||||
})
|
||||
const newToken = res.data
|
||||
|
||||
// 更新 store
|
||||
userStore.getState().setUserInfo({
|
||||
...userStore.getState().userInfo!,
|
||||
token: newToken,
|
||||
})
|
||||
|
||||
// 重试原请求
|
||||
method.config.headers.token = newToken
|
||||
await method.send()
|
||||
|
||||
return
|
||||
}
|
||||
catch (err) {
|
||||
console.log('===到这一步了===')
|
||||
router.replace('/auth/login')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 其他错误
|
||||
return Promise.reject(resData.message || '请求失败')
|
||||
},
|
||||
|
||||
onError: (error: AxiosError, method) => {
|
||||
console.log(error.status)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
},
|
||||
beforeRequest(method) {
|
||||
if (isWhiteListed(method.url))
|
||||
return
|
||||
|
||||
if (!userStore.getState().isLoggedIn()) {
|
||||
toast.show({
|
||||
title: '尚未登录',
|
||||
})
|
||||
router.replace('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
method.config.headers.token = userStore.getState().userInfo?.token ?? ''
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -4,26 +4,22 @@ import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
|
||||
interface UserState {
|
||||
userInfo: { userId: string, username: string } | null
|
||||
token: string | null
|
||||
setUserInfo: (userInfo: UserState['userInfo']) => void
|
||||
setToken: (token: string) => void
|
||||
userInfo: UserInfo | null
|
||||
setUserInfo: (userInfo: UserInfo | null) => void
|
||||
logout: () => void
|
||||
isLoggedIn: () => boolean
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>()(
|
||||
const useUserStore = create<UserState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
userInfo: null,
|
||||
token: null,
|
||||
setUserInfo: userInfo => set({ userInfo }),
|
||||
setToken: token => set({ token }),
|
||||
logout: () => {
|
||||
console.log('当前 token:', get().token)
|
||||
set({ userInfo: null, token: null })
|
||||
console.log('当前 token:', get().userInfo?.token)
|
||||
set({ userInfo: null })
|
||||
},
|
||||
isLoggedIn: () => !!get().token,
|
||||
isLoggedIn: () => !!get().userInfo?.token,
|
||||
}),
|
||||
{
|
||||
name: 'user-storage',
|
||||
@@ -31,3 +27,6 @@ export const useUserStore = create<UserState>()(
|
||||
},
|
||||
),
|
||||
)
|
||||
const userStore = useUserStore
|
||||
|
||||
export { userStore, useUserStore }
|
||||
@@ -0,0 +1,20 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface ResponseData<T> {
|
||||
trackId: string
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
account: string
|
||||
id: string
|
||||
seqNo: number
|
||||
username: string
|
||||
gender: number
|
||||
token?: string
|
||||
refreshToken?: string
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IToastProps } from '@gluestack-ui/core/lib/esm/toast/creator/types'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { createContext, useContext, useRef } from 'react'
|
||||
import { Toast, ToastDescription, ToastTitle, useToast as useGluestackToast } from '@/components/ui/toast'
|
||||
|
||||
type ToastVariant = 'solid' | 'outline'
|
||||
@@ -12,28 +12,74 @@ interface ShowToastOptions extends IToastProps {
|
||||
action?: ToastAction
|
||||
}
|
||||
|
||||
// 创建全局 toast 实例
|
||||
let globalToast: any = null
|
||||
|
||||
// 创建 Context 用于在组件内部使用
|
||||
const ToastContext = createContext<ReturnType<typeof useGluestackToast> | null>(null)
|
||||
|
||||
export function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
|
||||
const toast = useGluestackToast()
|
||||
const toastRef = useRef(toast)
|
||||
toastRef.current = toast
|
||||
|
||||
return <>{children}</>
|
||||
// 设置全局实例
|
||||
React.useEffect(() => {
|
||||
globalToast = {
|
||||
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])
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={toast}>
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 Hook,用法:
|
||||
* const toast = useToast();
|
||||
* toast.show({ ... });
|
||||
* toast.close(id);
|
||||
* toast.closeAll();
|
||||
* 自定义 Hook,用于 React 组件内部
|
||||
*/
|
||||
export function useToast() {
|
||||
const toast = useGluestackToast()
|
||||
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',
|
||||
@@ -54,18 +100,40 @@ export function useToast() {
|
||||
</Toast>
|
||||
),
|
||||
})
|
||||
|
||||
return id
|
||||
},
|
||||
|
||||
close: (id: string) => {
|
||||
toast.close(id)
|
||||
},
|
||||
|
||||
closeAll: () => {
|
||||
toast.closeAll()
|
||||
},
|
||||
|
||||
isActive: (id: string) => toast.isActive(id),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 toast 实例,可在组件外部使用
|
||||
*/
|
||||
export const toast = {
|
||||
show: (options: ShowToastOptions) => {
|
||||
if (!globalToast) {
|
||||
console.warn('Toast not initialized. Make sure ToastProviderWrapper is rendered.')
|
||||
return ''
|
||||
}
|
||||
return globalToast.show(options)
|
||||
},
|
||||
close: (id: string) => {
|
||||
if (globalToast) {
|
||||
globalToast.close(id)
|
||||
}
|
||||
},
|
||||
closeAll: () => {
|
||||
if (globalToast) {
|
||||
globalToast.closeAll()
|
||||
}
|
||||
},
|
||||
isActive: (id: string) => {
|
||||
return globalToast ? globalToast.isActive(id) : false
|
||||
},
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx",
|
||||
"./src/types/*.d.ts",
|
||||
"src/**/*.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
Reference in new issue
Block a user