Skip to content

Commit

Permalink
添加 useApiRequest hook (#122)
Browse files Browse the repository at this point in the history
使用自定义的 useApiRequest hook 简化 app 中的请求处理逻辑
  • Loading branch information
CubeSugarCheese authored Mar 5, 2025
1 parent 5f6cc56 commit 3ac2b70
Show file tree
Hide file tree
Showing 13 changed files with 462 additions and 649 deletions.
5 changes: 4 additions & 1 deletion app/(guest)/contributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { Stack } from 'expo-router';
import { useColorScheme } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

import { getApiV1CommonContributor } from '@/api/generate';
import ContributorsDOMComponent from '@/components/dom/contributors';
import useApiRequest from '@/hooks/useApiRequest';

// 这个页面更多地作为一种测试用途,用于展示 DOM 组件的使用
// 复杂的页面可以使用 DOM 组件来简化开发流程,毕竟相应的 Native 组件不太好做
// -- Baoshuo <[email protected]>, 2025-02-06

export default function Contributors() {
const { data } = useApiRequest(getApiV1CommonContributor, {});
const colorScheme = useColorScheme();

return (
Expand All @@ -17,7 +20,7 @@ export default function Contributors() {

<SafeAreaView className="h-full w-full" edges={['bottom']}>
{/* 在原生端传入 colorScheme,防止出现闪动 */}
<ContributorsDOMComponent colorScheme={colorScheme} />
{data && <ContributorsDOMComponent data={data} colorScheme={colorScheme} />}
</SafeAreaView>
</>
);
Expand Down
192 changes: 74 additions & 118 deletions app/common/academic-calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Stack } from 'expo-router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { Dimensions, RefreshControl, ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { toast } from 'sonner-native';
Expand All @@ -11,140 +11,96 @@ import { TabFlatList } from '@/components/tab-flatlist';
import { Text } from '@/components/ui/text';

import { ResultEnum } from '@/api/enum';
import { getApiV1TermsInfo, getApiV1TermsList } from '@/api/generate';
import { useSafeResponseSolve } from '@/hooks/useSafeResponseSolve';
import { type AcademicCalendar } from '@/types/calendar';
import { getApiV1JwchTermList, getApiV1TermsInfo } from '@/api/generate';
import useApiRequest from '@/hooks/useApiRequest';

// 处理API错误
const handleApiError = (errorData: any) => {
if (errorData) {
if (errorData.code === ResultEnum.BizErrorCode) {
return;
}
toast.error(errorData.message || '发生未知错误,请稍后再试');
}
};

export default function AcademicCalendarPage() {
const [isRefreshing, setIsRefreshing] = useState(false); // 是否正在刷新
const [termList, setTermList] = useState<string[]>([]); // 学期列表
const [currentTerm, setCurrentTerm] = useState<string>(''); // 当前学期
const [academicCalendarDataMap, setAcademicCalendarMap] = useState<
Record<string, { data: AcademicCalendar[]; lastUpdated?: Date }>
>({});
const screenWidth = Dimensions.get('window').width; // 获取屏幕宽度
interface CourseContentProps {
term: string;
}

const handleErrorRef = useRef(useSafeResponseSolve().handleError);
// 每个学期的内容
const CourseContent: React.FC<CourseContentProps> = ({ term }) => {
const screenWidth = Dimensions.get('window').width; // 获取屏幕宽度
// 获取学期数据
const { data, dataUpdatedAt, isLoading, refetch } = useApiRequest(
getApiV1TermsInfo,
{ term },
{ errorHandler: handleApiError },
);

// 处理API错误
const handleApiError = useCallback(
(error: any) => {
const data = handleErrorRef.current(error);
const termData = data?.events || [];
const lastUpdated = new Date(dataUpdatedAt);

if (data) {
if (data.code === ResultEnum.BizErrorCode) {
return;
}
toast.error(data.message || '发生未知错误,请稍后再试');
return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={isLoading}
// 处理下拉刷新逻辑
onRefresh={refetch}
/>
}
},
[handleErrorRef],
className="grow"
style={{ width: screenWidth }}
>
{/* 渲染考试数据 */}
<SafeAreaView edges={['bottom']}>
{termData.length > 0 ? (
termData.map((item, idx) => (
<View key={idx} className="mx-4">
<LabelEntry leftText={item.name} description={item.start_date + ' - ' + item.end_date} disabled noIcon />
</View>
))
) : (
<Text className="text-center text-text-secondary">{isLoading ? '正在刷新中' : '暂无学期数据'}</Text>
)}
</SafeAreaView>

{/* 显示刷新时间 */}
{lastUpdated && (
<View className="my-4 flex flex-row items-center justify-center">
<Icon name="time-outline" size={16} className="mr-2" />
<Text className="text-sm leading-5 text-text-primary">数据同步时间:{lastUpdated.toLocaleString()}</Text>
</View>
)}
</ScrollView>
);
};

export default function AcademicCalendarPage() {
const [currentTerm, setCurrentTerm] = useState<string>(''); // 当前学期
// 获取学期列表(当前用户)
const fetchTermList = useCallback(async () => {
try {
const result = await getApiV1TermsList();
const terms = result.data.data.terms.map(item => item.term) as string[];
setTermList(terms);
const onSuccess = useCallback(
(terms: string[]) => {
if (!currentTerm && terms.length) {
setCurrentTerm(terms[0]);
}
} catch (error: any) {
handleApiError(error);
}
}, [currentTerm, handleApiError]);

// 刷新当前学期数据
const refreshData = useCallback(async () => {
// 清空当前学期的数据,保留对象结构
setAcademicCalendarMap(prev => ({
...prev,
[currentTerm]: { data: [], lastUpdated: undefined },
}));

try {
const result = await getApiV1TermsInfo({ term: currentTerm });

setAcademicCalendarMap(prev => ({
...prev,
[currentTerm]: {
data: result.data.data.events as AcademicCalendar[],
lastUpdated: new Date(), // 记录刷新时间
},
}));
} catch (error) {
handleApiError(error);
} finally {
setIsRefreshing(false);
}
}, [currentTerm, handleApiError]);

// 加载学期列表
useEffect(() => {
fetchTermList();
}, [fetchTermList]);

// 当 currentTerm 变化或 examDataMap 缺少数据时刷新
useEffect(() => {
if (!isRefreshing && currentTerm && !academicCalendarDataMap[currentTerm]) {
setIsRefreshing(true);
refreshData();
}
}, [isRefreshing, currentTerm, academicCalendarDataMap, refreshData]);

// 处理下拉刷新逻辑
const handleRefresh = useCallback(() => {
setIsRefreshing(true);
refreshData();
}, [refreshData]);

// 渲染每个学期的内容
const renderContent = (term: string) => {
const termData = academicCalendarDataMap[term]?.data || [];
const lastUpdated = academicCalendarDataMap[term]?.lastUpdated;

return (
<ScrollView
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />}
className="grow"
style={{ width: screenWidth }}
>
{/* 渲染考试数据 */}
<SafeAreaView edges={['bottom']}>
{termData.length > 0 ? (
termData.map((item, idx) => (
<View key={idx} className="mx-4">
<LabelEntry
leftText={item.name}
description={item.start_date + ' - ' + item.end_date}
disabled
noIcon
/>
</View>
))
) : (
<Text className="text-center text-text-secondary">{isRefreshing ? '正在刷新中' : '暂无学期数据'}</Text>
)}
</SafeAreaView>

{/* 显示刷新时间 */}
{lastUpdated && (
<View className="my-4 flex flex-row items-center justify-center">
<Icon name="time-outline" size={16} className="mr-2" />
<Text className="text-sm leading-5 text-text-primary">数据同步时间:{lastUpdated.toLocaleString()}</Text>
</View>
)}
</ScrollView>
);
};
},
[currentTerm],
);
const { data: termList } = useApiRequest(getApiV1JwchTermList, {}, { onSuccess, errorHandler: handleApiError });

return (
<>
<Stack.Screen options={{ title: '校历' }} />

<PageContainer>
<TabFlatList data={termList} value={currentTerm} onChange={setCurrentTerm} renderContent={renderContent} />
<TabFlatList
data={termList ?? []}
value={currentTerm}
onChange={setCurrentTerm}
renderContent={term => <CourseContent term={term} />}
/>
</PageContainer>
</>
);
Expand Down
54 changes: 15 additions & 39 deletions app/toolbox/academic/credits.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Stack } from 'expo-router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RefreshControl, ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { toast } from 'sonner-native';
Expand All @@ -11,55 +10,32 @@ import Loading from '@/components/loading';
import PageContainer from '@/components/page-container';
import { Text } from '@/components/ui/text';

import type { JwchAcademicCreditResponse_AcademicCreditData as CreditData } from '@/api/backend';
import { getApiV1JwchAcademicCredit } from '@/api/generate';
import { useSafeResponseSolve } from '@/hooks/useSafeResponseSolve';
import useApiRequest from '@/hooks/useApiRequest';

export default function CreditsPage() {
const [isRefreshing, setIsRefreshing] = useState(true); // 是否正在刷新
const [creditData, setCreditData] = useState<CreditData[] | null>(null); // 学分数据
const [lastUpdated, setLastUpdated] = useState<Date | null>(null); // 数据最后更新时间
const handleErrorRef = useRef(useSafeResponseSolve().handleError); // 错误处理函数

// 获取学分数据
const fetchCreditData = useCallback(async () => {
try {
const response = await getApiV1JwchAcademicCredit();
setCreditData(response.data.data);
setLastUpdated(new Date()); // 更新最后更新时间
} catch (error: any) {
const data = handleErrorRef.current(error);
if (data) toast.error(data.message || '发生未知错误,请稍后再试');
} finally {
setIsRefreshing(false);
}
}, []);

// 初始化时获取学分数据
useEffect(() => {
fetchCreditData();
}, [fetchCreditData]);
const errorHandler = (data: any) => {
if (data) toast.error(data.msg || '发生未知错误,请稍后再试');
};

// 处理下拉刷新逻辑
const handleRefresh = useCallback(() => {
if (!isRefreshing) {
setIsRefreshing(true); // 确保不会重复触发刷新
setCreditData([]); // 清空数据
fetchCreditData();
}
}, [fetchCreditData, isRefreshing]);
export default function CreditsPage() {
const {
data: creditData,
dataUpdatedAt,
isLoading,
refetch,
} = useApiRequest(getApiV1JwchAcademicCredit, {}, { errorHandler });
const lastUpdated = new Date(dataUpdatedAt); // 数据最后更新时间

return (
<>
<Stack.Screen options={{ title: '学分统计' }} />

{isRefreshing ? (
<Stack.Screen options={{ headerTitle: '学分统计' }} />
{isLoading ? (
<Loading />
) : (
<PageContainer className="bg-background">
<ScrollView
className="flex-1 p-4"
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />}
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={refetch} />}
>
{creditData && creditData.length > 0 && (
<>
Expand Down
Loading

0 comments on commit 3ac2b70

Please sign in to comment.