This commit is contained in:
oneao committed 2026-04-13 17:29:18 +08:00
1 parent 61e29b5f76
commit 157c84b0b4
15 files changed
+598 -320

No files matched your search

@@ -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>
);
}
+10 -9
View File
@@ -1,22 +1,23 @@
import { Stack } from "expo-router";
import { useIsDark } from "@/hooks/use-color-scheme";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { StatusBar } from "expo-status-bar";
import "@/styles/global.css";
import { useEffect } from "react";
export default function RootLayout() {
const isDark = useIsDark();
useEffect(() => {
console.log(isDark);
}, [isDark]);
return (
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
{/* <StatusBar style={isDark ? "light" : "dark"} /> */}
<Stack screenOptions={{ headerShown: false }} />
<Stack
screenOptions={{
headerTitleAlign: "center",
animation: "slide_from_right",
contentStyle: {
backgroundColor: "#fff",
},
headerShown: false,
}}
/>
</ThemeProvider>
);
}
-118
View File
@@ -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>
);
}
+13 -193
View File
@@ -1,49 +1,22 @@
import { useSafeAreaInsets } from "react-native-safe-area-context";
import React, { ReactNode } from "react";
import { View, Text, Pressable, StyleSheet } from "react-native";
import {
ChevronLeft,
SlidersHorizontal,
CirclePower,
House,
LayoutGrid,
} from "lucide-react-native";
import { Shadow } from "react-native-shadow-2";
import { View } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import Svg, { Path } from "react-native-svg";
type LayoutProps = {
import Tabbar from "./tabbar";
import { TabbarItem } from "./tabbar";
import Navbar from "./navbar";
interface LayoutProps {
tabs: TabbarItem[];
children: ReactNode;
};
}
const AnimatedView = Animated.View;
export default function Layout({ children }: LayoutProps) {
export default function Layout({ tabs, children }: LayoutProps) {
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 (
<View className="flex-1 bg-white">
{/* 内容区域 */}
<View
className="flex-1"
style={{
@@ -54,167 +27,14 @@ export default function Layout({ children }: LayoutProps) {
{children}
</View>
{/* Header */}
<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>
<Navbar title="123" style={{ paddingTop: insets.top }} />
{/* 标题 */}
<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
className="absolute left-0 right-0 bottom-0"
style={{ paddingBottom: insets.bottom + 6 }}
style={{ paddingBottom: insets.bottom + 8 }}
>
<View className="m-3">
<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>
<Tabbar tabs={tabs} />
</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",
},
});
+50
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
export function cn(...args: (string | undefined | false | null)[]) {
return args.filter(Boolean).join(" ");
}