u
This commit is contained in:
1 parent
500fab7a80
commit
8f3f6982db
54 files changed
+2076
-1877
No files matched your search
@@ -1,47 +1,27 @@
|
||||
import type { TabbarItem } from '@/components/custom/Tabbar'
|
||||
import CustomTabbar from '@/components/custom/Tabbar'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
export const unstable_settings = {
|
||||
initialRouteName: 'money/index',
|
||||
}
|
||||
import { TabSlot } from 'expo-router/ui'
|
||||
import { Text, View } from 'react-native'
|
||||
|
||||
export default function TabsLayout() {
|
||||
const tabbarOptions: TabbarItem[] = [
|
||||
{
|
||||
name: 'money',
|
||||
href: '/money',
|
||||
icon: 'CircleDollarSign',
|
||||
},
|
||||
{
|
||||
name: 'love',
|
||||
href: '/love',
|
||||
icon: 'Heart',
|
||||
},
|
||||
{
|
||||
name: 'app',
|
||||
icon: 'LayoutGrid',
|
||||
activeIcon: 'Smile',
|
||||
iconSize: 26,
|
||||
onPress: () => {
|
||||
toast.show({
|
||||
title: '该功能尚未开发,请尽情期待!',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'chat',
|
||||
href: '/chat',
|
||||
icon: 'MessageCircle',
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
href: '/user',
|
||||
icon: 'CircleUserRound',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<CustomTabbar data={tabbarOptions} />
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1 px-4 pt-4 pb-24">
|
||||
<TabSlot />
|
||||
</View>
|
||||
|
||||
<View
|
||||
className="
|
||||
absolute
|
||||
left-3 right-3 bottom-8
|
||||
h-14
|
||||
flex-row items-center
|
||||
px-2
|
||||
gap-4
|
||||
"
|
||||
>
|
||||
<View className="bg-white rounded-md shadow-lg h-12 w-12"></View>
|
||||
<View className="flex-1 bg-white rounded-md shadow-lg h-12"></View>
|
||||
<View className="bg-white rounded-md shadow-lg h-12 w-12"></View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Whitespace-only changes.
@@ -13,11 +13,11 @@ export default function User() {
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 p-4">
|
||||
<View className="flex flex-row items-center ">
|
||||
<View className="flex flex-row items-center">
|
||||
<Avatar size="md" className="border-0">
|
||||
<AvatarImage
|
||||
className="bg-white"
|
||||
source={require('@/static/images/logo.png')}
|
||||
source={{ uri: userStore.userInfo?.avatar }}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text className="text-xl font-semibold ml-3">{userStore.userInfo?.username}</Text>
|
||||
|
||||
@@ -1,94 +1,263 @@
|
||||
import { useEvent } from 'expo'
|
||||
import ExpoQQ from 'expo-qq'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useEffect } from 'react'
|
||||
import ExpoWeChat from 'expo-wechat'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { AButton, ACheckbox } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { Button, ButtonText } from '@/components/ui/button'
|
||||
import { IconQq, IconWeixin } from '@/components/iconfont'
|
||||
import { loginQqApi, loginWxApi } from '@/request/api/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isAgree, setIsAgree] = useState(true)
|
||||
const [form, setForm] = useState({
|
||||
type: 0,
|
||||
openid: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
export default function AuthIndex() {
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function toPath(url: string) {
|
||||
router.push(url)
|
||||
const onLoginFinished = useEvent(ExpoQQ, 'onLoginFinished')
|
||||
const onGetUserInfo = useEvent(ExpoQQ, 'onGetUserInfo')
|
||||
const authResult = useEvent(ExpoWeChat, 'onAuthResult')
|
||||
|
||||
async function onQQLogin() {
|
||||
setLoading(true)
|
||||
setForm(val => ({
|
||||
...val,
|
||||
type: 1,
|
||||
}))
|
||||
await ExpoQQ.login(['get_user_info'])
|
||||
}
|
||||
|
||||
// /// 监听登录结果,你也可以使用ExpoQQ.addListener('onLoginFinished', callback)来实现,但请不要忘记移除监听
|
||||
// const onSendLoginFinished = useEvent(ExpoQQ, 'onGetUserInfo')
|
||||
// const onLoginFinished = useEvent(ExpoQQ, 'onLoginFinished')
|
||||
async function onWeChatLogin() {
|
||||
setLoading(true)
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(onSendLoginFinished)
|
||||
// }, [onSendLoginFinished])
|
||||
// 检查微信是否安装
|
||||
const isInstalled = await ExpoWeChat.isWXAppInstalled()
|
||||
if (!isInstalled) {
|
||||
toast.show({
|
||||
title: onLoginFinished?.errorMessage || '请先安装微信',
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// /// 国内应用需要在用户接受隐私协议后再初始化
|
||||
// useEffect(() => {
|
||||
// const init = async () => {
|
||||
// await ExpoQQ.init('102826474', 'universal link')
|
||||
// }
|
||||
// init()
|
||||
// }, [])
|
||||
setForm(val => ({
|
||||
...val,
|
||||
type: 0,
|
||||
}))
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!onLoginFinished || !onLoginFinished.success)
|
||||
// return
|
||||
ExpoWeChat.sendAuthRequest('snsapi_userinfo', 'state123')
|
||||
}
|
||||
|
||||
// const fetchUserInfo = async () => {
|
||||
// try {
|
||||
// // 1️⃣ 确认 SDK 已经拿到 token
|
||||
// const tokenInfo = await ExpoQQ.getLoginTokenInfo()
|
||||
// console.log('tokenInfo', tokenInfo)
|
||||
useEffect(() => {
|
||||
ExpoQQ.init('102826474', '')
|
||||
ExpoWeChat.registerApp('wxdab3e21a1f7e392f', '')
|
||||
}, [])
|
||||
|
||||
// // 2️⃣ 再请求用户信息
|
||||
// await ExpoQQ.sendGetUserInfoReq()
|
||||
// }
|
||||
// catch (e) {
|
||||
// console.error('QQ 登录未完成', e)
|
||||
// }
|
||||
// }
|
||||
useEffect(() => {
|
||||
if (!onLoginFinished) {
|
||||
return
|
||||
}
|
||||
|
||||
// fetchUserInfo()
|
||||
// }, [onLoginFinished])
|
||||
if (!onLoginFinished.success) {
|
||||
toast.show({
|
||||
title: onLoginFinished?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// /// 调用qq登陆
|
||||
// const loginByQQ = async () => {
|
||||
// await ExpoQQ.login(['get_user_info'])
|
||||
// }
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const tokenInfo = await ExpoQQ.getLoginTokenInfo()
|
||||
if (!tokenInfo.openId) {
|
||||
toast.show({
|
||||
title: tokenInfo?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setForm(val => ({
|
||||
...val,
|
||||
openid: String(tokenInfo.openId),
|
||||
}))
|
||||
|
||||
await ExpoQQ.sendGetUserInfoReq()
|
||||
}
|
||||
catch (e) {
|
||||
console.error('QQ 登录未完成', e)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchUserInfo()
|
||||
}, [onLoginFinished])
|
||||
|
||||
// QQ登录
|
||||
useEffect(() => {
|
||||
const handleLogin = async () => {
|
||||
if (!onGetUserInfo)
|
||||
return
|
||||
|
||||
try {
|
||||
const loginRes = await loginQqApi({
|
||||
openid: form.openid,
|
||||
username: onGetUserInfo.nickname || '',
|
||||
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
||||
})
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
username: loginRes.data.username,
|
||||
avatar: loginRes.data.avatar,
|
||||
})
|
||||
|
||||
console.log(loginRes.data.token)
|
||||
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('登录接口出错', err)
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin()
|
||||
}, [onGetUserInfo])
|
||||
|
||||
// 微信登录
|
||||
useEffect(() => {
|
||||
if (!authResult)
|
||||
return
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (authResult.errorCode === 0) {
|
||||
try {
|
||||
const loginRes = await loginWxApi({
|
||||
code: authResult.code,
|
||||
})
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
username: loginRes.data.username,
|
||||
avatar: loginRes.data.avatar,
|
||||
})
|
||||
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('登录接口出错', err)
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('授权失败:', authResult.errorMessage)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin()
|
||||
}, [authResult])
|
||||
|
||||
function testLogin() {
|
||||
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3NjY3ODU5ODIxNjUwNjEsImlhdCI6MTc2OTM0ODI0Nn0.17ZNaHD3HSLM8dVn_JQK-gvsG9MKj47IRYV9D4xZkzs
|
||||
|
||||
userStore.setUserInfo({
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3NjY3ODU5ODIxNjUwNjEsImlhdCI6MTc2OTM0ODI0Nn0.17ZNaHD3HSLM8dVn_JQK-gvsG9MKj47IRYV9D4xZkzs',
|
||||
id: 766785982165061,
|
||||
username: 'Zhang',
|
||||
avatar: 'http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100',
|
||||
})
|
||||
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<View className="flex-1 bg-white px-6 justify-center">
|
||||
<CustomHeader
|
||||
showHeader={false}
|
||||
/>
|
||||
<View className="flex-1 bg-white items-center justify-center">
|
||||
<Image
|
||||
source={require('@/static/images/work.png')}
|
||||
style={{ width: '100%', height: 280 }}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
<View className="items-center mb-12">
|
||||
<Image
|
||||
source={require('@/static/images/logo.png')}
|
||||
style={{ width: 60, height: 60 }}
|
||||
className="mb-6"
|
||||
className="w-20 h-20 mb-4"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
<Text className="font-bold text-3xl text-[#333] tracking-wide">
|
||||
ONEAO
|
||||
<Text className="text-xl font-semibold text-gray-900">
|
||||
Welcome to OneAO
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full flex flex-col gap-3 mt-12 px-3">
|
||||
<Button action="secondary" onPress={() => toPath('/auth/login')}>
|
||||
<ButtonText>登录</ButtonText>
|
||||
</Button>
|
||||
<View className="flex flex-col gap-4">
|
||||
<AButton
|
||||
leftIcon={
|
||||
<IconWeixin color="#fff" />
|
||||
}
|
||||
loading={loading}
|
||||
onPress={onWeChatLogin}
|
||||
>
|
||||
WeChat
|
||||
</AButton>
|
||||
|
||||
<Button action="secondary" onPress={() => toPath('/auth/register')}>
|
||||
<ButtonText>注册</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
<AButton
|
||||
variant="outline"
|
||||
leftIcon={
|
||||
<IconQq />
|
||||
}
|
||||
loading={loading}
|
||||
onPress={onQQLogin}
|
||||
>
|
||||
QQ
|
||||
</AButton>
|
||||
|
||||
<AButton
|
||||
variant="outline"
|
||||
leftIcon={
|
||||
<IconQq />
|
||||
}
|
||||
loading={loading}
|
||||
onPress={testLogin}
|
||||
>
|
||||
模拟登录
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
<View className="mt-10 items-center flex flex-row justify-center">
|
||||
<ACheckbox
|
||||
checked={isAgree}
|
||||
size="sm"
|
||||
label={(
|
||||
<Text className="text-sm text-gray-400 text-center leading-4">
|
||||
登录即表示你同意我们的
|
||||
<Text className="text-gray-600"> 用户协议 </Text>
|
||||
和
|
||||
<Text className="text-gray-600"> 隐私政策</Text>
|
||||
</Text>
|
||||
)}
|
||||
onChange={(val) => {
|
||||
setIsAgree(val)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -157,7 +157,6 @@ export default function CreateCategory({
|
||||
data={flatListData}
|
||||
keyExtractor={([groupName]) => groupName}
|
||||
renderItem={renderGroup}
|
||||
initialNumToRender={1}
|
||||
/>
|
||||
</ADrawer>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import type { icons } from 'lucide-react-native'
|
||||
import type { ViewStyle } from 'react-native'
|
||||
import type { LucideIconProps } from './LucideIcon'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import {
|
||||
ButtonSpinner,
|
||||
ButtonText,
|
||||
Button as NativeButton,
|
||||
} from '@/components/ui/button'
|
||||
import LucideIcon from './LucideIcon'
|
||||
|
||||
type IconProp = (LucideIconProps & { icon: keyof typeof icons }) | React.ReactNode
|
||||
type IconProp = React.ReactNode | (() => React.ReactNode)
|
||||
|
||||
interface CustomButtonProps {
|
||||
children?: React.ReactNode
|
||||
@@ -21,7 +19,6 @@ interface CustomButtonProps {
|
||||
action?: 'primary' | 'secondary' | 'positive' | 'negative' | 'default'
|
||||
variant?: 'solid' | 'outline' | 'link'
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
leftIcon?: IconProp
|
||||
rightIcon?: IconProp
|
||||
}
|
||||
@@ -41,31 +38,39 @@ const Button: React.FC<CustomButtonProps> = ({
|
||||
}) => {
|
||||
const isDisabled = disabled || loading
|
||||
const hasChildren = !!children
|
||||
const renderLeftIcon = () => {
|
||||
if (loading)
|
||||
return <ButtonSpinner color="#fff" className={hasChildren ? 'mr-2' : ''} />
|
||||
|
||||
if (!leftIcon)
|
||||
const renderIcon = (
|
||||
icon?: IconProp,
|
||||
position: 'left' | 'right' = 'left',
|
||||
) => {
|
||||
if (!icon)
|
||||
return null
|
||||
|
||||
if (React.isValidElement(leftIcon)) {
|
||||
return <span className={hasChildren ? 'mr-2' : ''}>{leftIcon}</span>
|
||||
if (loading && position === 'left') {
|
||||
const spinnerColor = variant === 'outline'
|
||||
? 'rgba(0,0,0,0.6)'
|
||||
: 'rgba(255,255,255,0.9)'
|
||||
return (
|
||||
<View className={hasChildren ? 'mr-2' : undefined}>
|
||||
<ButtonSpinner color={spinnerColor} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const { icon, ...props } = leftIcon as LucideIconProps & { icon: keyof typeof icons }
|
||||
return <LucideIcon icon={icon} className={hasChildren ? 'mr-2' : ''} {...props} />
|
||||
}
|
||||
const node = typeof icon === 'function'
|
||||
? icon()
|
||||
: icon
|
||||
|
||||
const renderRightIcon = () => {
|
||||
if (!rightIcon)
|
||||
return null
|
||||
|
||||
if (React.isValidElement(rightIcon)) {
|
||||
return <span className={hasChildren ? 'ml-2' : ''}>{rightIcon}</span>
|
||||
}
|
||||
|
||||
const { icon, ...props } = rightIcon as LucideIconProps & { icon: keyof typeof icons }
|
||||
return <LucideIcon icon={icon} className={hasChildren ? 'ml-2' : ''} {...props} />
|
||||
return (
|
||||
<View className={hasChildren
|
||||
? position === 'left'
|
||||
? 'mr-2'
|
||||
: 'ml-2'
|
||||
: undefined}
|
||||
>
|
||||
{node}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -78,13 +83,9 @@ const Button: React.FC<CustomButtonProps> = ({
|
||||
variant={variant}
|
||||
size={size}
|
||||
>
|
||||
{renderLeftIcon()}
|
||||
{
|
||||
children && (
|
||||
<ButtonText>{children}</ButtonText>
|
||||
)
|
||||
}
|
||||
{renderRightIcon()}
|
||||
{renderIcon(leftIcon, 'left')}
|
||||
{children && <ButtonText>{children}</ButtonText>}
|
||||
{renderIcon(rightIcon, 'right')}
|
||||
</NativeButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,33 +11,36 @@ import { CheckIcon } from '@/components/ui/icon'
|
||||
|
||||
interface CustomCheckboxProps {
|
||||
value?: string
|
||||
isChecked: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
label: React.ReactNode
|
||||
checked?: boolean
|
||||
onChange?: (checked: boolean) => void
|
||||
label?: React.ReactNode
|
||||
className?: string
|
||||
style?: ViewStyle
|
||||
disabled?: boolean
|
||||
icon?: React.ElementType
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CustomCheckboxProps> = ({
|
||||
value,
|
||||
isChecked,
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
className,
|
||||
style,
|
||||
disabled,
|
||||
icon,
|
||||
size,
|
||||
}) => {
|
||||
return (
|
||||
<NativeCheckbox
|
||||
value={value ?? ''} // 避免 undefined
|
||||
isChecked={isChecked}
|
||||
value={value ?? ''}
|
||||
isChecked={checked}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
style={style}
|
||||
isDisabled={disabled}
|
||||
size={size}
|
||||
>
|
||||
<CheckboxIndicator>
|
||||
<CheckboxIcon as={icon ?? CheckIcon} />
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { createAvatar } from '@gluestack-ui/core/avatar/creator';
|
||||
'use client'
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
|
||||
import { createAvatar } from '@gluestack-ui/core/avatar/creator'
|
||||
|
||||
import { View, Text, Image, Platform } from 'react-native';
|
||||
import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'
|
||||
|
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import {
|
||||
withStyleContext,
|
||||
useStyleContext,
|
||||
} from '@gluestack-ui/utils/nativewind-utils';
|
||||
const SCOPE = 'AVATAR';
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import React from 'react'
|
||||
import { Image, Platform, Text, View } from 'react-native'
|
||||
|
||||
const SCOPE = 'AVATAR'
|
||||
|
||||
const UIAvatar = createAvatar({
|
||||
Root: withStyleContext(View, SCOPE),
|
||||
Badge: View,
|
||||
Group: View,
|
||||
Image: Image,
|
||||
Image,
|
||||
FallbackText: Text,
|
||||
});
|
||||
})
|
||||
|
||||
const avatarStyle = tva({
|
||||
base: 'rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5',
|
||||
@@ -32,7 +29,7 @@ const avatarStyle = tva({
|
||||
'2xl': 'w-32 h-32',
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const avatarFallbackTextStyle = tva({
|
||||
base: 'text-typography-0 font-semibold overflow-hidden text-transform:uppercase web:cursor-default',
|
||||
@@ -47,11 +44,11 @@ const avatarFallbackTextStyle = tva({
|
||||
'2xl': 'text-5xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const avatarGroupStyle = tva({
|
||||
base: 'group/avatar-group flex-row-reverse relative avatar-group',
|
||||
});
|
||||
})
|
||||
|
||||
const avatarBadgeStyle = tva({
|
||||
base: 'w-5 h-5 bg-success-500 rounded-full absolute right-0 bottom-0 border-background-0 border-2',
|
||||
@@ -65,22 +62,22 @@ const avatarBadgeStyle = tva({
|
||||
'2xl': 'w-8 h-8',
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const avatarImageStyle = tva({
|
||||
base: 'h-full w-full rounded-full absolute',
|
||||
});
|
||||
})
|
||||
|
||||
type IAvatarProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof UIAvatar>,
|
||||
'context'
|
||||
> &
|
||||
VariantProps<typeof avatarStyle>;
|
||||
>
|
||||
& VariantProps<typeof avatarStyle>
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ComponentRef<typeof UIAvatar>,
|
||||
IAvatarProps
|
||||
>(function Avatar({ className, size = 'md', ...props }, ref) {
|
||||
>(({ className, size = 'md', ...props }, ref) => {
|
||||
return (
|
||||
<UIAvatar
|
||||
ref={ref}
|
||||
@@ -88,17 +85,17 @@ const Avatar = React.forwardRef<
|
||||
className={avatarStyle({ size, class: className })}
|
||||
context={{ size }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
type IAvatarBadgeProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Badge> &
|
||||
VariantProps<typeof avatarBadgeStyle>;
|
||||
type IAvatarBadgeProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Badge>
|
||||
& VariantProps<typeof avatarBadgeStyle>
|
||||
|
||||
const AvatarBadge = React.forwardRef<
|
||||
React.ComponentRef<typeof UIAvatar.Badge>,
|
||||
IAvatarBadgeProps
|
||||
>(function AvatarBadge({ className, size, ...props }, ref) {
|
||||
const { size: parentSize } = useStyleContext(SCOPE);
|
||||
>(({ className, size, ...props }, ref) => {
|
||||
const { size: parentSize } = useStyleContext(SCOPE)
|
||||
|
||||
return (
|
||||
<UIAvatar.Badge
|
||||
@@ -112,18 +109,18 @@ const AvatarBadge = React.forwardRef<
|
||||
class: className,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef<
|
||||
typeof UIAvatar.FallbackText
|
||||
> &
|
||||
VariantProps<typeof avatarFallbackTextStyle>;
|
||||
>
|
||||
& VariantProps<typeof avatarFallbackTextStyle>
|
||||
const AvatarFallbackText = React.forwardRef<
|
||||
React.ComponentRef<typeof UIAvatar.FallbackText>,
|
||||
IAvatarFallbackTextProps
|
||||
>(function AvatarFallbackText({ className, size, ...props }, ref) {
|
||||
const { size: parentSize } = useStyleContext(SCOPE);
|
||||
>(({ className, size, ...props }, ref) => {
|
||||
const { size: parentSize } = useStyleContext(SCOPE)
|
||||
|
||||
return (
|
||||
<UIAvatar.FallbackText
|
||||
@@ -137,16 +134,16 @@ const AvatarFallbackText = React.forwardRef<
|
||||
class: className,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
type IAvatarImageProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Image> &
|
||||
VariantProps<typeof avatarImageStyle>;
|
||||
type IAvatarImageProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Image>
|
||||
& VariantProps<typeof avatarImageStyle>
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ComponentRef<typeof UIAvatar.Image>,
|
||||
IAvatarImageProps
|
||||
>(function AvatarImage({ className, ...props }, ref) {
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<UIAvatar.Image
|
||||
ref={ref}
|
||||
@@ -161,16 +158,16 @@ const AvatarImage = React.forwardRef<
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
type IAvatarGroupProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Group> &
|
||||
VariantProps<typeof avatarGroupStyle>;
|
||||
type IAvatarGroupProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Group>
|
||||
& VariantProps<typeof avatarGroupStyle>
|
||||
|
||||
const AvatarGroup = React.forwardRef<
|
||||
React.ComponentRef<typeof UIAvatar.Group>,
|
||||
IAvatarGroupProps
|
||||
>(function AvatarGroup({ className, ...props }, ref) {
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<UIAvatar.Group
|
||||
ref={ref}
|
||||
@@ -179,7 +176,7 @@ const AvatarGroup = React.forwardRef<
|
||||
class: className,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup };
|
||||
export { Avatar, AvatarBadge, AvatarFallbackText, AvatarGroup, AvatarImage }
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { LoginDefaultParams, LoginDefaultResult, RegisterDefaultParams } from '@/types/auth'
|
||||
import type { LoginQqParams, LoginResult, LoginWxParams } from '@/types/auth'
|
||||
import alovaInstance from '../index'
|
||||
|
||||
const API_PREFIX = '/auth'
|
||||
|
||||
export function registerDefaultApi(params: RegisterDefaultParams) {
|
||||
return alovaInstance.Post<ResponseData<any>>(`${API_PREFIX}/register/default`, params)
|
||||
export function loginQqApi(params: LoginQqParams) {
|
||||
return alovaInstance.Post<ResponseData<LoginResult>>(`${API_PREFIX}/login/qq`, params)
|
||||
}
|
||||
|
||||
export function loginDefaultApi(params: LoginDefaultParams) {
|
||||
return alovaInstance.Post<ResponseData<LoginDefaultResult>>(`${API_PREFIX}/login/default`, params)
|
||||
export function loginWxApi(params: LoginWxParams) {
|
||||
return alovaInstance.Post<ResponseData<LoginResult>>(`${API_PREFIX}/login/wx`, params)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Method } from 'alova'
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { RefreshTokenResult } from '@/types/auth'
|
||||
import { axiosRequestAdapter } from '@alova/adapter-axios'
|
||||
import { createAlova } from 'alova'
|
||||
import { useNavStore } from '@/store/navigation'
|
||||
@@ -11,14 +10,13 @@ import { toast } from '@/utils/toast'
|
||||
enum HttpCode {
|
||||
Success = 200,
|
||||
Unauthorized = 401,
|
||||
RefreshToken = 402,
|
||||
Fail = 500,
|
||||
}
|
||||
|
||||
// 仅保留「无需登录」接口
|
||||
const WhiteList = [
|
||||
'/auth/login/**',
|
||||
'/auth/register/**',
|
||||
'/auth/refresh/token',
|
||||
]
|
||||
|
||||
function isWhiteListed(url: string) {
|
||||
@@ -32,13 +30,14 @@ function isWhiteListed(url: string) {
|
||||
}
|
||||
|
||||
const alovaInstance = createAlova({
|
||||
// baseURL: 'http://192.168.5.6:8086/app',
|
||||
baseURL: 'http://192.168.1.137:8086/app',
|
||||
baseURL: 'http://192.168.5.6:8086/app',
|
||||
// baseURL: 'http://192.168.1.137:8086/app',
|
||||
requestAdapter: axiosRequestAdapter(),
|
||||
timeout: 5000,
|
||||
cacheFor: null,
|
||||
|
||||
responded: {
|
||||
onSuccess: async (response, method: Method) => {
|
||||
onSuccess: async (response, _method: Method) => {
|
||||
const resData = response.data as ResponseData<any>
|
||||
|
||||
const result = {
|
||||
@@ -46,116 +45,78 @@ const alovaInstance = createAlova({
|
||||
isSuccess: resData.code === HttpCode.Success,
|
||||
}
|
||||
|
||||
// Access Token 失效 → 重新登录
|
||||
/**
|
||||
* 登录态失效:
|
||||
* - 清空本地状态
|
||||
* - 跳转登录页
|
||||
* - 不做 token 刷新、不重试请求
|
||||
*/
|
||||
if (resData.code === HttpCode.Unauthorized) {
|
||||
useUserStore.getState().clear()
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return
|
||||
}
|
||||
|
||||
// Access Token 过期但 Refresh Token 有效 → 刷新 token 并重试
|
||||
if (resData.code === HttpCode.RefreshToken) {
|
||||
try {
|
||||
const refreshToken = useUserStore.getState().userInfo?.refreshToken
|
||||
if (!refreshToken) {
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用刷新接口
|
||||
const res = await alovaInstance.Get<ResponseData<RefreshTokenResult>>('/auth/refresh/token', {
|
||||
headers: { refreshToken },
|
||||
}).send()
|
||||
|
||||
const newToken = res.data.refreshToken
|
||||
|
||||
// 更新 store
|
||||
useUserStore.getState().setUserInfo({
|
||||
...useUserStore.getState().userInfo!,
|
||||
token: newToken,
|
||||
})
|
||||
|
||||
// 重试原请求
|
||||
method.config.headers.token = newToken
|
||||
const retryResult = await method.send()
|
||||
|
||||
return retryResult
|
||||
}
|
||||
catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
return Promise.reject(new Error('登录已失效'))
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
onError: (error: AxiosError, _method) => {
|
||||
console.log(error.status)
|
||||
|
||||
switch (error.status) {
|
||||
onError: (error: AxiosError) => {
|
||||
const status = error.status
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
toast.show({
|
||||
title: '请求错误,请检查输入参数',
|
||||
})
|
||||
toast.show({ title: '请求错误,请检查输入参数' })
|
||||
break
|
||||
case 401:
|
||||
toast.show({
|
||||
title: '未授权,请重新登录',
|
||||
})
|
||||
toast.show({ title: '未授权,请重新登录' })
|
||||
break
|
||||
case 403:
|
||||
toast.show({
|
||||
title: '没有权限访问该资源',
|
||||
})
|
||||
toast.show({ title: '没有权限访问该资源' })
|
||||
break
|
||||
case 404:
|
||||
toast.show({
|
||||
title: '请求丢失啦,请稍后再试!',
|
||||
})
|
||||
toast.show({ title: '请求丢失啦,请稍后再试!' })
|
||||
break
|
||||
case 500:
|
||||
toast.show({
|
||||
title: '服务器出错啦,请稍后重试',
|
||||
})
|
||||
toast.show({ title: '服务器出错啦,请稍后重试' })
|
||||
break
|
||||
case 502:
|
||||
toast.show({
|
||||
title: '网关错误,请稍后重试',
|
||||
})
|
||||
toast.show({ title: '网关错误,请稍后重试' })
|
||||
break
|
||||
case 503:
|
||||
toast.show({
|
||||
title: '服务暂不可用,请稍后再试',
|
||||
})
|
||||
toast.show({ title: '服务暂不可用,请稍后再试' })
|
||||
break
|
||||
case 504:
|
||||
toast.show({
|
||||
title: '网关超时,请稍后再试',
|
||||
})
|
||||
toast.show({ title: '网关超时,请稍后再试' })
|
||||
break
|
||||
default:
|
||||
toast.show({
|
||||
title: `服务器出错啦,请稍后重试`,
|
||||
})
|
||||
toast.show({ title: '服务器出错啦,请稍后重试' })
|
||||
break
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
},
|
||||
|
||||
beforeRequest(method) {
|
||||
// 白名单接口直接放行
|
||||
if (isWhiteListed(method.url))
|
||||
return
|
||||
|
||||
if (!useUserStore.getState().isLoggedIn()) {
|
||||
const userStore = useUserStore.getState()
|
||||
|
||||
// 未登录直接拦截
|
||||
if (!userStore.isLoggedIn()) {
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return Promise.reject(new Error('未登录'))
|
||||
}
|
||||
|
||||
method.config.headers.token = useUserStore.getState().userInfo?.token ?? ''
|
||||
// 注入 JWT
|
||||
method.config.headers.token = userStore.userInfo!.token
|
||||
|
||||
// 注入 space 信息(如果存在)
|
||||
const userSpace = useSpaceStore.getState().userSpace
|
||||
if (userSpace !== null) {
|
||||
const spaceID = userSpace.space_id
|
||||
|
||||
method.config.headers['x-space-id'] = spaceID
|
||||
if (userSpace) {
|
||||
method.config.headers['x-space-id'] = userSpace.space_id
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ const useUserStore = create<UserState>()(
|
||||
set({ userInfo: null, userSpace: null })
|
||||
},
|
||||
|
||||
isLoggedIn: () => !!get().userInfo?.token,
|
||||
isLoggedIn: () => Boolean(get().userInfo?.token),
|
||||
}),
|
||||
{
|
||||
name: 'user-storage',
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
export interface RefreshTokenResult {
|
||||
refreshToken: string
|
||||
export interface LoginQqParams {
|
||||
openid: string
|
||||
username: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
export interface RegisterDefaultParams {
|
||||
account: string
|
||||
password: string
|
||||
export interface LoginWxParams {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface LoginDefaultParams {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginDefaultResult {
|
||||
export interface LoginResult {
|
||||
token: string
|
||||
refreshToken: string
|
||||
userId: number
|
||||
username: string
|
||||
seqNo: number
|
||||
gender: number
|
||||
avatar: string
|
||||
}
|
||||
+3
-7
@@ -10,14 +10,10 @@ declare global {
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
account: string
|
||||
id: number
|
||||
seqNo: number
|
||||
username: string
|
||||
gender: number
|
||||
token?: string
|
||||
refreshToken?: string
|
||||
rememberMe?: boolean
|
||||
token: string
|
||||
username?: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
interface UserSpace {
|
||||
|
||||
Reference in new issue
Block a user