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.
-
-
-
-
+
+ >
);
}
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 @@
-
\ 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/");
+};