diff --git a/code/app/app-rn4/.vscode/rn.code-snippets b/code/app/app-rn4/.vscode/rn.code-snippets index d0b4a521..218290fa 100644 --- a/code/app/app-rn4/.vscode/rn.code-snippets +++ b/code/app/app-rn4/.vscode/rn.code-snippets @@ -1,19 +1,22 @@ { - "Print React Native Component (NativeWind)": { + "Print LayoutView Scroll Example (NativeWind)": { "scope": "typescriptreact,javascriptreact", "prefix": "rn", "body": [ "import React from \"react\";", - "import { View, Text } from \"react-native\";", + "import LayoutView from \"@/components/layout/view\";", + "import { Text } from \"react-native\";", "", "export default function ${1/(.*)/${1:/pascalcase}/}() {", " return (", - " ", - " ${2:Hello RN}", - " ", + " ", + " ", + " ${2:Hello}", + " ", + " ", " );", "}" ], - "description": "React Native NativeWind 函数组件模板" + "description": "LayoutView + ScrollView 结构组件模板" } } \ No newline at end of file diff --git a/code/app/app-rn4/iconfont.json b/code/app/app-rn4/iconfont.json index 70052cb5..c49cfbc8 100644 --- a/code/app/app-rn4/iconfont.json +++ b/code/app/app-rn4/iconfont.json @@ -1,5 +1,5 @@ { - "symbol_url": "//at.alicdn.com/t/c/font_5161133_ylxxigmqv3.js", + "symbol_url": "//at.alicdn.com/t/c/font_5161133_jurut6g45n9.js", "use_typescript": true, "save_dir": "./src/components/iconfont", "trim_icon_prefix": "icon", diff --git a/code/app/app-rn4/src/app/(tabs)/_layout.tsx b/code/app/app-rn4/src/app/(tabs)/_layout.tsx index 288bbb48..c933e9fd 100644 --- a/code/app/app-rn4/src/app/(tabs)/_layout.tsx +++ b/code/app/app-rn4/src/app/(tabs)/_layout.tsx @@ -5,8 +5,8 @@ import { HomeIcon, MessageCircleIcon, UserRoundIcon, - StarIcon, LayoutGridIcon, + EarthIcon, } from "lucide-react-native"; export const unstable_settings = { @@ -31,9 +31,9 @@ const tabs: TabbarItem[] = [ isCenter: true, }, { - id: "/star", - title: "收藏", - icon: StarIcon, + id: "/space", + title: "空间", + icon: EarthIcon, }, { id: "/my", diff --git a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx index 1c12e8bb..a3cd2db3 100644 --- a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx @@ -1,46 +1,36 @@ -import { ScrollShadow, Surface } from "heroui-native"; -import { LinearGradient } from "expo-linear-gradient"; -import { FlatList, ScrollView, Text, View } from "react-native"; +import { Button, Dialog } from "heroui-native"; +import { View } from "react-native"; +import { useState } from "react"; -export default function ScrollShadowExample() { - const horizontalData = Array.from({ length: 10 }, (_, i) => ({ - id: i, - title: `Card ${i + 1}`, - })); +export default function DialogExample() { + const [isOpen, setIsOpen] = useState(false); return ( - - Horizontal List - - ( - - {item.title} - - )} - showsHorizontalScrollIndicator={false} - contentContainerClassName="p-5 gap-4" - /> - + <> + - Vertical Content - - - Long Content - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris. - - - Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque - laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi - architecto beatae vitae. - - - - + + + + + + + Confirm Action + + Are you sure you want to proceed with this action? This cannot be undone. + + + + + + + + + + ); } diff --git a/code/app/app-rn4/src/app/(tabs)/home/index.tsx b/code/app/app-rn4/src/app/(tabs)/home/index.tsx index 058718ab..a8935bf6 100644 --- a/code/app/app-rn4/src/app/(tabs)/home/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/home/index.tsx @@ -1,49 +1,25 @@ -import { ScrollShadow } from "heroui-native"; -import { LinearGradient } from "expo-linear-gradient"; -import { ScrollView, Text, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import React from "react"; import LayoutView from "@/components/layout/view"; +import EmptyState from "@/components/EmptyStatus"; +export default function Home() { + // const [dailyData, setDailyData] = useState(null); -export default function ScrollShadowExample() { - const list = Array.from({ length: 20 }, (_, i) => i.toString()); - - const items = []; - - for (let i = 0; i < list.length; i++) { - items.push( - - {list[i]} - , - ); - } + // useEffect(() => { + // fetchDailyInfo() + // .then((res: any) => { + // if (res?.code === "1") { + // console.log(res); + // setDailyData(res); + // } + // }) + // .catch(() => {}); + // }, []); return ( - - - Long Content - {items} + + + - - {/* - - - Long Content - {items} - - - */} - + ); } diff --git a/code/app/app-rn4/src/app/(tabs)/my/index.tsx b/code/app/app-rn4/src/app/(tabs)/my/index.tsx index 1a137763..79ab74c5 100644 --- a/code/app/app-rn4/src/app/(tabs)/my/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/my/index.tsx @@ -1,8 +1,110 @@ -import { View } from "react-native"; +import React, { useState } from "react"; +import LayoutView from "@/components/layout/view"; +import { Text, View } from "react-native"; +import { Avatar } from "heroui-native"; +import { ChevronRightIcon } from "lucide-react-native"; +import { ListGroup, PressableFeedback, Dialog, Button } from "heroui-native"; +import { Ionicons } from "@expo/vector-icons"; +import { withUniwind } from "uniwind"; +import { useUserStore } from "@/store"; +import { useRouter } from "expo-router"; +const StyledIonicons = withUniwind(Ionicons); export default function My() { + const router = useRouter(); + const userStore = useUserStore(); + + const [openLogout, setOpenLogout] = useState(false); + + function handleLogout() { + setOpenLogout(false); + + requestAnimationFrame(() => { + userStore.clear(); + router.replace("/auth/login"); + }); + } return ( - + + + + + + JD + + + + JD + + + + + + 账户 + + + {}}> + + + + + + 个人信息 + + + + + + + + + + { + setOpenLogout(true); + }} + > + + + + + + 退出登录 + + + + + + + + + + + + + + 提示 + + + + + 确认退出当前登录账号么? + + + + + + + + + + + + ); } diff --git a/code/app/app-rn4/src/app/(tabs)/space/index.tsx b/code/app/app-rn4/src/app/(tabs)/space/index.tsx new file mode 100644 index 00000000..b4b1ccf3 --- /dev/null +++ b/code/app/app-rn4/src/app/(tabs)/space/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import LayoutView from "@/components/layout/view"; +import { Text } from "react-native"; + +export default function Space() { + return ( + + + 我的空间 + + + ); +} diff --git a/code/app/app-rn4/src/app/+not-found.tsx b/code/app/app-rn4/src/app/+not-found.tsx new file mode 100644 index 00000000..c38ab23e --- /dev/null +++ b/code/app/app-rn4/src/app/+not-found.tsx @@ -0,0 +1,31 @@ +import { Stack } from "expo-router"; +import EmptyState from "@/components/EmptyStatus"; +import { Button } from "heroui-native"; +import { useRouter } from "expo-router"; + +export default function NotFound() { + const router = useRouter(); + + return ( + <> + + { + requestAnimationFrame(() => { + router.replace("/home"); + }); + }} + > + 返回首页 + + } + /> + + ); +} diff --git a/code/app/app-rn4/src/app/auth/login.tsx b/code/app/app-rn4/src/app/auth/login.tsx index 6ce7a16c..8cceab6b 100644 --- a/code/app/app-rn4/src/app/auth/login.tsx +++ b/code/app/app-rn4/src/app/auth/login.tsx @@ -31,14 +31,16 @@ export default function Login() { setLoading(true); setTimeout(() => { - userStore.setUserInfo({ - id: 1, - token: "123", - nickname: "Zhang", - avatar: "", - }); + requestAnimationFrame(() => { + userStore.setUserInfo({ + id: 1, + token: "123", + nickname: "Zhang", + avatar: "", + }); - router.replace("/(tabs)/home"); + router.replace("/(tabs)/home"); + }); setLoading(false); }, 2000); diff --git a/code/app/app-rn4/src/app/index.tsx b/code/app/app-rn4/src/app/index.tsx index 83cdb0a3..ec2622e4 100644 --- a/code/app/app-rn4/src/app/index.tsx +++ b/code/app/app-rn4/src/app/index.tsx @@ -10,9 +10,13 @@ export default function Index() { useEffect(() => { const run = async () => { if (!userStore.isLogged()) { - router.replace("/auth/login"); + requestAnimationFrame(() => { + router.replace("/auth/login"); + }); } else { - router.replace("/(tabs)/home"); + requestAnimationFrame(() => { + router.replace("/(tabs)/home"); + }); } await SplashScreen.hideAsync(); diff --git a/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx b/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx index 77d36bc4..4ee6e265 100644 --- a/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx +++ b/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx @@ -1,27 +1,24 @@ -import React from "react"; -import { View, Text, ScrollView } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import React, { useState } from "react"; +import LayoutView from "@/components/layout/view"; +import { Text } from "react-native"; +import EmptyState from "@/components/EmptyStatus"; export default function Home() { - const insets = useSafeAreaInsets(); + const [dataList, setDataList] = useState([]); return ( - - + + sections={dataList} + keyExtractor={(item) => item.id.toString()} + stickySectionHeadersEnabled={true} + showsVerticalScrollIndicator={false} + ListEmptyComponent={() => ( + + )} > - Money Home - - {Array.from({ length: 30 }).map((_, i) => ( - - 这是第 {i + 1} 条内容 - - ))} - - + Hello + + ); } diff --git a/code/app/app-rn4/src/app/tools/index.tsx b/code/app/app-rn4/src/app/tools/index.tsx index 14e2395f..97ab4c97 100644 --- a/code/app/app-rn4/src/app/tools/index.tsx +++ b/code/app/app-rn4/src/app/tools/index.tsx @@ -1,18 +1,132 @@ -import React from "react"; +import LayoutView from "@/components/layout/view"; +import { SearchField, CloseButton, ListGroup, PressableFeedback, Separator } from "heroui-native"; import { View, Text } from "react-native"; -import { Stack } from "expo-router"; +import { Stack, useRouter } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getGroupedApps } from "@/configs/pages"; +import { useState, useMemo, useRef } from "react"; +import AZIndex from "@/components/AZIndex"; +import type { LegendListRef } from "@legendapp/list"; +import { AppListItem } from "@/configs/pages"; +import EmptyState from "@/components/EmptyStatus"; + +export default function Tools() { + const router = useRouter(); + const safeAreaInsets = useSafeAreaInsets(); + const listRef = useRef(null); + + const [keyword, setKeyword] = useState(""); + + const data = useMemo(() => getGroupedApps(keyword), [keyword]); + + const scrollToLetter = (letter: string) => { + const index = data.findIndex((item) => item.type === "header" && item.title === letter); + + if (index === -1) return; + + listRef.current?.scrollToIndex({ + index, + animated: true, + viewOffset: safeAreaInsets.top + 56, + }); + + setTimeout(() => { + listRef.current?.scrollToIndex({ + index, + animated: true, + viewOffset: safeAreaInsets.top + 56, + }); + }, 100); + }; -export default function Layout() { return ( - - + + + + ref={listRef} + data={data} + keyExtractor={(item, i) => String(i)} + estimatedItemSize={120} + recycleItems + initialContainerPoolRatio={20} + contentContainerStyle={{ + paddingTop: safeAreaInsets.top + 60, + paddingBottom: safeAreaInsets.bottom + 100, + paddingRight: 32, + }} + ListEmptyComponent={} + renderItem={({ item }: { item: AppListItem }) => { + if (item.type === "header") { + return {item.title}; + } + + return ( + + + {item.data.map((subItem, index) => { + const Icon = subItem.icon; + + return ( + { + router.navigate(subItem.id as any); + }} + > + + + + + + + {subItem.title} + {subItem.description && ( + + {subItem.description} + + )} + + + + + + {index !== item.data.length - 1 && } + + + + ); + })} + + + ); }} /> - Hello222 RN - + + + + + + + + + + + + + router.back()} + iconProps={{ size: 28 }} + style={{ padding: 24 }} + /> + + ); } diff --git a/code/app/app-rn4/src/components/AZIndex.tsx b/code/app/app-rn4/src/components/AZIndex.tsx new file mode 100644 index 00000000..e96b4d93 --- /dev/null +++ b/code/app/app-rn4/src/components/AZIndex.tsx @@ -0,0 +1,46 @@ +import React, { useState } from "react"; +import { View, Text, Pressable } from "react-native"; + +const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); + +type Props = { + onSelect?: (letter: string) => void; +}; + +export default function AZIndex({ onSelect }: Props) { + const [active, setActive] = useState(null); + + function handlePress(letter: string) { + setActive(letter); + onSelect?.(letter); + } + + return ( + + {LETTERS.map((l) => { + const isActive = active === l; + + return ( + handlePress(l)} + className="py-0.5 px-1 items-center" + style={({ pressed }) => ({ + opacity: pressed ? 0.4 : 1, + })} + > + + {l} + + + ); + })} + + ); +} diff --git a/code/app/app-rn4/src/components/EmptyStatus.tsx b/code/app/app-rn4/src/components/EmptyStatus.tsx new file mode 100644 index 00000000..0fffed62 --- /dev/null +++ b/code/app/app-rn4/src/components/EmptyStatus.tsx @@ -0,0 +1,58 @@ +import { ScrollView, Text, Image } from "react-native"; + +interface EmptyStateProps { + type?: "empty" | "error" | "404" | "network" | "loading"; + title?: string; + description?: string; + footer?: React.ReactNode; +} + +const images = { + empty: require("@/static/images/status/记录空空的.png"), + error: require("@/static/images/status/图片加载失败.png"), + "404": require("@/static/images/status/404黑洞.png"), + network: require("@/static/images/status/网络加载异常.png"), + loading: require("@/static/images/status/正在紧急开发.png"), +}; + +const defaultTitles = { + empty: "记录空空的", + error: "加载失败", + "404": "页面走丢了", + network: "网络异常", + loading: "正在紧急开发", +}; + +export default function EmptyState({ + type = "empty", + title, + description, + footer, +}: EmptyStateProps) { + const finalTitle = title ?? defaultTitles[type]; + + return ( + + {/* image */} + + + {/* title */} + {finalTitle} + + {/* description */} + {description && ( + {description} + )} + + {footer} + + ); +} diff --git a/code/app/app-rn4/src/components/iconfont/IconAppClothes.tsx b/code/app/app-rn4/src/components/iconfont/IconAppClothes.tsx new file mode 100644 index 00000000..c7c30665 --- /dev/null +++ b/code/app/app-rn4/src/components/iconfont/IconAppClothes.tsx @@ -0,0 +1,72 @@ +/* eslint-disable */ +/* tslint:disable */ + +import React from 'react'; +import type { FunctionComponent } from 'react'; +import type { ViewProps } from 'react-native'; +import { Svg, GProps, Path } from 'react-native-svg'; +import { getIconColor } from './helper'; + +interface Props extends Omit, ViewProps { + size?: number; + color?: string | string[]; +} + +const IconAppClothes: FunctionComponent = ({ + size = 18, + color, + ...rest +}) => { + return ( + + + + + + + + + + + + + + + + ); +}; + +export default React.memo(IconAppClothes); \ No newline at end of file diff --git a/code/app/app-rn4/src/components/iconfont/IconAppFinance.tsx b/code/app/app-rn4/src/components/iconfont/IconAppFinance.tsx new file mode 100644 index 00000000..82e64fae --- /dev/null +++ b/code/app/app-rn4/src/components/iconfont/IconAppFinance.tsx @@ -0,0 +1,56 @@ +/* eslint-disable */ +/* tslint:disable */ + +import React from 'react'; +import type { FunctionComponent } from 'react'; +import type { ViewProps } from 'react-native'; +import { Svg, GProps, Path } from 'react-native-svg'; +import { getIconColor } from './helper'; + +interface Props extends Omit, ViewProps { + size?: number; + color?: string | string[]; +} + +const IconAppFinance: FunctionComponent = ({ + size = 18, + color, + ...rest +}) => { + return ( + + + + + + + + + + + + ); +}; + +export default React.memo(IconAppFinance); \ No newline at end of file diff --git a/code/app/app-rn4/src/components/iconfont/index.tsx b/code/app/app-rn4/src/components/iconfont/index.tsx index 96231a8d..452d86c9 100644 --- a/code/app/app-rn4/src/components/iconfont/index.tsx +++ b/code/app/app-rn4/src/components/iconfont/index.tsx @@ -4,12 +4,16 @@ import React, { FunctionComponent } from 'react'; import { ViewProps } from 'react-native'; import { GProps } from 'react-native-svg'; +import IconAppFinance from './IconAppFinance'; +import IconAppClothes from './IconAppClothes'; import IconWeixin from './IconWeixin'; import IconQq from './IconQq'; +export { default as IconAppFinance } from './IconAppFinance'; +export { default as IconAppClothes } from './IconAppClothes'; export { default as IconWeixin } from './IconWeixin'; export { default as IconQq } from './IconQq'; -export type IconNames = 'weixin' | 'QQ'; +export type IconNames = 'app_finance' | 'app_clothes' | 'weixin' | 'QQ'; interface Props extends GProps, ViewProps { name: IconNames; @@ -19,10 +23,14 @@ interface Props extends GProps, ViewProps { let IconFont: FunctionComponent = ({ name, ...rest }) => { switch (name) { + case 'app_finance': + return ; + case 'app_clothes': + return ; case 'weixin': - return ; + return ; case 'QQ': - return ; + return ; } return null; diff --git a/code/app/app-rn4/src/components/layout/index.tsx b/code/app/app-rn4/src/components/layout/index.tsx deleted file mode 100644 index 31ced07f..00000000 --- a/code/app/app-rn4/src/components/layout/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import React, { ReactNode } from "react"; -import { View } from "react-native"; - -import Tabbar from "./tabbar"; -import { TabbarItem } from "./tabbar"; - -import Navbar from "./navbar"; - -interface LayoutProps { - tabs: TabbarItem[]; - children: ReactNode; -} - -export default function Layout({ tabs, children }: LayoutProps) { - const insets = useSafeAreaInsets(); - - return ( - - - {children} - - - - - - - - - ); -} diff --git a/code/app/app-rn4/src/components/layout/navbar.tsx b/code/app/app-rn4/src/components/layout/navbar.tsx index 01640358..d05d6611 100644 --- a/code/app/app-rn4/src/components/layout/navbar.tsx +++ b/code/app/app-rn4/src/components/layout/navbar.tsx @@ -74,8 +74,8 @@ export default function Navbar({ - - + + { diff --git a/code/app/app-rn4/src/components/layout/tabbar.tsx b/code/app/app-rn4/src/components/layout/tabbar.tsx index 0cb40ba6..aeb6fb11 100644 --- a/code/app/app-rn4/src/components/layout/tabbar.tsx +++ b/code/app/app-rn4/src/components/layout/tabbar.tsx @@ -31,8 +31,8 @@ export default function Tabbar({ tabs, value, onChange, className, style }: Tabb @@ -140,7 +140,7 @@ function CenterButton({ tab, onPress }: { tab: TabbarItem; onPress?: () => void > */} {children}; } -/** - * Shadow(只封装一次 ScrollShadow) - */ function Shadow({ children }: { children: React.ReactElement }) { return ( - + {children} ); } -/** - * 统一 padding 计算 - */ -function useContentPadding() { +function useContentPadding(config?: LayoutConfig) { const insets = useSafeAreaInsets(); return { - paddingTop: insets.top + TOP_EXTRA_PADDING, - paddingBottom: insets.bottom + BOTTOM_EXTRA_PADDING, + paddingTop: insets.top + (config?.topExtraPadding ?? 50), + paddingBottom: insets.bottom + (config?.bottomExtraPadding ?? 80), + paddingHorizontal: 16, }; } -/** - * LayoutView - */ -const LayoutView = (props: { children?: React.ReactNode }) => { - return {props.children}; +/* ========================= Root ========================= */ + +type LayoutViewProps = ViewProps & + LayoutConfig & { + className?: string; + style?: StyleProp; + children?: React.ReactNode; + }; + +const LayoutView = ({ className, style, children, ...rest }: LayoutViewProps) => { + return ( + + {children} + + ); }; -/** - * ScrollView - */ -LayoutView.ScrollView = (props: React.ComponentProps) => { - const padding = useContentPadding(); +/* ========================= ScrollView ========================= */ + +LayoutView.ScrollView = forwardRef< + ScrollView, + React.ComponentProps & LayoutConfig +>(({ topExtraPadding, bottomExtraPadding, contentContainerStyle, ...props }, ref) => { + const padding = useContentPadding({ topExtraPadding, bottomExtraPadding }); return ( ); -}; +}); -/** - * SectionList - */ -LayoutView.SectionView = (props: React.ComponentProps>) => { - const padding = useContentPadding(); +/* ========================= SectionList ========================= */ - return ( - - - - - - ); -}; +LayoutView.SectionView = forwardRef( + ( + { + topExtraPadding, + bottomExtraPadding, + contentContainerStyle, + ...props + }: React.ComponentProps> & LayoutConfig, + ref: React.Ref>, + ) => { + const padding = useContentPadding({ topExtraPadding, bottomExtraPadding }); -/** - * FlatList - */ -LayoutView.FastList = (props: React.ComponentProps>) => { - const padding = useContentPadding(); + return ( + + + + + + ); + }, +) as ( + props: React.ComponentProps> & + LayoutConfig & { ref?: React.Ref> }, +) => React.ReactElement; - return ( - - - - - - ); -}; +/* ========================= FlatList ========================= */ -/** - * LegendList - */ -LayoutView.LegendList = (props: React.ComponentProps>) => { - const padding = useContentPadding(); +LayoutView.FlatList = forwardRef( + ( + { + topExtraPadding, + bottomExtraPadding, + contentContainerStyle, + ...props + }: React.ComponentProps> & LayoutConfig, + ref: React.Ref>, + ) => { + const padding = useContentPadding({ topExtraPadding, bottomExtraPadding }); - return ( - - - {...props} contentContainerStyle={[padding, props.contentContainerStyle]} /> - - - ); -}; + return ( + + + + + + ); + }, +) as ( + props: React.ComponentProps> & LayoutConfig & { ref?: React.Ref> }, +) => React.ReactElement; + +/* ========================= LegendList ========================= */ + +LayoutView.LegendList = forwardRef( + ( + { + topExtraPadding, + bottomExtraPadding, + contentContainerStyle, + ...props + }: React.ComponentProps> & LayoutConfig, + ref: React.Ref, + ) => { + const padding = useContentPadding({ topExtraPadding, bottomExtraPadding }); + + return ( + + + + + + ); + }, +) as ( + props: React.ComponentProps> & LayoutConfig & { ref?: React.Ref }, +) => React.ReactElement; + +/* ========================= */ export default LayoutView; diff --git a/code/app/app-rn4/src/configs/pages.ts b/code/app/app-rn4/src/configs/pages.ts index 7d0e48dc..3ffaac38 100644 --- a/code/app/app-rn4/src/configs/pages.ts +++ b/code/app/app-rn4/src/configs/pages.ts @@ -1,50 +1,70 @@ -import { LucideIcon } from "lucide-react-native"; +import { IconAppFinance, IconAppClothes } from "@/components/iconfont"; -import { - StarIcon, - HouseIcon, - LayoutGridIcon, - UserRoundIcon, - MessageCircleIcon, -} from "lucide-react-native"; - -export interface NavItem { +export interface SubAppItem { id: string; title: string; - icon?: LucideIcon; - isCenter?: boolean; - - children?: NavItem[]; + icon: any; + group: string; + description?: string; } -// 主 -export const MainRoutes: NavItem[] = [ +type HeaderItem = { + type: "header"; + title: string; +}; + +type GroupItem = { + type: "group"; + title: string; + data: SubAppItem[]; +}; + +export type AppListItem = HeaderItem | GroupItem; + +export const SUB_APPS: SubAppItem[] = [ { - id: "/", - title: "首页", - icon: HouseIcon, + id: "/money/home", + title: "记账", + icon: IconAppFinance, + group: "J", + description: "记录日常收支,管理你的每一笔钱", }, { - id: "/star", - title: "关注", - icon: StarIcon, - }, - { - id: "/app", - title: "", - icon: LayoutGridIcon, - isCenter: true, - }, - { - id: "/chat", - title: "聊天", - icon: MessageCircleIcon, - }, - { - id: "/my", - title: "我的", - icon: UserRoundIcon, + id: "/clothes/home", + title: "衣服搭配", + icon: IconAppClothes, + group: "Y", + description: "帮你整理每日穿搭灵感", }, ]; -// 子 +export function getGroupedApps(keyword?: string): AppListItem[] { + let list = SUB_APPS; + + if (keyword && keyword.trim()) { + const lower = keyword.toLowerCase(); + list = SUB_APPS.filter((item) => item.title.toLowerCase().includes(lower)); + } + + const map: Record = {}; + + list.forEach((item) => { + if (!map[item.group]) map[item.group] = []; + map[item.group].push(item); + }); + + const sortedKeys = Object.keys(map).sort((a, b) => { + if (a === "#") return 1; + if (b === "#") return -1; + return a.localeCompare(b); + }); + + const data: AppListItem[] = []; + + sortedKeys.forEach((key) => { + data.push({ type: "header", title: key }); + data.push({ type: "group", title: key, data: map[key] }); + }); + + return data; +} diff --git a/code/app/app-rn4/src/layouts/AppLayout.tsx b/code/app/app-rn4/src/layouts/AppLayout.tsx index 9b2c9c2d..503f7c9e 100644 --- a/code/app/app-rn4/src/layouts/AppLayout.tsx +++ b/code/app/app-rn4/src/layouts/AppLayout.tsx @@ -2,7 +2,7 @@ import React from "react"; import { View, BackHandler } from "react-native"; import { Slot, useRouter, usePathname } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useMemo } from "react"; +import { useMemo, useRef } from "react"; import Tabbar, { TabbarItem } from "@/components/layout/tabbar"; import Navbar from "@/components/layout/navbar"; @@ -11,9 +11,22 @@ export default function AppLayout({ tabs }: { tabs: TabbarItem[] }) { const insets = useSafeAreaInsets(); const pathname = usePathname(); + const lastValidTabRef = useRef(undefined); + const activeTabItem = useMemo(() => { - return tabs.find((t) => t.id === pathname); - }, [pathname]); + const found = tabs.find((t) => t.id === pathname); + + if (found?.isCenter) { + return lastValidTabRef.current; + } + + if (found) { + lastValidTabRef.current = found; + return found; + } + + return lastValidTabRef.current; + }, [pathname, tabs]); function handleClose() { BackHandler.exitApp(); diff --git a/code/app/app-rn4/src/layouts/ModuleLayout.tsx b/code/app/app-rn4/src/layouts/ModuleLayout.tsx index 20861196..b332bf3d 100644 --- a/code/app/app-rn4/src/layouts/ModuleLayout.tsx +++ b/code/app/app-rn4/src/layouts/ModuleLayout.tsx @@ -3,16 +3,29 @@ import Tabbar, { TabbarItem } from "@/components/layout/tabbar"; import Navbar from "@/components/layout/navbar"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Slot, useRouter, usePathname } from "expo-router"; -import { useMemo } from "react"; +import { useMemo, useRef } from "react"; export default function ModuleLayout({ tabs }: { tabs: TabbarItem[] }) { const router = useRouter(); const insets = useSafeAreaInsets(); const pathname = usePathname(); + const lastValidTabRef = useRef(undefined); + const activeTabItem = useMemo(() => { - return tabs.find((t) => t.id === pathname); - }, [pathname]); + const found = tabs.find((t) => t.id === pathname); + + if (found?.isCenter) { + return lastValidTabRef.current; + } + + if (found) { + lastValidTabRef.current = found; + return found; + } + + return lastValidTabRef.current; + }, [pathname, tabs]); function handleClose() { router.replace("/(tabs)/home"); @@ -33,11 +46,7 @@ export default function ModuleLayout({ tabs }: { tabs: TabbarItem[] }) { zIndex: 10, }} > - + - { - router.push(id as any); - }} - /> + + router.push(id as any)} /> + ); diff --git a/code/app/app-rn4/src/static/images/Empty.svg b/code/app/app-rn4/src/static/images/Empty.svg deleted file mode 100644 index 90fb7ef7..00000000 --- a/code/app/app-rn4/src/static/images/Empty.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/app/app-rn4/src/static/images/Error.svg b/code/app/app-rn4/src/static/images/Error.svg deleted file mode 100644 index 37e2934f..00000000 --- a/code/app/app-rn4/src/static/images/Error.svg +++ /dev/null @@ -1 +0,0 @@ -server down \ No newline at end of file diff --git a/code/app/app-rn4/src/static/images/NotFound.svg b/code/app/app-rn4/src/static/images/NotFound.svg deleted file mode 100644 index d79b7a22..00000000 --- a/code/app/app-rn4/src/static/images/NotFound.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/app/app-rn4/src/static/images/status/404黑洞.png b/code/app/app-rn4/src/static/images/status/404黑洞.png new file mode 100644 index 00000000..64a63937 Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/404黑洞.png differ diff --git a/code/app/app-rn4/src/static/images/status/图片加载失败.png b/code/app/app-rn4/src/static/images/status/图片加载失败.png new file mode 100644 index 00000000..375088ee Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/图片加载失败.png differ diff --git a/code/app/app-rn4/src/static/images/status/正在紧急开发.png b/code/app/app-rn4/src/static/images/status/正在紧急开发.png new file mode 100644 index 00000000..35f9d447 Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/正在紧急开发.png differ diff --git a/code/app/app-rn4/src/static/images/status/消息空荡荡的.png b/code/app/app-rn4/src/static/images/status/消息空荡荡的.png new file mode 100644 index 00000000..4a0e7233 Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/消息空荡荡的.png differ diff --git a/code/app/app-rn4/src/static/images/status/网络加载异常.png b/code/app/app-rn4/src/static/images/status/网络加载异常.png new file mode 100644 index 00000000..d8c2683f Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/网络加载异常.png differ diff --git a/code/app/app-rn4/src/static/images/status/记录空空的.png b/code/app/app-rn4/src/static/images/status/记录空空的.png new file mode 100644 index 00000000..5601db7d Binary files /dev/null and b/code/app/app-rn4/src/static/images/status/记录空空的.png differ diff --git a/code/app/app-rn4/src/static/images/welcome.png b/code/app/app-rn4/src/static/images/welcome.png deleted file mode 100644 index 51e36f88..00000000 Binary files a/code/app/app-rn4/src/static/images/welcome.png and /dev/null differ diff --git a/code/app/app-rn4/src/static/images/work.png b/code/app/app-rn4/src/static/images/work.png deleted file mode 100644 index 539d6d80..00000000 Binary files a/code/app/app-rn4/src/static/images/work.png and /dev/null differ diff --git a/code/app/app-rn4/src/utils/http.ts b/code/app/app-rn4/src/utils/http.ts new file mode 100644 index 00000000..868ea760 --- /dev/null +++ b/code/app/app-rn4/src/utils/http.ts @@ -0,0 +1,18 @@ +import { axiosRequestAdapter } from "@alova/adapter-axios"; +import { createAlova } from "alova"; + +export const alovaInstance = createAlova({ + requestAdapter: axiosRequestAdapter(), + + responded: { + onSuccess: async (response) => response.data, + onError: (err) => Promise.reject(err), + }, +}); + +// ====================== +// 地区和天气 +// ====================== +export const fetchDailyInfo = () => { + return alovaInstance.Get("https://60s.aa1.cn/60s-api/new/api/weather/"); +};