feat: 增加 hook 封装

This commit is contained in:
大森 2025-11-05 15:59:53 +08:00
parent 20a970d783
commit 952d5ded20
12 changed files with 299 additions and 18 deletions

View File

@ -8,6 +8,7 @@ import "@/styles/tailwind.basic.css";
function App({ children }: PropsWithChildren<any>) {
useLaunch(() => {
loadGlobalConfig();
// 自定义header信息
console.log("App launched.");
});

View File

@ -1,22 +1,131 @@
import { View, Text } from "@tarojs/components";
import { headerModel } from "@/model/header.model";
import {
View,
Text,
ScrollView,
CommonEventFunction,
} from "@tarojs/components";
import Taro from "@tarojs/taro";
import { clsx } from "clsx";
import { FC, memo, PropsWithChildren } from "react";
import { useRecoilValue } from "recoil";
import type { loadMoreConfig, refresherConfig } from "./hooks";
const PageLoader = memo(() => {
return (
<View className="h-screen w-screen flex items-center justify-center bg-red-500 text-white text-2xl">
<View className="h-screen w-screen flex items-center justify-center bg-red-500 text-white">
<Text>Loading...</Text>
</View>
);
});
interface LayoutHeaderProps {
className?: string;
hideBack?: boolean;
}
// 自定义导航栏
const LayoutHeader = memo(
({
className,
hideBack = true,
children,
}: PropsWithChildren<LayoutHeaderProps>) => {
const header = useRecoilValue(headerModel);
const capsStyles = {
width: `${header.capsuleWidth}px`,
height: `${header.capsuleHeight}px`,
};
const handlerBack = () => {
Taro.navigateBack();
};
return (
<View className={className}>
<View style={{ height: `${header.statusBar}px` }}></View>
<View
className="flex flex-row items-center"
style={{ height: `${header.customBar}px` }}
>
<View
className="flex items-center"
style={{
...capsStyles,
marginLeft: `${header.capsulePadding}px`,
}}
>
{hideBack ? (
<View
className="h-full aspect-square pl-[5px] flex justify-center items-center"
onClick={handlerBack}
>
<Text></Text>
</View>
) : null}
</View>
<View className="flex-1 flex justify-center items-center truncate w-full">
{children}
</View>
<View
style={{ ...capsStyles, marginRight: `${header.capsulePadding}px` }}
></View>
</View>
</View>
);
},
);
interface LayoutContainerProps {
className?: string;
refresherConfig?: refresherConfig;
refresherEnabled?: boolean;
loadMore?: loadMoreConfig;
onScroll?: CommonEventFunction;
}
// 页面容器
const LayoutContainer = memo(
({
className,
refresherConfig,
loadMore,
onScroll,
children,
}: PropsWithChildren<LayoutContainerProps>) => {
return (
<ScrollView
className={clsx("flex-1 overflow-hidden", className)}
scrollY
{...(refresherConfig ?? {})}
{...(loadMore ?? {})}
scrollWithAnimation
enableFlex
onScroll={onScroll}
>
{children}
</ScrollView>
);
},
);
// 自定义页脚
const LayoutFooter = memo(({ children }: PropsWithChildren) => {
return (
<View className="h-[150px] bg-green-500 text-white flex items-center justify-center">
{children}
</View>
);
});
interface LayoutProps {
loadding: boolean;
}
interface LayoutComponent extends FC<PropsWithChildren<LayoutProps>> {
Header: FC<PropsWithChildren>;
Header: FC<PropsWithChildren<LayoutHeaderProps>>;
Footer: FC<PropsWithChildren>;
Container: FC<PropsWithChildren>;
Container: FC<PropsWithChildren<LayoutContainerProps>>;
}
const LayoutBase = ({
@ -26,13 +135,15 @@ const LayoutBase = ({
if (loadding) {
return <PageLoader></PageLoader>;
}
return (
<View className="h-[200px] text-center leading-[200px] bg-red-500 text-white text-2xl">
{children}
</View>
);
return <View className="h-screen w-screen flex flex-col">{children}</View>;
};
const Layout = LayoutBase as unknown as LayoutComponent;
Layout.Header = LayoutHeader;
Layout.Container = LayoutContainer;
Layout.Footer = LayoutFooter;
export default Layout;

View File

@ -0,0 +1,71 @@
import { CommonEventFunction } from "@tarojs/components";
import { useCallback, useState } from "react";
export interface refresherConfig {
refresherEnabled: boolean;
refresherThreshold?: number;
refresherDefaultStyle?: "black" | "white" | "none" | string;
refresherBackground?: string;
onRefresherPulling?: CommonEventFunction;
onRefresherRefresh?: CommonEventFunction;
onRefresherRestore?: CommonEventFunction;
onRefresherAbort?: CommonEventFunction;
}
// 下拉刷新
export const useRefresherConfig = ({
onRefresherRefresh,
...safeConfig
}: refresherConfig) => {
const [refresherTriggered, setRefresherTriggered] = useState(false);
const handleRefresh = useCallback(
async (event) => {
setRefresherTriggered(true);
onRefresherRefresh?.(event);
await new Promise((resolve) => setTimeout(resolve, 400));
setRefresherTriggered(false);
},
[onRefresherRefresh, setRefresherTriggered],
);
return {
refresherThreshold: 45,
refresherDefaultStyle: "none",
refresherBackground: "transparent",
refresherTriggered: refresherTriggered,
onRefresherRefresh: handleRefresh,
...safeConfig,
};
};
export interface loadMoreConfig {
onScrollToLower: CommonEventFunction;
threshold?: number; // 距离底部触发加载
hasMore?: boolean; // 是否还有更多
}
// 上拉加载更多
export const useLoadMoreConfig = ({
onScrollToLower,
threshold = 50,
hasMore = true,
}: loadMoreConfig) => {
const [isLoading, setIsLoading] = useState(false);
const handleScrollToLower: CommonEventFunction = useCallback(
async (event) => {
if (isLoading || !hasMore) return;
setIsLoading(true);
onScrollToLower(event);
setIsLoading(false);
},
[isLoading, hasMore, onScrollToLower],
);
return {
lowerThreshold: threshold,
hasMore: hasMore,
isLoading: isLoading,
onScrollToLower: handleScrollToLower,
};
};

View File

@ -1,2 +1,2 @@
export * from './Layout';
export * from "./Layout";
export * from "./hooks";

View File

53
app/model/header.model.ts Normal file
View File

@ -0,0 +1,53 @@
import Taro from "@tarojs/taro";
import { atom, selector } from "recoil";
export interface HeaderModel {
safeAreaBottom: number;
statusBar: number;
customBar: number;
capsulePadding: number;
capsuleWidth: number;
capsuleHeight: number;
capsuleBottom: number;
}
const handleSelector = selector<HeaderModel>({
key: "handleSelector",
get: async () => {
try {
const result = await Taro.getSystemInfo();
let windowsWidth = result.windowWidth;
const rect = Taro.getMenuButtonBoundingClientRect();
let customBar =
(rect.top - (result.statusBarHeight as number)) * 2 + rect.height;
let safeAreaBottom =
result.screenHeight != result.safeArea?.bottom
? result.screenHeight - (result.safeArea?.bottom as number)
: 0;
return {
safeAreaBottom: safeAreaBottom,
statusBar: result.statusBarHeight ?? 0,
customBar: customBar,
capsulePadding: windowsWidth - rect.right,
capsuleWidth: rect.width,
capsuleHeight: rect.height,
capsuleBottom: customBar - rect.bottom,
};
} catch (error) {
return {
safeAreaBottom: 0,
statusBar: 0,
customBar: 0,
capsulePadding: 0,
capsuleWidth: 0,
capsuleHeight: 0,
capsuleBottom: 0,
};
}
},
});
export const headerModel = atom<HeaderModel>({
key: "headerModel",
default: handleSelector,
});

View File

@ -1,3 +1,4 @@
import Taro from "@tarojs/taro";
import { useState } from "react";
// 页面请求hooks
@ -9,9 +10,13 @@ export const usePageContent = () => {
try {
console.log("Page content loaded:", code);
setPageContent("请求结果");
await Taro.setNavigationBarColor({
frontColor: "#000000",
backgroundColor: "#000000",
});
setTimeout(() => {
setPageLoading(false);
}, 5000);
}, 400);
} catch (error) {
console.error("Error loading page content:", error);
setPageLoading(false);

View File

@ -1,4 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '首页',
navigationBarTitleText: "首页",
disableScroll: true,
})
navigationBarTextStyle: "white",
});

View File

@ -1,11 +1,23 @@
import Layout from "@/components/layout/Layout";
import { View, Text } from "@tarojs/components";
import { useDidShow } from "@tarojs/taro";
import { useRefresherConfig } from "@/components/layout";
import { usePageContent } from "./hooks";
export default function Index() {
const { pageLoading, pageContent, loadPageContent } = usePageContent();
// 下拉刷新
const refresherConfig = useRefresherConfig({
refresherEnabled: true,
refresherBackground: "#000",
refresherDefaultStyle: "white",
refresherThreshold: 50,
onRefresherRefresh: (event) => {
console.log("触发下拉刷新", event);
},
});
useDidShow(async () => {
await loadPageContent("测试身份编码");
console.log("Page shown.");
@ -13,9 +25,23 @@ export default function Index() {
return (
<Layout loadding={pageLoading}>
<View className="h-[200px] text-center leading-[200px] bg-red-500 text-white text-2xl">
<Text className="">{pageContent}</Text>
</View>
<Layout.Header hideBack={false}></Layout.Header>
<Layout.Container
className="bg-red-400"
refresherConfig={refresherConfig}
>
<View className="space-y-4">
{Array.from({ length: 10 }).map((_, index) => (
<View
key={index}
className="h-[200px] text-center leading-[200px] bg-red-500 text-white text-2xl"
>
<Text className="">{pageContent}</Text>
</View>
))}
</View>
</Layout.Container>
<Layout.Footer></Layout.Footer>
</Layout>
);
}

10
package-lock.json generated
View File

@ -19,6 +19,7 @@
"@tarojs/runtime": "4.1.7",
"@tarojs/shared": "4.1.7",
"@tarojs/taro": "4.1.7",
"clsx": "^2.1.1",
"mockjs": "^1.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
@ -9669,6 +9670,15 @@
"mimic-response": "^1.0.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/code-block-writer": {
"version": "13.0.3",
"resolved": "https://registry.npmmirror.com/code-block-writer/-/code-block-writer-13.0.3.tgz",

View File

@ -32,6 +32,7 @@
"@tarojs/runtime": "4.1.7",
"@tarojs/shared": "4.1.7",
"@tarojs/taro": "4.1.7",
"clsx": "^2.1.1",
"mockjs": "^1.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",

View File

@ -6,7 +6,9 @@
"setting": {
"urlCheck": false,
"bigPackageSizeSupport": true,
"preloadBackgroundData": false
"preloadBackgroundData": false,
"enhance": true,
"es6": true
},
"compileType": "miniprogram"
}