diff --git a/code/app/app-rn4/.vscode/rn.code-snippets b/code/app/app-rn4/.vscode/rn.code-snippets
new file mode 100644
index 00000000..d0b4a521
--- /dev/null
+++ b/code/app/app-rn4/.vscode/rn.code-snippets
@@ -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 (",
+ " ",
+ " ${2:Hello RN}",
+ " ",
+ " );",
+ "}"
+ ],
+ "description": "React Native NativeWind 函数组件模板"
+ }
+}
\ No newline at end of file
diff --git a/code/app/app-rn4/src/app/(tabs)/_layout.tsx b/code/app/app-rn4/src/app/(tabs)/_layout.tsx
new file mode 100644
index 00000000..02621425
--- /dev/null
+++ b/code/app/app-rn4/src/app/(tabs)/_layout.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx
new file mode 100644
index 00000000..37af92fe
--- /dev/null
+++ b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { View, Text } from "react-native";
+
+export default function Chat() {
+ return (
+
+ Chat
+
+ );
+}
diff --git a/code/app/app-rn4/src/app/(tabs)/home/index.tsx b/code/app/app-rn4/src/app/(tabs)/home/index.tsx
new file mode 100644
index 00000000..e889bf14
--- /dev/null
+++ b/code/app/app-rn4/src/app/(tabs)/home/index.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { View, Text } from "react-native";
+
+export default function Home() {
+ return (
+
+ Home
+
+ );
+}
\ No newline at end of file
diff --git a/code/app/app-rn4/src/app/_layout.tsx b/code/app/app-rn4/src/app/_layout.tsx
index 1a39ff24..3127a91b 100644
--- a/code/app/app-rn4/src/app/_layout.tsx
+++ b/code/app/app-rn4/src/app/_layout.tsx
@@ -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 (
- {/* */}
-
-
+
);
}
diff --git a/code/app/app-rn4/src/app/index.tsx b/code/app/app-rn4/src/app/index.tsx
deleted file mode 100644
index 088688e6..00000000
--- a/code/app/app-rn4/src/app/index.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import Layout from "@/components/layout";
-import { View, Text } from "react-native";
-
-export default function Index() {
- return (
-
-
- 123
-
-
- );
-}
-
-// 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 (
-//
-//
-
-// {/* 👇 悬浮 TabBar */}
-//
-//
-// {TABS.map((item, index) => {
-// const isActive = index === active;
-
-// return (
-// setActive(index)}
-// activeOpacity={0.8}
-// >
-//
-
-// {item.label}
-//
-// );
-// })}
-//
-//
-//
-// );
-// }
-
-// 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",
-// },
-// });
diff --git a/code/app/app-rn4/src/app/money/_layout.tsx b/code/app/app-rn4/src/app/money/_layout.tsx
new file mode 100644
index 00000000..4a4cacb6
--- /dev/null
+++ b/code/app/app-rn4/src/app/money/_layout.tsx
@@ -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 ;
+}
diff --git a/code/app/app-rn4/src/app/money/home/index.tsx b/code/app/app-rn4/src/app/money/home/index.tsx
new file mode 100644
index 00000000..bc645931
--- /dev/null
+++ b/code/app/app-rn4/src/app/money/home/index.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { View, Text, ScrollView } from "react-native";
+
+export default function Home() {
+ return (
+
+
+ Money Home
+
+ {Array.from({ length: 30 }).map((_, i) => (
+
+ 这是第 {i + 1} 条内容
+
+ ))}
+
+
+ );
+}
diff --git a/code/app/app-rn4/src/components/layout/index.tsx b/code/app/app-rn4/src/components/layout/index.tsx
index fd1b125a..31ced07f 100644
--- a/code/app/app-rn4/src/components/layout/index.tsx
+++ b/code/app/app-rn4/src/components/layout/index.tsx
@@ -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 (
- {/* 内容区域 */}
- {/* Header */}
-
-
- {/* 返回按钮 */}
- {
- scale.value = withSpring(0.8);
- }}
- onPressOut={() => {
- scale.value = withSpring(1);
- }}
- >
-
-
-
-
-
-
+
- {/* 标题 */}
-
- 记账
-
-
- {/* 右侧按钮组 */}
-
- {
- leftScale.value = withSpring(0.8);
- }}
- onPressOut={() => {
- leftScale.value = withSpring(1);
- }}
- >
-
-
-
-
-
-
-
- {/* 右按钮 */}
- {
- rightScale.value = withSpring(0.8);
- }}
- onPressOut={() => {
- rightScale.value = withSpring(1);
- }}
- >
-
-
-
-
-
-
-
-
- {/* tabbar */}
-
-
-
-
-
- 首页
-
-
-
- 分类
-
-
-
-
-
-
-
-
-
-
- 统计
-
-
-
- 我的
-
-
-
-
+
);
}
-
-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)",
- },
-});
diff --git a/code/app/app-rn4/src/components/layout/navbar.tsx b/code/app/app-rn4/src/components/layout/navbar.tsx
new file mode 100644
index 00000000..bc950e71
--- /dev/null
+++ b/code/app/app-rn4/src/components/layout/navbar.tsx
@@ -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;
+}
+
+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 (
+ //
+
+ {showBack && (
+ {
+ scale.value = withSpring(0.8);
+ }}
+ onPressOut={() => {
+ scale.value = withSpring(1);
+ }}
+ >
+
+
+
+
+
+
+ )}
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ {
+ leftScale.value = withSpring(0.8);
+ }}
+ onPressOut={() => {
+ leftScale.value = withSpring(1);
+ }}
+ >
+
+
+
+
+
+
+
+ {/* 右按钮 */}
+ {
+ rightScale.value = withSpring(0.8);
+ }}
+ onPressOut={() => {
+ rightScale.value = withSpring(1);
+ }}
+ >
+
+
+
+
+
+
+ //
+ );
+}
+
+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)",
+ },
+});
diff --git a/code/app/app-rn4/src/components/layout/tabbar.tsx b/code/app/app-rn4/src/components/layout/tabbar.tsx
new file mode 100644
index 00000000..212f8f11
--- /dev/null
+++ b/code/app/app-rn4/src/components/layout/tabbar.tsx
@@ -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;
+}
+
+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 (
+
+
+
+ {tabs.map((tab) =>
+ tab.isCenter ? (
+ handlePress(tab.id)} />
+ ) : (
+ handlePress(tab.id)}
+ />
+ ),
+ )}
+
+
+
+ );
+}
+
+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 (
+ {
+ scale.value = withTiming(0.9, { duration: 80 });
+ }}
+ onPressOut={() => {
+ scale.value = withTiming(1, { duration: 120 });
+ }}
+ className="flex-1 items-center justify-center"
+ >
+
+
+ {tab.title && (
+
+ {tab.title}
+
+ )}
+
+
+ );
+}
+
+function CenterButton({ tab, onPress }: { tab: TabbarItem; onPress?: () => void }) {
+ const Icon = tab.icon;
+
+ const scale = useSharedValue(1);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }],
+ }));
+
+ return (
+
+ {
+ scale.value = withTiming(0.9, { duration: 80 });
+ }}
+ onPressOut={() => {
+ scale.value = withTiming(1, { duration: 120 });
+ }}
+ >
+
+ {/* */}
+
+
+
+ {/* */}
+
+
+
+ );
+}
+
+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",
+ },
+});
diff --git a/code/app/app-rn4/src/configs/pages.ts b/code/app/app-rn4/src/configs/pages.ts
new file mode 100644
index 00000000..7d0e48dc
--- /dev/null
+++ b/code/app/app-rn4/src/configs/pages.ts
@@ -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,
+ },
+];
+
+// 子
diff --git a/code/app/app-rn4/src/layouts/AppLayout.tsx b/code/app/app-rn4/src/layouts/AppLayout.tsx
new file mode 100644
index 00000000..98882873
--- /dev/null
+++ b/code/app/app-rn4/src/layouts/AppLayout.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { View, Text } from "react-native";
+import { MainRoutes } from "@/configs/pages";
+
+export default function AppLayout() {
+ return (
+
+ Hello RN
+
+ );
+}
diff --git a/code/app/app-rn4/src/layouts/ModuleLayout.tsx b/code/app/app-rn4/src/layouts/ModuleLayout.tsx
new file mode 100644
index 00000000..4ecd83c4
--- /dev/null
+++ b/code/app/app-rn4/src/layouts/ModuleLayout.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/code/app/app-rn4/src/utils/cn.ts b/code/app/app-rn4/src/utils/cn.ts
new file mode 100644
index 00000000..97528ca7
--- /dev/null
+++ b/code/app/app-rn4/src/utils/cn.ts
@@ -0,0 +1,3 @@
+export function cn(...args: (string | undefined | false | null)[]) {
+ return args.filter(Boolean).join(" ");
+}