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": {
|
"expo": {
|
||||||
"name": "base-app",
|
"name": "base-app",
|
||||||
"slug": "base-app",
|
"slug": "base-app",
|
||||||
"scheme": "oneao",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
|
"scheme": "oneao",
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash-icon.png",
|
"image": "./assets/splash-icon.png",
|
||||||
"resizeMode": "contain",
|
"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 { getDefaultConfig } = require('expo/metro-config')
|
||||||
const { withNativeWind } = require('nativewind/metro')
|
const { withNativeWind } = require('nativewind/metro')
|
||||||
|
/** @type {import('expo/metro-config').MetroConfig} */
|
||||||
const config = getDefaultConfig(__dirname)
|
const config = getDefaultConfig(__dirname)
|
||||||
|
|
||||||
module.exports = withNativeWind(config, { input: './src/styles/global.css' })
|
module.exports = withNativeWind(config, { input: './src/styles/global.css' })
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="nativewind/types" />
|
||||||
@@ -10,34 +10,31 @@
|
|||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@gluestack-ui/core": "^3.0.12",
|
||||||
"@gluestack-ui/core": "^3.0.10",
|
|
||||||
"@gluestack-ui/utils": "^3.0.11",
|
"@gluestack-ui/utils": "^3.0.11",
|
||||||
"@gluestack/ui-next-adapter": "^3.0.3",
|
"@gluestack/ui-next-adapter": "^3.0.3",
|
||||||
|
"@legendapp/motion": "^2.4.0",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"expo": "~54.0.20",
|
"expo": "~54.0.20",
|
||||||
"expo-blur": "~15.0.7",
|
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-router": "~6.0.13",
|
"expo-router": "~6.0.14",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.10",
|
||||||
"expo-status-bar": "~3.0.8",
|
"expo-status-bar": "~3.0.8",
|
||||||
"lucide-react-native": "^0.548.0",
|
"lucide-react-native": "^0.552.0",
|
||||||
"moti": "^0.30.0",
|
|
||||||
"nativewind": "^4.2.1",
|
"nativewind": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~3.17.5",
|
"react-native-reanimated": "~4.1.3",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "~5.6.1",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-worklets": "0.5.1",
|
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.1.0",
|
"@antfu/eslint-config": "^6.2.0",
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"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 { router, Stack } from 'expo-router'
|
||||||
import { ArrowLeft } from 'lucide-react-native'
|
import { ArrowLeft } from 'lucide-react-native'
|
||||||
import { EmptyState } from '@/components/empty-status'
|
import { EmptyState } from '@/components/common/empty-status'
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
+21
-15
@@ -1,12 +1,14 @@
|
|||||||
import type { ErrorBoundaryProps } from 'expo-router'
|
import type { ErrorBoundaryProps } from 'expo-router'
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator'
|
||||||
import { useFonts } from 'expo-font'
|
import { useFonts } from 'expo-font'
|
||||||
import { Stack } from 'expo-router'
|
import { Stack } from 'expo-router'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { RefreshCw } from 'lucide-react-native'
|
import { RefreshCw } from 'lucide-react-native'
|
||||||
import { useEffect } from 'react'
|
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 { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
|
||||||
|
import { ToastProviderWrapper } from '@/utils/toast'
|
||||||
import '@/styles/global.css'
|
import '@/styles/global.css'
|
||||||
import 'react-native-reanimated'
|
import 'react-native-reanimated'
|
||||||
import 'react-native-gesture-handler'
|
import 'react-native-gesture-handler'
|
||||||
@@ -29,7 +31,7 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const [loaded, error] = useFonts({
|
const [loaded] = useFonts({
|
||||||
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
|
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,19 +43,23 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GluestackUIProvider>
|
<GluestackUIProvider>
|
||||||
<Stack
|
<ToastProvider>
|
||||||
screenOptions={{
|
<ToastProviderWrapper>
|
||||||
headerTitleAlign: 'center',
|
<Stack
|
||||||
animation: 'slide_from_right',
|
screenOptions={{
|
||||||
}}
|
headerTitleAlign: 'center',
|
||||||
>
|
animation: 'slide_from_right',
|
||||||
<Stack.Screen
|
}}
|
||||||
name="(tabs)"
|
>
|
||||||
options={{ headerShown: false }}
|
<Stack.Screen
|
||||||
/>
|
name="(tabs)"
|
||||||
<Stack.Screen name="+not-found" />
|
options={{ headerShown: false }}
|
||||||
</Stack>
|
/>
|
||||||
<StatusBar style="auto" />
|
<Stack.Screen name="+not-found" />
|
||||||
|
</Stack>
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
</ToastProviderWrapper>
|
||||||
|
</ToastProvider>
|
||||||
</GluestackUIProvider>
|
</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'
|
'use client';
|
||||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
|
import React from 'react';
|
||||||
import { createButton } from '@gluestack-ui/core/button/creator'
|
import { createButton } from '@gluestack-ui/core/button/creator';
|
||||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'
|
|
||||||
import {
|
import {
|
||||||
tva,
|
tva,
|
||||||
useStyleContext,
|
|
||||||
|
|
||||||
withStyleContext,
|
withStyleContext,
|
||||||
} from '@gluestack-ui/utils/nativewind-utils'
|
useStyleContext,
|
||||||
import { cssInterop } from 'nativewind'
|
type VariantProps,
|
||||||
import React from 'react'
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
|
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({
|
const UIButton = createButton({
|
||||||
Root,
|
Root: Root,
|
||||||
Text,
|
Text,
|
||||||
Group: View,
|
Group: View,
|
||||||
Spinner: ActivityIndicator,
|
Spinner: ActivityIndicator,
|
||||||
Icon: UIIcon,
|
Icon: UIIcon,
|
||||||
})
|
});
|
||||||
|
|
||||||
cssInterop(PrimitiveIcon, {
|
cssInterop(PrimitiveIcon, {
|
||||||
className: {
|
className: {
|
||||||
@@ -35,7 +34,7 @@ cssInterop(PrimitiveIcon, {
|
|||||||
stroke: true,
|
stroke: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const buttonStyle = tva({
|
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',
|
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',
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const buttonTextStyle = tva({
|
const buttonTextStyle = tva({
|
||||||
base: 'text-typography-0 font-semibold web:select-none',
|
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',
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const buttonIconStyle = tva({
|
const buttonIconStyle = tva({
|
||||||
base: 'fill-none',
|
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',
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const buttonGroupStyle = tva({
|
const buttonGroupStyle = tva({
|
||||||
base: '',
|
base: '',
|
||||||
@@ -277,13 +276,13 @@ const buttonGroupStyle = tva({
|
|||||||
'column-reverse': 'flex-col-reverse',
|
'column-reverse': 'flex-col-reverse',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
type IButtonProps = Omit<
|
type IButtonProps = Omit<
|
||||||
React.ComponentPropsWithoutRef<typeof UIButton>,
|
React.ComponentPropsWithoutRef<typeof UIButton>,
|
||||||
'context'
|
'context'
|
||||||
>
|
> &
|
||||||
& VariantProps<typeof buttonStyle> & { className?: string }
|
VariantProps<typeof buttonStyle> & { className?: string };
|
||||||
|
|
||||||
const Button = React.forwardRef<
|
const Button = React.forwardRef<
|
||||||
React.ElementRef<typeof UIButton>,
|
React.ElementRef<typeof UIButton>,
|
||||||
@@ -291,7 +290,7 @@ const Button = React.forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
|
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<UIButton
|
<UIButton
|
||||||
@@ -300,12 +299,12 @@ const Button = React.forwardRef<
|
|||||||
className={buttonStyle({ variant, size, action, class: className })}
|
className={buttonStyle({ variant, size, action, class: className })}
|
||||||
context={{ variant, size, action }}
|
context={{ variant, size, action }}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text>
|
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
|
||||||
& VariantProps<typeof buttonTextStyle> & { className?: string }
|
VariantProps<typeof buttonTextStyle> & { className?: string };
|
||||||
|
|
||||||
const ButtonText = React.forwardRef<
|
const ButtonText = React.forwardRef<
|
||||||
React.ElementRef<typeof UIButton.Text>,
|
React.ElementRef<typeof UIButton.Text>,
|
||||||
@@ -315,7 +314,7 @@ const ButtonText = React.forwardRef<
|
|||||||
variant: parentVariant,
|
variant: parentVariant,
|
||||||
size: parentSize,
|
size: parentSize,
|
||||||
action: parentAction,
|
action: parentAction,
|
||||||
} = useStyleContext(SCOPE)
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UIButton.Text
|
<UIButton.Text
|
||||||
@@ -333,18 +332,18 @@ const ButtonText = React.forwardRef<
|
|||||||
class: className,
|
class: className,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
const ButtonSpinner = UIButton.Spinner
|
const ButtonSpinner = UIButton.Spinner;
|
||||||
|
|
||||||
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon>
|
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
|
||||||
& VariantProps<typeof buttonIconStyle> & {
|
VariantProps<typeof buttonIconStyle> & {
|
||||||
className?: string | undefined
|
className?: string | undefined;
|
||||||
as?: React.ElementType
|
as?: React.ElementType;
|
||||||
height?: number
|
height?: number;
|
||||||
width?: number
|
width?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ButtonIcon = React.forwardRef<
|
const ButtonIcon = React.forwardRef<
|
||||||
React.ElementRef<typeof UIButton.Icon>,
|
React.ElementRef<typeof UIButton.Icon>,
|
||||||
@@ -354,7 +353,7 @@ const ButtonIcon = React.forwardRef<
|
|||||||
variant: parentVariant,
|
variant: parentVariant,
|
||||||
size: parentSize,
|
size: parentSize,
|
||||||
action: parentAction,
|
action: parentAction,
|
||||||
} = useStyleContext(SCOPE)
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
if (typeof size === 'number') {
|
if (typeof size === 'number') {
|
||||||
return (
|
return (
|
||||||
@@ -364,11 +363,10 @@ const ButtonIcon = React.forwardRef<
|
|||||||
className={buttonIconStyle({ class: className })}
|
className={buttonIconStyle({ class: className })}
|
||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
} else if (
|
||||||
else if (
|
(props.height !== undefined || props.width !== undefined) &&
|
||||||
(props.height !== undefined || props.width !== undefined)
|
size === undefined
|
||||||
&& size === undefined
|
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<UIButton.Icon
|
<UIButton.Icon
|
||||||
@@ -376,7 +374,7 @@ const ButtonIcon = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
className={buttonIconStyle({ class: className })}
|
className={buttonIconStyle({ class: className })}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<UIButton.Icon
|
<UIButton.Icon
|
||||||
@@ -392,11 +390,11 @@ const ButtonIcon = React.forwardRef<
|
|||||||
})}
|
})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group>
|
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
|
||||||
& VariantProps<typeof buttonGroupStyle>
|
VariantProps<typeof buttonGroupStyle>;
|
||||||
|
|
||||||
const ButtonGroup = React.forwardRef<
|
const ButtonGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof UIButton.Group>,
|
React.ElementRef<typeof UIButton.Group>,
|
||||||
@@ -410,7 +408,7 @@ const ButtonGroup = React.forwardRef<
|
|||||||
flexDirection = 'column',
|
flexDirection = 'column',
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<UIButton.Group
|
<UIButton.Group
|
||||||
@@ -423,14 +421,14 @@ const ButtonGroup = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
Button.displayName = 'Button'
|
Button.displayName = 'Button';
|
||||||
ButtonText.displayName = 'ButtonText'
|
ButtonText.displayName = 'ButtonText';
|
||||||
ButtonSpinner.displayName = 'ButtonSpinner'
|
ButtonSpinner.displayName = 'ButtonSpinner';
|
||||||
ButtonIcon.displayName = 'ButtonIcon'
|
ButtonIcon.displayName = 'ButtonIcon';
|
||||||
ButtonGroup.displayName = 'ButtonGroup'
|
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",
|
"node_modules",
|
||||||
"dist"
|
"dist"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ npx expo start
|
|||||||
|
|
||||||
## 集成 [antfu-eslint](https://github.com/antfu/eslint-config)
|
## 集成 [antfu-eslint](https://github.com/antfu/eslint-config)
|
||||||
|
|
||||||
可以使用快捷命令:
|
可以使用快捷命令(推荐):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dlx @antfu/eslint-config@latest
|
pnpm dlx @antfu/eslint-config@latest
|
||||||
@@ -127,7 +127,7 @@ export default antfu({
|
|||||||
1. 安装依赖
|
1. 安装依赖
|
||||||
|
|
||||||
```bash
|
```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
|
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)
|
## 集成[expo-router](expo-router)
|
||||||
@@ -605,7 +613,7 @@ export const ZhaoPinIcon: React.FC<IconProps> = ({ size = 30, color = '#fff', ..
|
|||||||
1. 安装相关依赖
|
1. 安装相关依赖
|
||||||
|
|
||||||
```bash
|
```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`
|
2. 修改 `package.json`
|
||||||
@@ -705,8 +713,8 @@ lottie例子网站:https://lottiefiles.com/free-animations
|
|||||||
安装依赖:
|
安装依赖:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx expo install zustand
|
npx expo add zustand
|
||||||
npx expo install @react-native-async-storage/async-storage
|
npx expo add @react-native-async-storage/async-storage
|
||||||
```
|
```
|
||||||
|
|
||||||
使用demo:
|
使用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