u
This commit is contained in:
1 parent
61e29b5f76
commit
157c84b0b4
15 files changed
+598
-320
No files matched your search
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Print React Native Component (NativeWind)": {
|
||||||
|
"scope": "typescriptreact,javascriptreact",
|
||||||
|
"prefix": "rn",
|
||||||
|
"body": [
|
||||||
|
"import React from \"react\";",
|
||||||
|
"import { View, Text } from \"react-native\";",
|
||||||
|
"",
|
||||||
|
"export default function ${1/(.*)/${1:/pascalcase}/}() {",
|
||||||
|
" return (",
|
||||||
|
" <View className=\"flex-1\">",
|
||||||
|
" <Text className=\"text-base text-black\">${2:Hello RN}</Text>",
|
||||||
|
" </View>",
|
||||||
|
" );",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "React Native NativeWind 函数组件模板"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button, ButtonText } from "@/components/ui/button";
|
||||||
|
import { Slot } from "expo-router";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
isDisabled={false}
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate("/(tabs)/home");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonText>Home</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
isDisabled={false}
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate("/(tabs)/chat");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonText>Chat</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
isDisabled={false}
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate("/money/home");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonText>Money</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Slot />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "react-native";
|
||||||
|
|
||||||
|
export default function Chat() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-base text-black">Chat</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "react-native";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-base text-black">Home</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useIsDark } from "@/hooks/use-color-scheme";
|
import { useIsDark } from "@/hooks/use-color-scheme";
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
|
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
|
||||||
import { StatusBar } from "expo-status-bar";
|
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const isDark = useIsDark();
|
const isDark = useIsDark();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(isDark);
|
|
||||||
}, [isDark]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
|
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
|
||||||
{/* <StatusBar style={isDark ? "light" : "dark"} /> */}
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
<Stack screenOptions={{ headerShown: false }} />
|
headerTitleAlign: "center",
|
||||||
|
animation: "slide_from_right",
|
||||||
|
contentStyle: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import Layout from "@/components/layout";
|
|
||||||
import { View, Text } from "react-native";
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<View>
|
|
||||||
<Text>123</Text>
|
|
||||||
</View>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// import React, { useState } from "react";
|
|
||||||
// import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
||||||
// import { Ionicons } from "@expo/vector-icons";
|
|
||||||
|
|
||||||
// const TABS = [
|
|
||||||
// { label: "首页", icon: "home-outline", activeIcon: "home" },
|
|
||||||
// { label: "统计", icon: "stats-chart-outline", activeIcon: "stats-chart" },
|
|
||||||
// { label: "应用", icon: "grid-outline", activeIcon: "grid" }, // 中间
|
|
||||||
// { label: "聊天", icon: "chatbubble-ellipses-outline", activeIcon: "chatbubble-ellipses" },
|
|
||||||
// { label: "我的", icon: "person-outline", activeIcon: "person" },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// export default function CapsuleTabBarIconDemo() {
|
|
||||||
// const [active, setActive] = useState(0);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <View style={styles.container}>
|
|
||||||
// <View style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
// {/* 👇 悬浮 TabBar */}
|
|
||||||
// <View style={styles.wrapper}>
|
|
||||||
// <View style={styles.tabBar}>
|
|
||||||
// {TABS.map((item, index) => {
|
|
||||||
// const isActive = index === active;
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <TouchableOpacity
|
|
||||||
// key={index}
|
|
||||||
// style={styles.tabItem}
|
|
||||||
// onPress={() => setActive(index)}
|
|
||||||
// activeOpacity={0.8}
|
|
||||||
// >
|
|
||||||
// <Ionicons
|
|
||||||
// name={isActive ? item.activeIcon : item.icon}
|
|
||||||
// size={20}
|
|
||||||
// color={isActive ? "#111" : "#888"}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Text style={[styles.text, isActive && styles.activeText]}>{item.label}</Text>
|
|
||||||
// </TouchableOpacity>
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </View>
|
|
||||||
// </View>
|
|
||||||
// </View>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const styles = StyleSheet.create({
|
|
||||||
// container: {
|
|
||||||
// flex: 1,
|
|
||||||
// backgroundColor: "#F2F3F5",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// wrapper: {
|
|
||||||
// position: "absolute",
|
|
||||||
// left: 0,
|
|
||||||
// right: 0,
|
|
||||||
// bottom: 24,
|
|
||||||
// alignItems: "center",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// tabBar: {
|
|
||||||
// flexDirection: "row",
|
|
||||||
// height: 64,
|
|
||||||
// paddingHorizontal: 8,
|
|
||||||
// borderRadius: 999,
|
|
||||||
|
|
||||||
// // ✅ 半透明更高级
|
|
||||||
// backgroundColor: "rgba(255,255,255,0.95)",
|
|
||||||
|
|
||||||
// // ✅ 极弱阴影(可去掉)
|
|
||||||
// shadowColor: "#000",
|
|
||||||
// shadowOffset: { width: 0, height: 4 },
|
|
||||||
// shadowOpacity: 0.05,
|
|
||||||
// shadowRadius: 12,
|
|
||||||
// },
|
|
||||||
|
|
||||||
// tabItem: {
|
|
||||||
// flex: 1,
|
|
||||||
// alignItems: "center",
|
|
||||||
// justifyContent: "center",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// text: {
|
|
||||||
// fontSize: 11,
|
|
||||||
// marginTop: 2,
|
|
||||||
// color: "#888",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// activeText: {
|
|
||||||
// color: "#111",
|
|
||||||
// fontWeight: "600",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// // ⭐ 精髓:比背景高亮更高级
|
|
||||||
// indicator: {
|
|
||||||
// position: "absolute",
|
|
||||||
// bottom: 6,
|
|
||||||
// width: 16,
|
|
||||||
// height: 3,
|
|
||||||
// borderRadius: 2,
|
|
||||||
// backgroundColor: "#111",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
CircleDollarSignIcon,
|
||||||
|
ChartNoAxesColumnIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "lucide-react-native";
|
||||||
|
import ModuleLayout from "@/layouts/ModuleLayout";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: "/money/home",
|
||||||
|
icon: CircleDollarSignIcon,
|
||||||
|
title: "记账",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
icon: PlusIcon,
|
||||||
|
isCenter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "/money/ana",
|
||||||
|
icon: ChartNoAxesColumnIcon,
|
||||||
|
title: "分析",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return <ModuleLayout tabs={tabs} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text, ScrollView } from "react-native";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<ScrollView className="flex-1 px-4 py-2">
|
||||||
|
<Text className="text-xl font-semibold mb-4">Money Home</Text>
|
||||||
|
|
||||||
|
{Array.from({ length: 30 }).map((_, i) => (
|
||||||
|
<View key={i} className="p-4 mb-3 rounded-xl bg-gray-100">
|
||||||
|
<Text className="text-black">这是第 {i + 1} 条内容</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,49 +1,22 @@
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { View, Text, Pressable, StyleSheet } from "react-native";
|
import { View } from "react-native";
|
||||||
import {
|
|
||||||
ChevronLeft,
|
|
||||||
SlidersHorizontal,
|
|
||||||
CirclePower,
|
|
||||||
House,
|
|
||||||
LayoutGrid,
|
|
||||||
} from "lucide-react-native";
|
|
||||||
import { Shadow } from "react-native-shadow-2";
|
|
||||||
|
|
||||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
|
import Tabbar from "./tabbar";
|
||||||
import Svg, { Path } from "react-native-svg";
|
import { TabbarItem } from "./tabbar";
|
||||||
type LayoutProps = {
|
|
||||||
|
import Navbar from "./navbar";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
tabs: TabbarItem[];
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
}
|
||||||
|
|
||||||
const AnimatedView = Animated.View;
|
export default function Layout({ tabs, children }: LayoutProps) {
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
// 返回按钮
|
|
||||||
const scale = useSharedValue(1);
|
|
||||||
const style = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ scale: scale.value }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 右侧按钮动画
|
|
||||||
const leftScale = useSharedValue(1);
|
|
||||||
const rightScale = useSharedValue(1);
|
|
||||||
|
|
||||||
const leftStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ scale: leftScale.value }],
|
|
||||||
opacity: leftScale.value === 1 ? 1 : 0.7,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const rightStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ scale: rightScale.value }],
|
|
||||||
opacity: rightScale.value === 1 ? 1 : 0.7,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-white">
|
<View className="flex-1 bg-white">
|
||||||
{/* 内容区域 */}
|
|
||||||
<View
|
<View
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
style={{
|
style={{
|
||||||
@@ -54,167 +27,14 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Header */}
|
<Navbar title="123" style={{ paddingTop: insets.top }} />
|
||||||
<View
|
|
||||||
pointerEvents="box-none"
|
|
||||||
className="absolute top-0 left-0 right-0"
|
|
||||||
style={{ paddingTop: insets.top }}
|
|
||||||
>
|
|
||||||
<View className="h-12 flex-row items-center mx-3">
|
|
||||||
{/* 返回按钮 */}
|
|
||||||
<Pressable
|
|
||||||
onPressIn={() => {
|
|
||||||
scale.value = withSpring(0.8);
|
|
||||||
}}
|
|
||||||
onPressOut={() => {
|
|
||||||
scale.value = withSpring(1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AnimatedView style={style}>
|
|
||||||
<Shadow
|
|
||||||
distance={32}
|
|
||||||
startColor="rgba(0,0,0,0.03)"
|
|
||||||
endColor="rgba(255,255,255,0)"
|
|
||||||
offset={[0, 0]}
|
|
||||||
paintInside={false}
|
|
||||||
safeRender
|
|
||||||
style={styles.iconButtonShadow}
|
|
||||||
>
|
|
||||||
<ChevronLeft size={24} />
|
|
||||||
</Shadow>
|
|
||||||
</AnimatedView>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{/* 标题 */}
|
|
||||||
<View className="flex-1 px-4">
|
|
||||||
<Text className="text-xl font-semibold tracking-tight text-black/90">记账</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧按钮组 */}
|
|
||||||
<Shadow
|
|
||||||
distance={32}
|
|
||||||
startColor="rgba(0,0,0,0.03)"
|
|
||||||
endColor="rgba(255,255,255,0)"
|
|
||||||
offset={[0, 0]}
|
|
||||||
paintInside={false}
|
|
||||||
safeRender
|
|
||||||
style={styles.groupShadow}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
onPressIn={() => {
|
|
||||||
leftScale.value = withSpring(0.8);
|
|
||||||
}}
|
|
||||||
onPressOut={() => {
|
|
||||||
leftScale.value = withSpring(1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AnimatedView style={leftStyle} className="w-14 items-center justify-center">
|
|
||||||
<SlidersHorizontal size={20} />
|
|
||||||
</AnimatedView>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<View className="w-[1px] h-4 bg-black/10" />
|
|
||||||
|
|
||||||
{/* 右按钮 */}
|
|
||||||
<Pressable
|
|
||||||
onPressIn={() => {
|
|
||||||
rightScale.value = withSpring(0.8);
|
|
||||||
}}
|
|
||||||
onPressOut={() => {
|
|
||||||
rightScale.value = withSpring(1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AnimatedView style={rightStyle} className="w-14 items-center justify-center">
|
|
||||||
<CirclePower size={20} />
|
|
||||||
</AnimatedView>
|
|
||||||
</Pressable>
|
|
||||||
</Shadow>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* tabbar */}
|
|
||||||
<View
|
<View
|
||||||
className="absolute left-0 right-0 bottom-0"
|
className="absolute left-0 right-0 bottom-0"
|
||||||
style={{ paddingBottom: insets.bottom + 6 }}
|
style={{ paddingBottom: insets.bottom + 8 }}
|
||||||
>
|
>
|
||||||
<View className="m-3">
|
<Tabbar tabs={tabs} />
|
||||||
<Shadow
|
|
||||||
distance={32}
|
|
||||||
startColor="rgba(0,0,0,0.03)"
|
|
||||||
endColor="rgba(255,255,255,0)"
|
|
||||||
offset={[0, 0]}
|
|
||||||
paintInside={false}
|
|
||||||
safeRender
|
|
||||||
style={styles.tabbarShadown}
|
|
||||||
>
|
|
||||||
<View className="w-full h-16 flex-row items-center justify-between">
|
|
||||||
<View className="flex-1 items-center justify-center">
|
|
||||||
<House size={24} color="#666" />
|
|
||||||
<Text className="text-[10px] text-[#666] mt-1">首页</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex-1 items-center justify-center">
|
|
||||||
<House size={24} color="#666" />
|
|
||||||
<Text className="text-[10px] text-[#666] mt-1">分类</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex-1 items-center justify-center" style={{ position: "relative" }}>
|
|
||||||
<Pressable>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 68,
|
|
||||||
height: 68,
|
|
||||||
borderRadius: 999,
|
|
||||||
backgroundColor: "#5B8CFF",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderWidth: 8,
|
|
||||||
borderColor: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayoutGrid size={24} color="white" />
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<View className="flex-1 items-center justify-center">
|
|
||||||
<House size={24} color="#666" />
|
|
||||||
<Text className="text-[10px] text-[#666] mt-1">统计</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex-1 items-center justify-center">
|
|
||||||
<House size={24} color="#666" />
|
|
||||||
<Text className="text-[10px] text-[#666] mt-1">我的</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Shadow>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
iconButtonShadow: {
|
|
||||||
borderRadius: 999,
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: "rgba(255,255,255,0.6)",
|
|
||||||
},
|
|
||||||
groupShadow: {
|
|
||||||
borderRadius: 999,
|
|
||||||
height: 36,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: "rgba(255,255,255,0.6)",
|
|
||||||
},
|
|
||||||
tabbarShadown: {
|
|
||||||
borderRadius: 999,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: "rgba(255,255,255,0.6)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text, StyleProp, ViewStyle, Pressable, StyleSheet } from "react-native";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
|
||||||
|
import { Shadow } from "react-native-shadow-2";
|
||||||
|
import { ChevronLeft, SlidersHorizontal, CirclePower } from "lucide-react-native";
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
showBack?: boolean;
|
||||||
|
onBack?: () => void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimatedView = Animated.View;
|
||||||
|
|
||||||
|
export default function Navbar({ title, showBack = true, onBack, className, style }: NavbarProps) {
|
||||||
|
// 返回按钮
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const backAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 右侧按钮动画
|
||||||
|
const leftScale = useSharedValue(1);
|
||||||
|
const rightScale = useSharedValue(1);
|
||||||
|
|
||||||
|
const leftStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: leftScale.value }],
|
||||||
|
opacity: leftScale.value === 1 ? 1 : 0.7,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rightStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: rightScale.value }],
|
||||||
|
opacity: rightScale.value === 1 ? 1 : 0.7,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <View
|
||||||
|
// pointerEvents="box-none"
|
||||||
|
// className={cn("absolute top-0 left-0 right-0", className)}
|
||||||
|
// style={[style]}
|
||||||
|
// >
|
||||||
|
<View
|
||||||
|
className={cn("flex-row gap-3 items-center justify-between mx-3", className)}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
...style,
|
||||||
|
height: 48
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{showBack && (
|
||||||
|
<Pressable
|
||||||
|
onPress={onBack}
|
||||||
|
onPressIn={() => {
|
||||||
|
scale.value = withSpring(0.8);
|
||||||
|
}}
|
||||||
|
onPressOut={() => {
|
||||||
|
scale.value = withSpring(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedView style={backAnimatedStyle}>
|
||||||
|
<Shadow
|
||||||
|
distance={20}
|
||||||
|
startColor="rgba(0,0,0,0.035)"
|
||||||
|
endColor="rgba(255,255,255,0)"
|
||||||
|
offset={[0, 0]}
|
||||||
|
paintInside={false}
|
||||||
|
safeRender
|
||||||
|
style={styles.iconButtonShadow}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={24} />
|
||||||
|
</Shadow>
|
||||||
|
</AnimatedView>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{title && (
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-xl font-semibold tracking-tight text-black/90">{title}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Shadow
|
||||||
|
distance={20}
|
||||||
|
startColor="rgba(0,0,0,0.035)"
|
||||||
|
endColor="rgba(255,255,255,0)"
|
||||||
|
offset={[0, 0]}
|
||||||
|
paintInside={false}
|
||||||
|
safeRender
|
||||||
|
style={styles.groupShadow}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPressIn={() => {
|
||||||
|
leftScale.value = withSpring(0.8);
|
||||||
|
}}
|
||||||
|
onPressOut={() => {
|
||||||
|
leftScale.value = withSpring(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedView style={leftStyle} className="w-14 items-center justify-center">
|
||||||
|
<SlidersHorizontal size={20} />
|
||||||
|
</AnimatedView>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<View className="w-[1px] h-4 bg-black/10" />
|
||||||
|
|
||||||
|
{/* 右按钮 */}
|
||||||
|
<Pressable
|
||||||
|
onPressIn={() => {
|
||||||
|
rightScale.value = withSpring(0.8);
|
||||||
|
}}
|
||||||
|
onPressOut={() => {
|
||||||
|
rightScale.value = withSpring(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedView style={rightStyle} className="w-14 items-center justify-center">
|
||||||
|
<CirclePower size={20} />
|
||||||
|
</AnimatedView>
|
||||||
|
</Pressable>
|
||||||
|
</Shadow>
|
||||||
|
</View>
|
||||||
|
// </View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
iconButtonShadow: {
|
||||||
|
borderRadius: 999,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: "rgba(255,255,255,0.6)",
|
||||||
|
},
|
||||||
|
groupShadow: {
|
||||||
|
borderRadius: 999,
|
||||||
|
height: 36,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: "rgba(255,255,255,0.6)",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { View, StyleProp, ViewStyle, Pressable, StyleSheet } from "react-native";
|
||||||
|
import { LucideIcon } from "lucide-react-native";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import { Shadow } from "react-native-shadow-2";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
|
||||||
|
|
||||||
|
export interface TabbarItem {
|
||||||
|
id: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
title?: string;
|
||||||
|
isCenter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabbarProps {
|
||||||
|
tabs: TabbarItem[];
|
||||||
|
value?: string;
|
||||||
|
onChange?: (id: string) => void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tabbar({ tabs, value, onChange, className, style }: TabbarProps) {
|
||||||
|
const [innerValue, setInnerValue] = useState(tabs[0]?.id);
|
||||||
|
|
||||||
|
const activeTab = value ?? innerValue;
|
||||||
|
|
||||||
|
const handlePress = (id: string) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
setInnerValue(id);
|
||||||
|
}
|
||||||
|
onChange?.(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={cn("mx-3", className)} style={style}>
|
||||||
|
<Shadow
|
||||||
|
distance={20}
|
||||||
|
startColor="rgba(0,0,0,0.035)"
|
||||||
|
endColor="rgba(255,255,255,0)"
|
||||||
|
style={styles.tabbarShadow}
|
||||||
|
>
|
||||||
|
<View className="w-full flex-row items-center justify-between">
|
||||||
|
{tabs.map((tab) =>
|
||||||
|
tab.isCenter ? (
|
||||||
|
<CenterButton key={tab.id} tab={tab} onPress={() => handlePress(tab.id)} />
|
||||||
|
) : (
|
||||||
|
<TabItem
|
||||||
|
key={tab.id}
|
||||||
|
tab={tab}
|
||||||
|
isActive={activeTab === tab.id}
|
||||||
|
onPress={() => handlePress(tab.id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Shadow>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabItem({
|
||||||
|
tab,
|
||||||
|
isActive,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
tab: TabbarItem;
|
||||||
|
isActive: boolean;
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
const scaleStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={() => {
|
||||||
|
scale.value = withTiming(0.9, { duration: 80 });
|
||||||
|
}}
|
||||||
|
onPressOut={() => {
|
||||||
|
scale.value = withTiming(1, { duration: 120 });
|
||||||
|
}}
|
||||||
|
className="flex-1 items-center justify-center"
|
||||||
|
>
|
||||||
|
<Animated.View style={scaleStyle}>
|
||||||
|
<Icon size={24} color={isActive ? "#5FAF7B" : "#666"} />
|
||||||
|
{tab.title && (
|
||||||
|
<Animated.Text
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
fontSize: 11,
|
||||||
|
marginTop: 4,
|
||||||
|
fontWeight: isActive ? "600" : "400",
|
||||||
|
color: isActive ? "#5FAF7B" : "#666",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</Animated.Text>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CenterButton({ tab, onPress }: { tab: TabbarItem; onPress?: () => void }) {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<Pressable
|
||||||
|
className="absolute"
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={() => {
|
||||||
|
scale.value = withTiming(0.9, { duration: 80 });
|
||||||
|
}}
|
||||||
|
onPressOut={() => {
|
||||||
|
scale.value = withTiming(1, { duration: 120 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Animated.View style={animatedStyle}>
|
||||||
|
{/* <Shadow
|
||||||
|
distance={12}
|
||||||
|
startColor="rgba(140,140,140,0.03)"
|
||||||
|
endColor="rgba(255,255,255,0)"
|
||||||
|
style={styles.centerButtonShadown}
|
||||||
|
> */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "#22C55E",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderWidth: 8,
|
||||||
|
borderColor: "white",
|
||||||
|
top: -12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={30} color="#fff" />
|
||||||
|
</View>
|
||||||
|
{/* </Shadow> */}
|
||||||
|
</Animated.View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
centerButtonShadown: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 999,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(0,0,0,0.03)",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
tabbarShadow: {
|
||||||
|
borderRadius: 999,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: "rgba(255,255,255,0.6)",
|
||||||
|
height: 56,
|
||||||
|
marginBottom: 12,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { LucideIcon } from "lucide-react-native";
|
||||||
|
|
||||||
|
import {
|
||||||
|
StarIcon,
|
||||||
|
HouseIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
UserRoundIcon,
|
||||||
|
MessageCircleIcon,
|
||||||
|
} from "lucide-react-native";
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
isCenter?: boolean;
|
||||||
|
|
||||||
|
children?: NavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主
|
||||||
|
export const MainRoutes: NavItem[] = [
|
||||||
|
{
|
||||||
|
id: "/",
|
||||||
|
title: "首页",
|
||||||
|
icon: HouseIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "/star",
|
||||||
|
title: "关注",
|
||||||
|
icon: StarIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "/app",
|
||||||
|
title: "",
|
||||||
|
icon: LayoutGridIcon,
|
||||||
|
isCenter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "/chat",
|
||||||
|
title: "聊天",
|
||||||
|
icon: MessageCircleIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "/my",
|
||||||
|
title: "我的",
|
||||||
|
icon: UserRoundIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 子
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "react-native";
|
||||||
|
import { MainRoutes } from "@/configs/pages";
|
||||||
|
|
||||||
|
export default function AppLayout() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text className="text-base text-black">Hello RN</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import Tabbar, { TabbarItem } from "@/components/layout/tabbar";
|
||||||
|
import Navbar from "@/components/layout/navbar";
|
||||||
|
import { Slot } from "expo-router";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
interface ModuleLayoutProps {
|
||||||
|
tabs: TabbarItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModuleLayout({ tabs }: ModuleLayoutProps) {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<View
|
||||||
|
pointerEvents="box-none"
|
||||||
|
className="absolute top-0 left-0 right-0 z-10 bg-white"
|
||||||
|
style={{ paddingTop: insets.top }}
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
className="flex-1 z-[1]"
|
||||||
|
style={{
|
||||||
|
paddingTop: insets.top + 48 + 4,
|
||||||
|
paddingBottom: insets.bottom + 12 + 16 + 56,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Slot />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
className="absolute left-0 right-0 bottom-0 z-10"
|
||||||
|
style={{ paddingBottom: insets.bottom }}
|
||||||
|
>
|
||||||
|
<Tabbar tabs={tabs} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function cn(...args: (string | undefined | false | null)[]) {
|
||||||
|
return args.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user