From 3ad0e08d58d53fa7c6634d29eae7085e2abfeff0 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 25 Nov 2025 15:35:30 +0800 Subject: [PATCH] =?UTF-8?q?perf(app):=20=E6=B7=BB=E5=8A=A0=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=8A=B6=E6=80=81=E6=A3=80=E6=9F=A5=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多个页面添加 isLoggedIn 检查,防止未登录时进行不必要的数据获取 - 使用 React.memo 和 useMemo 优化个人页面徽章渲染性能 - 为 badges API 添加节流机制,避免频繁请求 - 优化图片缓存策略和字符串处理 - 移除调试日志并改进推送通知的认证检查 --- app/(tabs)/challenges.tsx | 3 + app/(tabs)/medications.tsx | 10 +- app/(tabs)/personal.tsx | 140 ++++++++++++++++++---------- app/_layout.tsx | 9 +- app/nutrition/records.tsx | 6 +- components/NutritionRadarCard.tsx | 13 ++- services/api.ts | 3 +- services/pushNotificationManager.ts | 9 +- store/badgesSlice.ts | 12 ++- 9 files changed, 138 insertions(+), 67 deletions(-) diff --git a/app/(tabs)/challenges.tsx b/app/(tabs)/challenges.tsx index 6c81f82..456b383 100644 --- a/app/(tabs)/challenges.tsx +++ b/app/(tabs)/challenges.tsx @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { fetchChallenges, @@ -45,6 +46,8 @@ export default function ChallengesScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const insets = useSafeAreaInsets(); + const { isLoggedIn } = useAuthGuard() + const colorTokens = Colors[theme]; const router = useRouter(); const dispatch = useAppDispatch(); diff --git a/app/(tabs)/medications.tsx b/app/(tabs)/medications.tsx index 9ab3b0c..78e46e0 100644 --- a/app/(tabs)/medications.tsx +++ b/app/(tabs)/medications.tsx @@ -48,7 +48,7 @@ export default function MedicationsScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colors: ThemeColors = Colors[theme]; const userProfile = useAppSelector((state) => state.user.profile); - const { ensureLoggedIn } = useAuthGuard(); + const { ensureLoggedIn, isLoggedIn } = useAuthGuard(); const { checkServiceAccess } = useVipService(); const { openMembershipModal } = useMembershipModal(); const [selectedDate, setSelectedDate] = useState(dayjs()); @@ -144,9 +144,11 @@ export default function MedicationsScreen() { // 加载药物和记录数据 useEffect(() => { + if (!isLoggedIn) return; + dispatch(fetchMedications()); dispatch(fetchMedicationRecords({ date: selectedKey })); - }, [dispatch, selectedKey]); + }, [dispatch, selectedKey, isLoggedIn]); useEffect(() => { return () => { @@ -159,6 +161,8 @@ export default function MedicationsScreen() { // 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据 useFocusEffect( useCallback(() => { + if (!isLoggedIn) return; + // 重新安排药品通知并刷新数据 const refreshDataAndRescheduleNotifications = async () => { try { @@ -188,7 +192,7 @@ export default function MedicationsScreen() { }; refreshDataAndRescheduleNotifications(); - }, [dispatch, selectedKey]) + }, [dispatch, selectedKey, isLoggedIn]) ); useEffect(() => { diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index e107b81..255932e 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -59,6 +59,7 @@ export default function PersonalScreen() { const insets = useSafeAreaInsets(); const { t, i18n } = useTranslation(); const router = useRouter(); + const isLgAvaliable = isLiquidGlassAvailable(); const [languageModalVisible, setLanguageModalVisible] = useState(false); @@ -79,7 +80,7 @@ export default function PersonalScreen() { ]), [t]); const activeLanguageCode = getNormalizedLanguage(i18n.language); - const activeLanguageLabel = languageOptions.find((option) => option.code === activeLanguageCode)?.label ?? ''; + const activeLanguageLabel = languageOptions.find((option) => option.code === activeLanguageCode)?.label || ''; const handleLanguageSelect = useCallback(async (language: AppLanguage) => { setLanguageModalVisible(false); @@ -164,22 +165,25 @@ export default function PersonalScreen() { } }, [showcaseBadge]); - console.log('badgePreview', badgePreview); + // 首次加载时获取用户信息和数据 useEffect(() => { + dispatch(fetchAvailableBadges()); + + if (!isLoggedIn) return; + dispatch(fetchMyProfile()); dispatch(fetchActivityHistory()); - dispatch(fetchAvailableBadges()); - }, [dispatch]); + }, [dispatch, isLoggedIn]); // 页面聚焦时智能刷新(依赖 Redux 的缓存策略) useFocusEffect( useCallback(() => { // 徽章数据由 Redux 的缓存策略控制,只有过期才会重新请求 dispatch(fetchAvailableBadges()); - }, [dispatch]) + }, [dispatch, isLoggedIn]) ); // 手动刷新处理 @@ -300,11 +304,11 @@ export default function PersonalScreen() { {displayName} - {userProfile.memberNumber && ( + {userProfile.memberNumber && String(userProfile.memberNumber).trim().length > 0 ? ( {t('personal.memberNumber', { number: userProfile.memberNumber })} - )} + ) : null} {userProfile.freeUsageCount !== undefined && ( @@ -365,8 +369,8 @@ export default function PersonalScreen() { } const planName = - activeMembershipPlanName?.trim() || - userProfile.vipPlanName?.trim() || + (activeMembershipPlanName && activeMembershipPlanName.trim()) || + (userProfile.vipPlanName && userProfile.vipPlanName.trim()) || t('personal.membership.planFallback'); return ( @@ -420,7 +424,7 @@ export default function PersonalScreen() { const StatsSection = () => ( @@ -440,48 +444,34 @@ export default function PersonalScreen() { ); - const BadgesPreviewSection = () => { - const previewBadges = badgePreview.slice(0, 3); - const hasBadges = previewBadges.length > 0; - const extraCount = Math.max(0, badgeCounts.total - previewBadges.length); + // 优化性能:使用 useMemo 缓存计算结果,避免每次渲染都重新计算 + const BadgesPreviewSection = React.memo(() => { + // 使用 useMemo 缓存切片和计算结果,只有当 badgePreview 或 badgeCounts 变化时才重新计算 + const { previewBadges, hasBadges, extraCount } = useMemo(() => { + const previewBadges = badgePreview.slice(0, 3); + const hasBadges = previewBadges.length > 0; + const extraCount = Math.max(0, badgeCounts.total - previewBadges.length); + return { previewBadges, hasBadges, extraCount }; + }, [badgePreview, badgeCounts]); + + // 使用 useMemo 缓存标题文本,避免每次渲染都调用 t() 函数 + const titleText = useMemo(() => t('personal.badgesPreview.title'), [t]); + const emptyText = useMemo(() => t('personal.badgesPreview.empty'), [t]); return ( - {t('personal.badgesPreview.title')} + {titleText} {hasBadges ? ( {previewBadges.map((badge, index) => ( - - {badge.imageUrl ? ( - - ) : ( - - {badge.icon ?? '🏅'} - - )} - {!badge.isAwarded && ( - - - - )} - + badge={badge} + index={index} + totalBadges={previewBadges.length} + /> ))} {extraCount > 0 && ( @@ -491,12 +481,60 @@ export default function PersonalScreen() { )} ) : ( - {t('personal.badgesPreview.empty')} + {emptyText} )} ); - }; + }); + + // 将徽章项提取为独立的 memo 组件,减少重复渲染 + const BadgeCompactItem = React.memo(({ badge, index, totalBadges }: { + badge: BadgeDto; + index: number; + totalBadges: number; + }) => { + // 使用 useMemo 缓存样式计算,避免每次渲染都重新计算 + const badgeStyle = useMemo(() => [ + styles.badgeCompactBubble, + badge.isAwarded ? styles.badgeCompactBubbleEarned : styles.badgeCompactBubbleLocked, + { + marginLeft: index === 0 ? 0 : -12, + zIndex: totalBadges - index, + }, + ], [badge.isAwarded, index, totalBadges]); + + // 使用 useMemo 缓存图标文本,避免每次渲染都重新计算 + const iconText = useMemo(() => + (badge.icon && String(badge.icon).trim()) || '🏅', + [badge.icon] + ); + + return ( + + {badge.imageUrl ? ( + + ) : ( + + + {iconText} + + + )} + {!badge.isAwarded && ( + + + + )} + + ); + }); // 菜单项组件 const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => ( @@ -532,7 +570,7 @@ export default function PersonalScreen() { /> ) : ( - {item.rightText ? ( + {item.rightText && String(item.rightText).trim() ? ( {item.rightText} ) : null} @@ -583,7 +621,7 @@ export default function PersonalScreen() { icon: 'language-outline' as React.ComponentProps['name'], title: t('personal.language.menuTitle'), onPress: () => setLanguageModalVisible(true), - rightText: activeLanguageLabel, + rightText: activeLanguageLabel || '', }, ], }, @@ -682,8 +720,12 @@ export default function PersonalScreen() { disabled={isSwitchingLanguage} > - {option.label} - {option.description} + + {(option.label && String(option.label).trim()) || ''} + + + {(option.description && String(option.description).trim()) || ''} + {isSelected && ( diff --git a/app/_layout.tsx b/app/_layout.tsx index bb8b700..1b91f4c 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -133,9 +133,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { try { logger.info('🚀 开始初始化基础服务(不需要权限)...'); - // 1. 加载用户数据(首屏展示需要) - await dispatch(fetchMyProfile()); - logger.info('✅ 用户数据加载完成'); + if (isLoggedIn) { + // 1. 加载用户数据(首屏展示需要) + await dispatch(fetchMyProfile()); + logger.info('✅ 用户数据加载完成'); + } // 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化) initializeHealthPermissions(); @@ -181,7 +183,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { permissionInitializedRef.current = true; - const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // ==================== 辅助函数 ==================== diff --git a/app/nutrition/records.tsx b/app/nutrition/records.tsx index 4dcc7b6..6c7bda6 100644 --- a/app/nutrition/records.tsx +++ b/app/nutrition/records.tsx @@ -5,6 +5,7 @@ import { NutritionRecordCard } from '@/components/NutritionRecordCard'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { DietRecord } from '@/services/dietRecords'; @@ -43,6 +44,8 @@ export default function NutritionRecordsScreen() { const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); + const { isLoggedIn } = useAuthGuard() + // 日期相关状态 - 使用与统计页面相同的日期逻辑 const days = getMonthDaysZh(); const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); @@ -90,7 +93,8 @@ export default function NutritionRecordsScreen() { // 页面聚焦时自动刷新数据 useFocusEffect( useCallback(() => { - console.log('营养记录页面聚焦,刷新数据...'); + if (!isLoggedIn) return; + if (viewMode === 'daily') { dispatch(fetchDailyNutritionData(currentSelectedDate)); } else { diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 4f676b9..ced9102 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -100,6 +100,8 @@ export function NutritionRadarCard({ const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const [loading, setLoading] = useState(false); + const { isLoggedIn } = useAuthGuard() + const { pushIfAuthedElseLogin } = useAuthGuard(); const dispatch = useAppDispatch(); @@ -121,10 +123,11 @@ export function NutritionRadarCard({ try { setLoading(true); - await Promise.all([ - dispatch(fetchDailyNutritionData(targetDate)).unwrap(), - dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(), - ]); + + if (isLoggedIn) { + await dispatch(fetchDailyNutritionData(targetDate)).unwrap() + } + await dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap() } catch (error) { console.error('NutritionRadarCard: Failed to get nutrition card data:', error); } finally { @@ -133,7 +136,7 @@ export function NutritionRadarCard({ }; loadNutritionCardData(); - }, [selectedDate, dispatch]); + }, [selectedDate, dispatch, isLoggedIn]); const nutritionStats = useMemo(() => { return [ diff --git a/services/api.ts b/services/api.ts index ec171bc..4a6f02c 100644 --- a/services/api.ts +++ b/services/api.ts @@ -118,6 +118,7 @@ async function doFetch(path: string, options: ApiRequestOptions = {}): Promis if (token) { headers['Authorization'] = `Bearer ${token}`; } + const response = await fetch(url, { method: options.method ?? 'GET', @@ -128,8 +129,6 @@ async function doFetch(path: string, options: ApiRequestOptions = {}): Promis const json = await response.json() - console.log('json', json); - if (!response.ok) { // 检查是否为401未授权 if (response.status === 401) { diff --git a/services/pushNotificationManager.ts b/services/pushNotificationManager.ts index 7967bcf..c7e0a8e 100644 --- a/services/pushNotificationManager.ts +++ b/services/pushNotificationManager.ts @@ -1,3 +1,4 @@ +import { getAuthToken } from '@/services/api'; import { logger } from '@/utils/logger'; import Constants from 'expo-constants'; import * as Notifications from 'expo-notifications'; @@ -167,8 +168,12 @@ export class PushNotificationManager { if (!isRegistered || storedToken !== token) { await this.registerDeviceToken(token); } else { - // 令牌已注册且未变化,更新用户ID绑定关系 - await this.updateTokenUserId(token); + // 令牌已注册且未变化 + // 只有在用户已登录的情况下才更新用户ID绑定关系 + const authToken = await getAuthToken(); + if (authToken) { + await this.updateTokenUserId(token); + } } } catch (error) { console.error('检查和注册设备令牌失败:', error); diff --git a/store/badgesSlice.ts b/store/badgesSlice.ts index b8b0043..3be3904 100644 --- a/store/badgesSlice.ts +++ b/store/badgesSlice.ts @@ -2,6 +2,7 @@ import type { BadgeDto, BadgeRarity } from '@/services/badges'; import { getAvailableBadges } from '@/services/badges'; import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; +import { throttle } from 'lodash'; import type { RootState } from './index'; @@ -19,11 +20,20 @@ const initialState: BadgesState = { lastFetched: null, }; +// 创建节流版本的 fetchAvailableBadges 内部函数 +const throttledFetchAvailableBadges = throttle( + async (): Promise => { + return await getAvailableBadges(); + }, + 2000, // 2秒节流 + { leading: true, trailing: false } +); + export const fetchAvailableBadges = createAsyncThunk( 'badges/fetchAvailable', async (_, { rejectWithValue }) => { try { - return await getAvailableBadges(); + return await throttledFetchAvailableBadges(); } catch (error: any) { const message = error?.message ?? '获取勋章列表失败'; return rejectWithValue(message);