This commit is contained in:
oneao committed 2025-11-02 22:08:05 +08:00
1 parent 1d6bb2b7f5
commit 458f0cd6ac
55 files changed
+1052 -999

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};
-3
View File
@@ -1,3 +0,0 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
@@ -1,22 +0,0 @@
import { Tabs, TabSlot } from 'expo-router/ui' // ✅ 从 expo-router/ui 导入
import { CustomTabBar } from '@/components/CustomTabBar'
export default function TabsLayout() {
return (
<Tabs>
{/* 渲染当前激活的页面内容 */}
<TabSlot />
{/* 您的自定义 TabBar */}
<CustomTabBar
data={[
{ icon: 'AArrowDown', name: 'index', href: '/(tabs)', label: '首页' },
{ icon: 'MessageCircle', name: 'chat', href: '/(tabs)/chat', label: '聊天' },
{ icon: 'Heart', name: 'dating', href: '/(tabs)/dating', label: '约会' },
{ icon: 'LayoutGrid', name: 'app-center', href: '/(tabs)/app-center', label: '应用' },
{ icon: 'User', name: 'profile', href: '/(tabs)/profile', label: '我的' },
]}
/>
</Tabs>
)
}
@@ -1,26 +0,0 @@
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import { StyleSheet, Text, View } from 'react-native'
import { Button, ButtonIcon, ButtonText } from '@/components/ui/button'
export default function Tab() {
const to404 = () => {
router.navigate('/abc')
}
return (
<View style={styles.container}>
<Button onPress={to404} className="flex items-center justify-center">
<ButtonText size="sm">测试</ButtonText>
</Button>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})
-17
View File
@@ -1,17 +0,0 @@
import { StyleSheet, Text, View } from 'react-native'
export default function Tab() {
return (
<View style={styles.container}>
<Text>Chat [Settings]</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})
@@ -1,17 +0,0 @@
import { StyleSheet, Text, View } from 'react-native'
export default function Tab() {
return (
<View style={styles.container}>
<Text>heart [Settings]</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})
@@ -1,42 +0,0 @@
// App.tsx
import React, { useEffect } from 'react'
import { Button, Text, View } from 'react-native'
import { useUserStore } from '@/store/userStore'
export default function App() {
const { userInfo, token, setUserInfo, setToken, logout } = useUserStore()
// 模拟 App 启动后读取状态
useEffect(() => {
console.log('用户信息:', userInfo)
console.log('Token:', token)
}, [userInfo, token])
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>
User Info:
{userInfo ? userInfo.username : '未登录'}
</Text>
<Text>
Token:
{token || '无'}
</Text>
<Button
title="登录"
onPress={() => {
setUserInfo({ userId: '1', username: '张三' })
setToken('abc123')
}}
/>
<Button
title="退出登录"
onPress={() => {
logout()
}}
/>
</View>
)
}
@@ -1,17 +0,0 @@
import { StyleSheet, Text, View } from 'react-native'
export default function Tab() {
return (
<View style={styles.container}>
<Text>Tab [Settings]</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})
@@ -1,67 +0,0 @@
import type { MotiProps } from 'moti'
import { TabList, TabTrigger } from 'expo-router/ui'
import { icons } from 'lucide-react-native'
import { motifySvg } from 'moti/svg'
import { Text, View } from 'react-native'
type IconNames = keyof typeof icons
type IconProp = {
name: IconNames
size?: number
} & MotiProps
interface TabbarItem {
icon: IconNames
name: string
href: string
label: string
}
interface CustomTabBarProps {
data: TabbarItem[]
}
function Icon({ name, size = 24, ...rest }: IconProp) {
const IconComponent = motifySvg(icons[name] as any)()
return <IconComponent size={size} {...rest} />
}
export function CustomTabBar({ data }: CustomTabBarProps) {
return (
<TabList
asChild
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
borderTopWidth: 1,
borderTopColor: '#e5e5e5',
}}
>
<View className="flex-row justify-around bg-white py-2">
{data.map(item => (
<TabTrigger
key={item.name}
name={item.name}
href={item.href}
asChild
>
<View className="items-center px-3 py-2 active:opacity-70">
<Icon
name={item.icon}
size={24}
from={{ scale: 1 }}
animate={{ scale: 1 }}
transition={{ type: 'timing', duration: 150 }}
/>
<Text className="text-xs mt-1 text-gray-600">{item.label}</Text>
</View>
</TabTrigger>
))}
</View>
</TabList>
)
}
-31
View File
@@ -1,31 +0,0 @@
// store/userStore.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
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
logout: () => void
}
export 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) // 使用 get 获取最新状态
set({ userInfo: null, token: null })
},
}),
{
name: 'user-storage', // 存储在 AsyncStorage 的 key
storage: createJSONStorage(() => AsyncStorage),
},
),
)
-3
View File
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-91
View File
@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TabBar 毛玻璃示例</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
background:red; /* 浅灰渐变背景 */
display: flex;
flex-direction: column;
justify-content: flex-end; /* TabBar 固定底部 */
}
.content {
flex: 1;
padding: 20px;
}
.tabbar {
display: flex;
justify-content: space-around;
align-items: center;
height: 70px;
backdrop-filter: blur(12px); /* 毛玻璃效果 */
-webkit-backdrop-filter: blur(12px);
background-color: rgba(255, 255, 255, 0.4); /* 半透明叠加色,让白色背景可见毛玻璃 */
border-top: 1px solid rgba(200, 200, 200, 0.3);
border-radius: 20px 20px 0 0;
padding-bottom: 10px;
padding-top: 10px;
}
.tabbar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #333;
font-size: 12px;
text-decoration: none;
}
.tabbar-item.active {
color: #007aff;
}
/* 图标示例,用圆圈代替 */
.icon {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: currentColor;
margin-bottom: 4px;
}
</style>
</head>
<body>
<div class="content">
<p>这里是页面内容,TabBar 放在底部。</p>
<p>背景是浅灰渐变,所以毛玻璃效果会明显。</p>
</div>
<div class="tabbar">
<a href="#" class="tabbar-item active">
<div class="icon"></div>
Finance
</a>
<a href="#" class="tabbar-item">
<div class="icon"></div>
Dating
</a>
<a href="#" class="tabbar-item">
<div class="icon"></div>
App
</a>
<a href="#" class="tabbar-item">
<div class="icon"></div>
Chat
</a>
<a href="#" class="tabbar-item">
<div class="icon"></div>
Profile
</a>
</div>
</body>
</html>
File renamed without changes.
File renamed without changes.
@@ -2,12 +2,12 @@
"expo": {
"name": "base-app",
"slug": "base-app",
"scheme": "oneao",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"scheme": "oneao",
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
File renamed without changes.
@@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true)
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
}
}
@@ -1,6 +1,7 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config')
const { withNativeWind } = require('nativewind/metro')
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname)
module.exports = withNativeWind(config, { input: './src/styles/global.css' })
+1
View File
@@ -0,0 +1 @@
/// <reference types="nativewind/types" />
@@ -10,34 +10,31 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@gluestack-ui/core": "^3.0.10",
"@gluestack-ui/core": "^3.0.12",
"@gluestack-ui/utils": "^3.0.11",
"@gluestack/ui-next-adapter": "^3.0.3",
"@legendapp/motion": "^2.4.0",
"@react-native-async-storage/async-storage": "2.2.0",
"expo": "~54.0.20",
"expo-blur": "~15.0.7",
"expo-constants": "~18.0.10",
"expo-font": "~14.0.9",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.13",
"expo-router": "~6.0.14",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"lucide-react-native": "^0.548.0",
"moti": "^0.30.0",
"lucide-react-native": "^0.552.0",
"nativewind": "^4.2.1",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~3.17.5",
"react-native-safe-area-context": "5.4.0",
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-worklets": "0.5.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"@antfu/eslint-config": "^6.1.0",
"@antfu/eslint-config": "^6.2.0",
"@types/react": "~19.1.0",
"eslint": "^9.38.0",
"tailwindcss": "^3.4.17",
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
import type { TabbarItem } from '@/components/custom/app-tabbar'
import AppTabbar from '@/components/custom/app-tabbar'
import { useToast } from '@/utils/toast'
export default function TabsLayout() {
const toast = useToast()
const tabbarOptions: TabbarItem[] = [
{
name: 'user',
href: '/user',
icon: 'CircleCheckBig',
},
{
name: 'money',
href: '/money',
icon: 'CircleDollarSign',
},
{
name: 'app',
icon: 'LayoutGrid',
activeIcon: 'Smile',
iconSize: 26,
onPress: () => {
toast.show({
title: '该功能尚未开发,请尽情期待!',
})
},
},
{
name: 'chat',
href: '/chat',
icon: 'MessageCircle',
},
{
name: 'love',
href: '/love',
icon: 'Heart',
},
]
return (
<AppTabbar data={tabbarOptions} />
)
}
@@ -0,0 +1,9 @@
import { Text, View } from 'react-native'
export default function Chat() {
return (
<View>
<Text>Chat</Text>
</View>
)
}
@@ -0,0 +1,9 @@
import { Text, View } from 'react-native'
export default function Love() {
return (
<View>
<Text>Love</Text>
</View>
)
}
@@ -0,0 +1,9 @@
import { Text, View } from 'react-native'
export default function Money() {
return (
<View>
<Text>Money</Text>
</View>
)
}
@@ -0,0 +1,9 @@
import { Text, View } from 'react-native'
export default function User() {
return (
<View>
<Text>User</Text>
</View>
)
}
@@ -1,6 +1,6 @@
import { router, Stack } from 'expo-router'
import { ArrowLeft } from 'lucide-react-native'
import { EmptyState } from '@/components/empty-status'
import { EmptyState } from '@/components/common/empty-status'
export default function NotFound() {
return (
@@ -1,12 +1,14 @@
import type { ErrorBoundaryProps } from 'expo-router'
import { ToastProvider } from '@gluestack-ui/core/toast/creator'
import { useFonts } from 'expo-font'
import { Stack } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
import { StatusBar } from 'expo-status-bar'
import { RefreshCw } from 'lucide-react-native'
import { useEffect } from 'react'
import { EmptyState } from '@/components/empty-status'
import { EmptyState } from '@/components/common/empty-status'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import { ToastProviderWrapper } from '@/utils/toast'
import '@/styles/global.css'
import 'react-native-reanimated'
import 'react-native-gesture-handler'
@@ -29,7 +31,7 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
}
export default function RootLayout() {
const [loaded, error] = useFonts({
const [loaded] = useFonts({
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
})
@@ -41,19 +43,23 @@ export default function RootLayout() {
return (
<GluestackUIProvider>
<Stack
screenOptions={{
headerTitleAlign: 'center',
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
<ToastProvider>
<ToastProviderWrapper>
<Stack
screenOptions={{
headerTitleAlign: 'center',
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
</ToastProviderWrapper>
</ToastProvider>
</GluestackUIProvider>
)
}
@@ -0,0 +1,137 @@
import { usePathname } from 'expo-router'
import { TabList, Tabs, TabSlot, TabTrigger } from 'expo-router/ui'
import { icons } from 'lucide-react-native'
import { useEffect, useState } from 'react'
import { Pressable, View } from 'react-native'
import Animated, {
useAnimatedStyle,
useSharedValue,
withSequence,
withTiming,
} from 'react-native-reanimated'
export interface TabbarItem {
icon: keyof typeof icons
iconSize?: number
activeIcon?: keyof typeof icons
name: string
href?: string
onPress?: () => void
}
interface AppTabbarProps {
data: TabbarItem[]
onTabPress?: (item: TabbarItem) => void
}
function TabButton({
icon,
activeIcon,
iconSize = 24,
isFocused = false,
onPress,
}: {
icon: keyof typeof icons
activeIcon?: keyof typeof icons
iconSize?: number
isFocused?: boolean
onPress?: () => void
}) {
const activeColor = '#333'
const inactiveColor = '#aaa'
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
const IconComponent = icons[isFocused && activeIcon ? activeIcon : icon]
const handlePress = () => {
scale.value = withSequence(
withTiming(0.9, { duration: 200 }),
withTiming(1, { duration: 200 }),
)
onPress?.()
}
return (
<Pressable
onPress={handlePress}
className="flex-1 items-center justify-center h-full"
>
<Animated.View style={animatedStyle}>
<IconComponent
width={iconSize}
height={iconSize}
stroke={isFocused ? activeColor : inactiveColor}
/>
</Animated.View>
</Pressable>
)
}
export default function AppTabbar({ data, onTabPress }: AppTabbarProps) {
const pathname = usePathname()
// 初始选中 Tab
const defaultTab = data.find(item => item.href === pathname)?.name
?? data.find(item => item.href)?.name
?? data[0]?.name
const [activeTab, setActiveTab] = useState<string>(defaultTab)
// 当 pathname 改变(路由跳转)时,同步高亮
useEffect(() => {
const matchTab = data.find(item => item.href === pathname)
if (matchTab) {
setActiveTab(matchTab.name)
}
// 如果没有匹配的路由,保持当前的 activeTab 不变
// 这样无 href 的项就能保持选中状态
}, [pathname, data])
const handleTabPress = (item: TabbarItem) => {
setActiveTab(item.name)
item.onPress?.()
onTabPress?.(item)
}
return (
<Tabs>
<TabSlot />
<TabList asChild>
<View className="flex-row bg-white h-12 items-center">
{data.map((item) => {
const isFocused = activeTab === item.name
if (item.href) {
return (
<TabTrigger key={item.name} name={item.name} href={item.href} asChild>
<TabButton
icon={item.icon}
activeIcon={item.activeIcon}
iconSize={item.iconSize}
isFocused={isFocused}
onPress={() => handleTabPress(item)}
/>
</TabTrigger>
)
}
return (
<TabButton
key={item.name}
icon={item.icon}
activeIcon={item.activeIcon}
iconSize={item.iconSize}
onPress={() => handleTabPress(item)}
isFocused={isFocused}
/>
)
})}
</View>
</TabList>
</Tabs>
)
}
@@ -1,28 +1,27 @@
'use client'
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
import { createButton } from '@gluestack-ui/core/button/creator'
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'
'use client';
import React from 'react';
import { createButton } from '@gluestack-ui/core/button/creator';
import {
tva,
useStyleContext,
withStyleContext,
} from '@gluestack-ui/utils/nativewind-utils'
import { cssInterop } from 'nativewind'
import React from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
useStyleContext,
type VariantProps,
} from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
const SCOPE = 'BUTTON'
const SCOPE = 'BUTTON';
const Root = withStyleContext(Pressable, SCOPE)
const Root = withStyleContext(Pressable, SCOPE);
const UIButton = createButton({
Root,
Root: Root,
Text,
Group: View,
Spinner: ActivityIndicator,
Icon: UIIcon,
})
});
cssInterop(PrimitiveIcon, {
className: {
@@ -35,7 +34,7 @@ cssInterop(PrimitiveIcon, {
stroke: true,
},
},
})
});
const buttonStyle = tva({
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
@@ -117,7 +116,7 @@ const buttonStyle = tva({
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
],
})
});
const buttonTextStyle = tva({
base: 'text-typography-0 font-semibold web:select-none',
@@ -196,7 +195,7 @@ const buttonTextStyle = tva({
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
],
})
});
const buttonIconStyle = tva({
base: 'fill-none',
@@ -252,7 +251,7 @@ const buttonIconStyle = tva({
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
],
})
});
const buttonGroupStyle = tva({
base: '',
@@ -277,13 +276,13 @@ const buttonGroupStyle = tva({
'column-reverse': 'flex-col-reverse',
},
},
})
});
type IButtonProps = Omit<
React.ComponentPropsWithoutRef<typeof UIButton>,
'context'
>
& VariantProps<typeof buttonStyle> & { className?: string }
> &
VariantProps<typeof buttonStyle> & { className?: string };
const Button = React.forwardRef<
React.ElementRef<typeof UIButton>,
@@ -291,7 +290,7 @@ const Button = React.forwardRef<
>(
(
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
ref,
ref
) => {
return (
<UIButton
@@ -300,12 +299,12 @@ const Button = React.forwardRef<
className={buttonStyle({ variant, size, action, class: className })}
context={{ variant, size, action }}
/>
)
},
)
);
}
);
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text>
& VariantProps<typeof buttonTextStyle> & { className?: string }
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
VariantProps<typeof buttonTextStyle> & { className?: string };
const ButtonText = React.forwardRef<
React.ElementRef<typeof UIButton.Text>,
@@ -315,7 +314,7 @@ const ButtonText = React.forwardRef<
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE)
} = useStyleContext(SCOPE);
return (
<UIButton.Text
@@ -333,18 +332,18 @@ const ButtonText = React.forwardRef<
class: className,
})}
/>
)
})
);
});
const ButtonSpinner = UIButton.Spinner
const ButtonSpinner = UIButton.Spinner;
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon>
& VariantProps<typeof buttonIconStyle> & {
className?: string | undefined
as?: React.ElementType
height?: number
width?: number
}
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
VariantProps<typeof buttonIconStyle> & {
className?: string | undefined;
as?: React.ElementType;
height?: number;
width?: number;
};
const ButtonIcon = React.forwardRef<
React.ElementRef<typeof UIButton.Icon>,
@@ -354,7 +353,7 @@ const ButtonIcon = React.forwardRef<
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE)
} = useStyleContext(SCOPE);
if (typeof size === 'number') {
return (
@@ -364,11 +363,10 @@ const ButtonIcon = React.forwardRef<
className={buttonIconStyle({ 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 (
<UIButton.Icon
@@ -376,7 +374,7 @@ const ButtonIcon = React.forwardRef<
{...props}
className={buttonIconStyle({ class: className })}
/>
)
);
}
return (
<UIButton.Icon
@@ -392,11 +390,11 @@ const ButtonIcon = React.forwardRef<
})}
ref={ref}
/>
)
})
);
});
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group>
& VariantProps<typeof buttonGroupStyle>
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
VariantProps<typeof buttonGroupStyle>;
const ButtonGroup = React.forwardRef<
React.ElementRef<typeof UIButton.Group>,
@@ -410,7 +408,7 @@ const ButtonGroup = React.forwardRef<
flexDirection = 'column',
...props
},
ref,
ref
) => {
return (
<UIButton.Group
@@ -423,14 +421,14 @@ const ButtonGroup = React.forwardRef<
{...props}
ref={ref}
/>
)
},
)
);
}
);
Button.displayName = 'Button'
ButtonText.displayName = 'ButtonText'
ButtonSpinner.displayName = 'ButtonSpinner'
ButtonIcon.displayName = 'ButtonIcon'
ButtonGroup.displayName = 'ButtonGroup'
Button.displayName = 'Button';
ButtonText.displayName = 'ButtonText';
ButtonSpinner.displayName = 'ButtonSpinner';
ButtonIcon.displayName = 'ButtonIcon';
ButtonGroup.displayName = 'ButtonGroup';
export { Button, ButtonGroup, ButtonIcon, ButtonSpinner, ButtonText }
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
@@ -0,0 +1,240 @@
'use client'
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
import type {
MotionComponentProps,
} from '@legendapp/motion'
import type { ViewStyle } from 'react-native'
import { createToastHook } from '@gluestack-ui/core/toast/creator'
import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'
import {
AnimatePresence,
Motion,
} from '@legendapp/motion'
import { cssInterop } from 'nativewind'
import React from 'react'
import { AccessibilityInfo, Text, View } from 'react-native'
type IMotionViewProps = React.ComponentProps<typeof View>
& MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>
const useToast = createToastHook(MotionView, AnimatePresence)
const SCOPE = 'TOAST'
cssInterop(MotionView, { className: 'style' })
const toastStyle = tva({
base: 'p-4 m-1 rounded-md gap-1 web:pointer-events-auto shadow-hard-5 border-outline-100',
variants: {
action: {
error: 'bg-error-800',
warning: 'bg-warning-700',
success: 'bg-success-700',
info: 'bg-info-700',
muted: 'bg-background-800',
},
variant: {
solid: '',
outline: 'border bg-background-0',
},
},
})
const toastTitleStyle = tva({
base: 'text-typography-0 font-medium font-body tracking-md text-left',
variants: {
isTruncated: {
true: '',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
},
parentVariants: {
variant: {
solid: '',
outline: '',
},
action: {
error: '',
warning: '',
success: '',
info: '',
muted: '',
},
},
parentCompoundVariants: [
{
variant: 'outline',
action: 'error',
class: 'text-error-800',
},
{
variant: 'outline',
action: 'warning',
class: 'text-warning-800',
},
{
variant: 'outline',
action: 'success',
class: 'text-success-800',
},
{
variant: 'outline',
action: 'info',
class: 'text-info-800',
},
{
variant: 'outline',
action: 'muted',
class: 'text-background-800',
},
],
})
const toastDescriptionStyle = tva({
base: 'font-normal font-body tracking-md text-left',
variants: {
isTruncated: {
true: '',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
},
parentVariants: {
variant: {
solid: 'text-typography-50',
outline: 'text-typography-900',
},
},
})
const Root = withStyleContext(View, SCOPE)
type IToastProps = React.ComponentProps<typeof Root> & {
className?: string
} & VariantProps<typeof toastStyle>
const Toast = React.forwardRef<React.ComponentRef<typeof Root>, IToastProps>(
(
{ className, variant = 'solid', action = 'muted', ...props },
ref,
) => {
return (
<Root
ref={ref}
className={toastStyle({ variant, action, class: className })}
context={{ variant, action }}
{...props}
/>
)
},
)
type IToastTitleProps = React.ComponentProps<typeof Text> & {
className?: string
} & VariantProps<typeof toastTitleStyle>
const ToastTitle = React.forwardRef<
React.ComponentRef<typeof Text>,
IToastTitleProps
>(({ className, size = 'md', children, ...props }, ref) => {
const { variant: parentVariant, action: parentAction }
= useStyleContext(SCOPE)
React.useEffect(() => {
// Issue from react-native side
// Hack for now, will fix this later
AccessibilityInfo.announceForAccessibility(children as string)
}, [children])
return (
<Text
{...props}
ref={ref}
aria-live="assertive"
aria-atomic="true"
role="alert"
className={toastTitleStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
action: parentAction,
},
})}
>
{children}
</Text>
)
})
type IToastDescriptionProps = React.ComponentProps<typeof Text> & {
className?: string
} & VariantProps<typeof toastDescriptionStyle>
const ToastDescription = React.forwardRef<
React.ComponentRef<typeof Text>,
IToastDescriptionProps
>(({ className, size = 'md', ...props }, ref) => {
const { variant: parentVariant } = useStyleContext(SCOPE)
return (
<Text
ref={ref}
{...props}
className={toastDescriptionStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
},
})}
/>
)
})
Toast.displayName = 'Toast'
ToastTitle.displayName = 'ToastTitle'
ToastDescription.displayName = 'ToastDescription'
export { Toast, ToastDescription, ToastTitle, useToast }
@@ -0,0 +1,71 @@
import type { IToastProps } from '@gluestack-ui/core/lib/esm/toast/creator/types'
import React, { useRef } from 'react'
import { Toast, ToastDescription, ToastTitle, useToast as useGluestackToast } from '@/components/ui/toast'
type ToastVariant = 'solid' | 'outline'
type ToastAction = 'error' | 'warning' | 'success' | 'info' | 'muted'
interface ShowToastOptions extends IToastProps {
title?: React.ReactNode
description?: React.ReactNode
variant?: ToastVariant
action?: ToastAction
}
export function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
const toast = useGluestackToast()
const toastRef = useRef(toast)
toastRef.current = toast
return <>{children}</>
}
/**
* 自定义 Hook,用法:
* const toast = useToast();
* toast.show({ ... });
* toast.close(id);
* toast.closeAll();
*/
export function useToast() {
const toast = useGluestackToast()
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),
}
}
@@ -19,4 +19,4 @@
"node_modules",
"dist"
]
}
}
+30 -5
View File
@@ -32,7 +32,7 @@ npx expo start
## 集成 [antfu-eslint](https://github.com/antfu/eslint-config)
可以使用快捷命令:
可以使用快捷命令(推荐):
```bash
pnpm dlx @antfu/eslint-config@latest
@@ -127,7 +127,7 @@ export default antfu({
1. 安装依赖
```bash
npx expo add nativewind react-native-reanimated@~3.17.4 react-native-safe-area-context@5.4.0
npx expo add nativewind react-native-reanimated react-native-safe-area-context
npx expo add tailwindcss@^3.4.17 -- --save-dev
```
@@ -598,6 +598,14 @@ export const ZhaoPinIcon: React.FC<IconProps> = ({ size = 30, color = '#fff', ..
}
```
## 集成[lucide](https://lucide.dev/icons/)
```
npx expo add lucide-react-native
```
## 集成[expo-router](expo-router)
@@ -605,7 +613,7 @@ export const ZhaoPinIcon: React.FC<IconProps> = ({ size = 30, color = '#fff', ..
1. 安装相关依赖
```bash
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
npx expo add expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
```
2. 修改 `package.json`
@@ -705,8 +713,8 @@ lottie例子网站:https://lottiefiles.com/free-animations
安装依赖:
```bash
npx expo install zustand
npx expo install @react-native-async-storage/async-storage
npx expo add zustand
npx expo add @react-native-async-storage/async-storage
```
使用demo:
@@ -791,6 +799,23 @@ export default function App() {
## 集成moti
```bash
npx expo add moti
npx expo add react-native-reanimated react-native-gesture-handler
npx expo add react-native-gesture-handler
```
引入:
```ts
import 'react-native-reanimated'
import 'react-native-gesture-handler'
```
## 配置深度链接