u
This commit is contained in:
1 parent
1d6bb2b7f5
commit
458f0cd6ac
55 files changed
+1052
-999
No files matched your search
@@ -1,9 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
||||
-3
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true)
|
||||
return {
|
||||
presets: [
|
||||
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
||||
'nativewind/babel',
|
||||
],
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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' })
|
||||
@@ -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",
|
||||
+393
-561
File diff suppressed because it is too large.
Load diff
File renamed without changes.
@@ -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>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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 (
|
||||
+21
-15
@@ -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>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -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>
|
||||
)
|
||||
}
|
||||
+57
-59
@@ -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 };
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 }
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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),
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -19,4 +19,4 @@
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 配置深度链接
|
||||
|
||||
Reference in new issue
Block a user