From 7823fe2768c2a5d3bb7ed47844b70c79173ef8a6 Mon Sep 17 00:00:00 2001 From: oneao Date: Wed, 8 Apr 2026 21:28:52 +0800 Subject: [PATCH] u --- code/app/app-rn3/.vscode/rn.code-snippets | 17 +++ code/app/app-rn3/app/(tabs)/_layout.tsx | 32 +++++ code/app/app-rn3/app/(tabs)/home/index.tsx | 9 ++ code/app/app-rn3/app/(tabs)/user/index.tsx | 9 ++ code/app/app-rn3/app/_layout.tsx | 19 ++- code/app/app-rn3/app/index.tsx | 2 + code/app/app-rn3/components/common/tabbar.tsx | 127 ++++++++++++++++++ code/app/app-rn3/hooks/use-color-scheme.ts | 16 ++- code/app/app-rn3/hooks/use-theme-color.ts | 8 +- code/app/app-rn3/package.json | 4 +- code/app/app-rn3/pnpm-lock.yaml | 61 ++++++--- code/app/app-rn3/store/app.ts | 13 ++ code/app/app-rn3/types/global.d.ts | 5 + 13 files changed, 294 insertions(+), 28 deletions(-) create mode 100644 code/app/app-rn3/.vscode/rn.code-snippets create mode 100644 code/app/app-rn3/app/(tabs)/_layout.tsx create mode 100644 code/app/app-rn3/app/(tabs)/home/index.tsx create mode 100644 code/app/app-rn3/app/(tabs)/user/index.tsx create mode 100644 code/app/app-rn3/components/common/tabbar.tsx create mode 100644 code/app/app-rn3/store/app.ts create mode 100644 code/app/app-rn3/types/global.d.ts diff --git a/code/app/app-rn3/.vscode/rn.code-snippets b/code/app/app-rn3/.vscode/rn.code-snippets new file mode 100644 index 00000000..8207ac48 --- /dev/null +++ b/code/app/app-rn3/.vscode/rn.code-snippets @@ -0,0 +1,17 @@ +{ + "Print RN Component (NativeWind)": { + "prefix": "rn", + "body": [ + "import { View, Text } from 'react-native';", + "", + "export default function ${1:ComponentName}() {", + " return (", + " ", + " ${2:Hello World}", + " ", + " );", + "}", + ], + "description": "React Native NativeWind 基础组件", + }, +} diff --git a/code/app/app-rn3/app/(tabs)/_layout.tsx b/code/app/app-rn3/app/(tabs)/_layout.tsx new file mode 100644 index 00000000..1285340b --- /dev/null +++ b/code/app/app-rn3/app/(tabs)/_layout.tsx @@ -0,0 +1,32 @@ +import { View } from "react-native"; +import CustomTabbar from "@/components/common/tabbar"; +import { TabbarItem } from "@/components/common/tabbar"; +import { LayoutGridIcon, UserCircleIcon } from "lucide-react-native"; +import { Slot } from "expo-router"; +import { Icon } from "lucide-react-native"; + +const tabs: TabbarItem[] = [ + { + href: "/home", + name: "home", + label: "首页", + icon: "BookA", + }, + { + href: "/user", + name: "user", + label: "我的", + icon: "User", + }, +]; + +export default function BaseLayout() { + return ( + + + + + + + ); +} diff --git a/code/app/app-rn3/app/(tabs)/home/index.tsx b/code/app/app-rn3/app/(tabs)/home/index.tsx new file mode 100644 index 00000000..abed12b2 --- /dev/null +++ b/code/app/app-rn3/app/(tabs)/home/index.tsx @@ -0,0 +1,9 @@ +import { View, Text } from "react-native"; + +export default function Home() { + return ( + + Home + + ); +} diff --git a/code/app/app-rn3/app/(tabs)/user/index.tsx b/code/app/app-rn3/app/(tabs)/user/index.tsx new file mode 100644 index 00000000..4896c267 --- /dev/null +++ b/code/app/app-rn3/app/(tabs)/user/index.tsx @@ -0,0 +1,9 @@ +import { View, Text } from "react-native"; + +export default function User() { + return ( + + User + + ); +} diff --git a/code/app/app-rn3/app/_layout.tsx b/code/app/app-rn3/app/_layout.tsx index 358204de..33245d98 100644 --- a/code/app/app-rn3/app/_layout.tsx +++ b/code/app/app-rn3/app/_layout.tsx @@ -1,14 +1,23 @@ -import { useColorScheme } from "@/hooks/use-color-scheme"; import { Stack } from "expo-router"; import { useEffect } from "react"; import "@/styles/global.css"; +import { useIsDark } from "@/hooks/use-color-scheme"; +import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native"; +import { StatusBar } from "expo-status-bar"; export default function RootLayout() { - const colorScheme = useColorScheme(); + const isDark = useIsDark(); useEffect(() => { - console.log(colorScheme); - }, [colorScheme]); + console.log(isDark); + }, [isDark]); - return ; + return ( + + + + + + + ); } diff --git a/code/app/app-rn3/app/index.tsx b/code/app/app-rn3/app/index.tsx index f10296a8..9a4984f9 100644 --- a/code/app/app-rn3/app/index.tsx +++ b/code/app/app-rn3/app/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Button, ButtonText, ButtonSpinner, ButtonIcon } from "@/components/ui/button"; import { Text, View } from "react-native"; +import { Camera } from "lucide-react-native"; export default function App() { return ( @@ -11,6 +12,7 @@ export default function App() { + ); } diff --git a/code/app/app-rn3/components/common/tabbar.tsx b/code/app/app-rn3/components/common/tabbar.tsx new file mode 100644 index 00000000..45d8cce6 --- /dev/null +++ b/code/app/app-rn3/components/common/tabbar.tsx @@ -0,0 +1,127 @@ +import type { Href } from "expo-router"; +import { usePathname, useRouter } from "expo-router"; +import { useEffect, useState } from "react"; +import { Pressable, Text, View } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSequence, + withTiming, +} from "react-native-reanimated"; +import * as icons from "lucide-react-native/icons"; + +export interface TabbarItem { + name: string; + href: Href; + icon: keyof typeof icons; // 👈 传 icon 名字 + label: string; // 👈 label + onPress?: () => void; +} + +interface AppTabbarProps { + data: TabbarItem[]; +} + +function TabButton({ + iconName, + label, + isFocused = false, + onPress, +}: { + iconName: keyof typeof icons; + label: string; + isFocused?: boolean; + onPress?: () => void; +}) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + useEffect(() => { + if (isFocused) { + scale.value = withSequence( + withTiming(0.9, { duration: 120 }), + withTiming(1, { duration: 160 }), + ); + } + }, [isFocused]); + + // 👇 在内部统一处理 icon + const LucideIcon = icons[iconName]; + + return ( + + + + + + {label} + + + + ); +} + +export default function CustomTabbar({ data }: AppTabbarProps) { + const router = useRouter(); + const pathname = usePathname(); + + const [activeTab, setActiveTab] = useState(""); + + // 路由同步 + useEffect(() => { + const match = data.find((item) => item.href === pathname); + if (match) { + setActiveTab(match.name); + } + }, [pathname, data]); + + const handlePressTab = (item: TabbarItem) => { + setActiveTab(item.name); + + router.push(item.href); + + item.onPress?.(); + }; + + return ( + + {data.map((item) => ( + handlePressTab(item)} + /> + ))} + + ); +} diff --git a/code/app/app-rn3/hooks/use-color-scheme.ts b/code/app/app-rn3/hooks/use-color-scheme.ts index 17e3c63e..4176f1e0 100644 --- a/code/app/app-rn3/hooks/use-color-scheme.ts +++ b/code/app/app-rn3/hooks/use-color-scheme.ts @@ -1 +1,15 @@ -export { useColorScheme } from 'react-native'; +import { useColorScheme } from "react-native"; +import { useAppStore } from "@/store/app"; + +function useIsDark() { + const systemScheme = useColorScheme(); // 'light' | 'dark' + const themeScheme = useAppStore((state) => state.themeScheme); + + if (themeScheme === "system") { + return systemScheme === "dark"; + } + + return themeScheme === "dark"; +} + +export { useIsDark }; diff --git a/code/app/app-rn3/hooks/use-theme-color.ts b/code/app/app-rn3/hooks/use-theme-color.ts index 0cbc3a6e..36b012ca 100644 --- a/code/app/app-rn3/hooks/use-theme-color.ts +++ b/code/app/app-rn3/hooks/use-theme-color.ts @@ -3,14 +3,14 @@ * https://docs.expo.dev/guides/color-schemes/ */ -import { Colors } from '@/constants/theme'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { Colors } from "@/constants/theme"; +import { useColorScheme } from "@/hooks/use-color-scheme"; export function useThemeColor( props: { light?: string; dark?: string }, - colorName: keyof typeof Colors.light & keyof typeof Colors.dark + colorName: keyof typeof Colors.light & keyof typeof Colors.dark, ) { - const theme = useColorScheme() ?? 'light'; + const theme = useColorScheme() ?? "light"; const colorFromProps = props[theme]; if (colorFromProps) { diff --git a/code/app/app-rn3/package.json b/code/app/app-rn3/package.json index 1da9f2ae..a8bca62c 100644 --- a/code/app/app-rn3/package.json +++ b/code/app/app-rn3/package.json @@ -30,6 +30,7 @@ "expo-symbols": "~1.0.8", "expo-system-ui": "~6.0.9", "expo-web-browser": "~15.0.10", + "lucide-react-native": "^1.7.0", "nativewind": "^4.2.3", "react": "19.1.0", "react-dom": "19.1.0", @@ -41,7 +42,8 @@ "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1" + "react-native-worklets": "0.5.1", + "zustand": "^5.0.12" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/code/app/app-rn3/pnpm-lock.yaml b/code/app/app-rn3/pnpm-lock.yaml index ca3ada43..64eb6ba9 100644 --- a/code/app/app-rn3/pnpm-lock.yaml +++ b/code/app/app-rn3/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: expo-web-browser: specifier: ~15.0.10 version: 15.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) + lucide-react-native: + specifier: ^1.7.0 + version: 1.7.0(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) nativewind: specifier: ^4.2.3 version: 4.2.3(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.19(yaml@2.8.3)) @@ -101,6 +104,9 @@ importers: react-native-worklets: specifier: 0.5.1 version: 0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + zustand: + specifier: ^5.0.12 + version: 5.0.12(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@types/react': specifier: ~19.1.0 @@ -927,56 +933,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.44.0': resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.44.0': resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.44.0': resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.44.0': resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.44.0': resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.44.0': resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.44.0': resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxfmt/binding-openharmony-arm64@0.44.0': resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==} @@ -2783,7 +2781,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -3068,56 +3066,48 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.27.0: resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.27.0: resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.27.0: resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.27.0: resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==} @@ -3186,6 +3176,13 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react-native@1.7.0: + resolution: {integrity: sha512-wGJY5nosSawh028jg8r1ZKqnGPDIVfIL9xvKOs4wPYFQHeJMHsADYm/lmuFYXMXXatSkHhpsCjeqIRgeFGzf8g==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-native: '*' + react-native-svg: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -4451,6 +4448,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@0no-co/graphql.web@1.2.0': {} @@ -8347,6 +8362,12 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react-native@1.7.0(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + react-native-svg: 15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -9803,3 +9824,9 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zustand@5.0.12(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)): + optionalDependencies: + '@types/react': 19.1.17 + react: 19.1.0 + use-sync-external-store: 1.6.0(react@19.1.0) diff --git a/code/app/app-rn3/store/app.ts b/code/app/app-rn3/store/app.ts new file mode 100644 index 00000000..d0d776c1 --- /dev/null +++ b/code/app/app-rn3/store/app.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +export type ThemeScheme = "light" | "dark" | "system"; + +interface AppState { + themeScheme: ThemeScheme; + setThemeScheme: (theme: ThemeScheme) => void; +} + +export const useAppStore = create((set) => ({ + themeScheme: "system", + setThemeScheme: (theme) => set({ themeScheme: theme }), +})); diff --git a/code/app/app-rn3/types/global.d.ts b/code/app/app-rn3/types/global.d.ts new file mode 100644 index 00000000..8c8238cb --- /dev/null +++ b/code/app/app-rn3/types/global.d.ts @@ -0,0 +1,5 @@ +export {} + +declare global { + type ThemeScheme = 'light' | 'dark' | 'system' +}