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(" "); +}