perf(app): 添加登录状态检查并优化性能
- 在多个页面添加 isLoggedIn 检查,防止未登录时进行不必要的数据获取 - 使用 React.memo 和 useMemo 优化个人页面徽章渲染性能 - 为 badges API 添加节流机制,避免频繁请求 - 优化图片缓存策略和字符串处理 - 移除调试日志并改进推送通知的认证检查
This commit is contained in:
@@ -3,6 +3,7 @@ import dayjs from 'dayjs';
|
|||||||
import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard';
|
import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import {
|
import {
|
||||||
fetchChallenges,
|
fetchChallenges,
|
||||||
@@ -45,6 +46,8 @@ export default function ChallengesScreen() {
|
|||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { isLoggedIn } = useAuthGuard()
|
||||||
|
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function MedicationsScreen() {
|
|||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colors: ThemeColors = Colors[theme];
|
const colors: ThemeColors = Colors[theme];
|
||||||
const userProfile = useAppSelector((state) => state.user.profile);
|
const userProfile = useAppSelector((state) => state.user.profile);
|
||||||
const { ensureLoggedIn } = useAuthGuard();
|
const { ensureLoggedIn, isLoggedIn } = useAuthGuard();
|
||||||
const { checkServiceAccess } = useVipService();
|
const { checkServiceAccess } = useVipService();
|
||||||
const { openMembershipModal } = useMembershipModal();
|
const { openMembershipModal } = useMembershipModal();
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
||||||
@@ -144,9 +144,11 @@ export default function MedicationsScreen() {
|
|||||||
|
|
||||||
// 加载药物和记录数据
|
// 加载药物和记录数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
dispatch(fetchMedications());
|
dispatch(fetchMedications());
|
||||||
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
||||||
}, [dispatch, selectedKey]);
|
}, [dispatch, selectedKey, isLoggedIn]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -159,6 +161,8 @@ export default function MedicationsScreen() {
|
|||||||
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
|
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
// 重新安排药品通知并刷新数据
|
// 重新安排药品通知并刷新数据
|
||||||
const refreshDataAndRescheduleNotifications = async () => {
|
const refreshDataAndRescheduleNotifications = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +192,7 @@ export default function MedicationsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
refreshDataAndRescheduleNotifications();
|
refreshDataAndRescheduleNotifications();
|
||||||
}, [dispatch, selectedKey])
|
}, [dispatch, selectedKey, isLoggedIn])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function PersonalScreen() {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
const isLgAvaliable = isLiquidGlassAvailable();
|
const isLgAvaliable = isLiquidGlassAvailable();
|
||||||
const [languageModalVisible, setLanguageModalVisible] = useState(false);
|
const [languageModalVisible, setLanguageModalVisible] = useState(false);
|
||||||
const [isSwitchingLanguage, setIsSwitchingLanguage] = useState(false);
|
const [isSwitchingLanguage, setIsSwitchingLanguage] = useState(false);
|
||||||
@@ -79,7 +80,7 @@ export default function PersonalScreen() {
|
|||||||
]), [t]);
|
]), [t]);
|
||||||
|
|
||||||
const activeLanguageCode = getNormalizedLanguage(i18n.language);
|
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) => {
|
const handleLanguageSelect = useCallback(async (language: AppLanguage) => {
|
||||||
setLanguageModalVisible(false);
|
setLanguageModalVisible(false);
|
||||||
@@ -164,22 +165,25 @@ export default function PersonalScreen() {
|
|||||||
}
|
}
|
||||||
}, [showcaseBadge]);
|
}, [showcaseBadge]);
|
||||||
|
|
||||||
console.log('badgePreview', badgePreview);
|
|
||||||
|
|
||||||
|
|
||||||
// 首次加载时获取用户信息和数据
|
// 首次加载时获取用户信息和数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
dispatch(fetchAvailableBadges());
|
||||||
|
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
dispatch(fetchMyProfile());
|
dispatch(fetchMyProfile());
|
||||||
dispatch(fetchActivityHistory());
|
dispatch(fetchActivityHistory());
|
||||||
dispatch(fetchAvailableBadges());
|
}, [dispatch, isLoggedIn]);
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
// 页面聚焦时智能刷新(依赖 Redux 的缓存策略)
|
// 页面聚焦时智能刷新(依赖 Redux 的缓存策略)
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
// 徽章数据由 Redux 的缓存策略控制,只有过期才会重新请求
|
// 徽章数据由 Redux 的缓存策略控制,只有过期才会重新请求
|
||||||
dispatch(fetchAvailableBadges());
|
dispatch(fetchAvailableBadges());
|
||||||
}, [dispatch])
|
}, [dispatch, isLoggedIn])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 手动刷新处理
|
// 手动刷新处理
|
||||||
@@ -300,11 +304,11 @@ export default function PersonalScreen() {
|
|||||||
<TouchableOpacity onPress={handleUserNamePress} activeOpacity={0.7}>
|
<TouchableOpacity onPress={handleUserNamePress} activeOpacity={0.7}>
|
||||||
<Text style={styles.userName}>{displayName}</Text>
|
<Text style={styles.userName}>{displayName}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{userProfile.memberNumber && (
|
{userProfile.memberNumber && String(userProfile.memberNumber).trim().length > 0 ? (
|
||||||
<Text style={styles.userMemberNumber}>
|
<Text style={styles.userMemberNumber}>
|
||||||
{t('personal.memberNumber', { number: userProfile.memberNumber })}
|
{t('personal.memberNumber', { number: userProfile.memberNumber })}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
) : null}
|
||||||
{userProfile.freeUsageCount !== undefined && (
|
{userProfile.freeUsageCount !== undefined && (
|
||||||
<View style={styles.aiUsageContainer}>
|
<View style={styles.aiUsageContainer}>
|
||||||
<Ionicons name="sparkles-outline" as any size={12} color="#9370DB" />
|
<Ionicons name="sparkles-outline" as any size={12} color="#9370DB" />
|
||||||
@@ -365,8 +369,8 @@ export default function PersonalScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const planName =
|
const planName =
|
||||||
activeMembershipPlanName?.trim() ||
|
(activeMembershipPlanName && activeMembershipPlanName.trim()) ||
|
||||||
userProfile.vipPlanName?.trim() ||
|
(userProfile.vipPlanName && userProfile.vipPlanName.trim()) ||
|
||||||
t('personal.membership.planFallback');
|
t('personal.membership.planFallback');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -420,7 +424,7 @@ export default function PersonalScreen() {
|
|||||||
const StatsSection = () => (
|
const StatsSection = () => (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
<View style={[styles.cardContainer, {
|
<View style={[styles.cardContainer, {
|
||||||
backgroundColor: 'unset'
|
backgroundColor: 'transparent'
|
||||||
}]}>
|
}]}>
|
||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
@@ -440,48 +444,34 @@ export default function PersonalScreen() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const BadgesPreviewSection = () => {
|
// 优化性能:使用 useMemo 缓存计算结果,避免每次渲染都重新计算
|
||||||
|
const BadgesPreviewSection = React.memo(() => {
|
||||||
|
// 使用 useMemo 缓存切片和计算结果,只有当 badgePreview 或 badgeCounts 变化时才重新计算
|
||||||
|
const { previewBadges, hasBadges, extraCount } = useMemo(() => {
|
||||||
const previewBadges = badgePreview.slice(0, 3);
|
const previewBadges = badgePreview.slice(0, 3);
|
||||||
const hasBadges = previewBadges.length > 0;
|
const hasBadges = previewBadges.length > 0;
|
||||||
const extraCount = Math.max(0, badgeCounts.total - previewBadges.length);
|
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 (
|
return (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
<TouchableOpacity style={[styles.cardContainer, styles.badgesRowCard]} onPress={handleBadgesPress} activeOpacity={0.85}>
|
<TouchableOpacity style={[styles.cardContainer, styles.badgesRowCard]} onPress={handleBadgesPress} activeOpacity={0.85}>
|
||||||
<Text style={styles.badgesRowTitle}>{t('personal.badgesPreview.title')}</Text>
|
<Text style={styles.badgesRowTitle}>{titleText}</Text>
|
||||||
{hasBadges ? (
|
{hasBadges ? (
|
||||||
<View style={styles.badgesRowContent}>
|
<View style={styles.badgesRowContent}>
|
||||||
<View style={styles.badgesStack}>
|
<View style={styles.badgesStack}>
|
||||||
{previewBadges.map((badge, index) => (
|
{previewBadges.map((badge, index) => (
|
||||||
<View
|
<BadgeCompactItem
|
||||||
key={badge.code}
|
key={badge.code}
|
||||||
style={[
|
badge={badge}
|
||||||
styles.badgeCompactBubble,
|
index={index}
|
||||||
badge.isAwarded ? styles.badgeCompactBubbleEarned : styles.badgeCompactBubbleLocked,
|
totalBadges={previewBadges.length}
|
||||||
{
|
|
||||||
marginLeft: index === 0 ? 0 : -12,
|
|
||||||
zIndex: previewBadges.length - index,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{badge.imageUrl ? (
|
|
||||||
<Image
|
|
||||||
source={{ uri: badge.imageUrl }}
|
|
||||||
style={styles.badgeCompactImage}
|
|
||||||
contentFit="cover"
|
|
||||||
transition={200}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<View style={styles.badgeCompactFallback}>
|
|
||||||
<Text style={styles.badgeCompactFallbackText}>{badge.icon ?? '🏅'}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{!badge.isAwarded && (
|
|
||||||
<View style={styles.badgeCompactOverlay}>
|
|
||||||
<Ionicons name="lock-closed" as any size={16} color="#FFFFFF" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
{extraCount > 0 && (
|
{extraCount > 0 && (
|
||||||
@@ -491,12 +481,60 @@ export default function PersonalScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.badgesRowEmpty}>{t('personal.badgesPreview.empty')}</Text>
|
<Text style={styles.badgesRowEmpty}>{emptyText}</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// 将徽章项提取为独立的 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 (
|
||||||
|
<View style={badgeStyle}>
|
||||||
|
{badge.imageUrl ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: badge.imageUrl }}
|
||||||
|
style={styles.badgeCompactImage}
|
||||||
|
contentFit="cover"
|
||||||
|
transition={200}
|
||||||
|
cachePolicy="memory-disk"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={styles.badgeCompactFallback}>
|
||||||
|
<Text style={styles.badgeCompactFallbackText}>
|
||||||
|
{iconText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!badge.isAwarded && (
|
||||||
|
<View style={styles.badgeCompactOverlay}>
|
||||||
|
<Ionicons name="lock-closed" as any size={16} color="#FFFFFF" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// 菜单项组件
|
// 菜单项组件
|
||||||
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
||||||
@@ -532,7 +570,7 @@ export default function PersonalScreen() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.menuRight}>
|
<View style={styles.menuRight}>
|
||||||
{item.rightText ? (
|
{item.rightText && String(item.rightText).trim() ? (
|
||||||
<Text style={styles.menuRightText}>{item.rightText}</Text>
|
<Text style={styles.menuRightText}>{item.rightText}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<Ionicons name="chevron-forward" as any size={20} color="#CCCCCC" />
|
<Ionicons name="chevron-forward" as any size={20} color="#CCCCCC" />
|
||||||
@@ -583,7 +621,7 @@ export default function PersonalScreen() {
|
|||||||
icon: 'language-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
icon: 'language-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: t('personal.language.menuTitle'),
|
title: t('personal.language.menuTitle'),
|
||||||
onPress: () => setLanguageModalVisible(true),
|
onPress: () => setLanguageModalVisible(true),
|
||||||
rightText: activeLanguageLabel,
|
rightText: activeLanguageLabel || '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -682,8 +720,12 @@ export default function PersonalScreen() {
|
|||||||
disabled={isSwitchingLanguage}
|
disabled={isSwitchingLanguage}
|
||||||
>
|
>
|
||||||
<View style={styles.languageOptionTextGroup}>
|
<View style={styles.languageOptionTextGroup}>
|
||||||
<Text style={styles.languageOptionLabel}>{option.label}</Text>
|
<Text style={styles.languageOptionLabel}>
|
||||||
<Text style={styles.languageOptionDescription}>{option.description}</Text>
|
{(option.label && String(option.label).trim()) || ''}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.languageOptionDescription}>
|
||||||
|
{(option.description && String(option.description).trim()) || ''}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Ionicons name="checkmark-circle" as any size={20} color="#9370DB" />
|
<Ionicons name="checkmark-circle" as any size={20} color="#9370DB" />
|
||||||
|
|||||||
@@ -133,9 +133,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
logger.info('🚀 开始初始化基础服务(不需要权限)...');
|
logger.info('🚀 开始初始化基础服务(不需要权限)...');
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
// 1. 加载用户数据(首屏展示需要)
|
// 1. 加载用户数据(首屏展示需要)
|
||||||
await dispatch(fetchMyProfile());
|
await dispatch(fetchMyProfile());
|
||||||
logger.info('✅ 用户数据加载完成');
|
logger.info('✅ 用户数据加载完成');
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化)
|
// 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化)
|
||||||
initializeHealthPermissions();
|
initializeHealthPermissions();
|
||||||
@@ -181,7 +183,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
permissionInitializedRef.current = true;
|
permissionInitializedRef.current = true;
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
// ==================== 辅助函数 ====================
|
// ==================== 辅助函数 ====================
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NutritionRecordCard } from '@/components/NutritionRecordCard';
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import { DietRecord } from '@/services/dietRecords';
|
import { DietRecord } from '@/services/dietRecords';
|
||||||
@@ -43,6 +44,8 @@ export default function NutritionRecordsScreen() {
|
|||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { isLoggedIn } = useAuthGuard()
|
||||||
|
|
||||||
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
||||||
const days = getMonthDaysZh();
|
const days = getMonthDaysZh();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||||
@@ -90,7 +93,8 @@ export default function NutritionRecordsScreen() {
|
|||||||
// 页面聚焦时自动刷新数据
|
// 页面聚焦时自动刷新数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
console.log('营养记录页面聚焦,刷新数据...');
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
if (viewMode === 'daily') {
|
if (viewMode === 'daily') {
|
||||||
dispatch(fetchDailyNutritionData(currentSelectedDate));
|
dispatch(fetchDailyNutritionData(currentSelectedDate));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export function NutritionRadarCard({
|
|||||||
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { isLoggedIn } = useAuthGuard()
|
||||||
|
|
||||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -121,10 +123,11 @@ export function NutritionRadarCard({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await Promise.all([
|
|
||||||
dispatch(fetchDailyNutritionData(targetDate)).unwrap(),
|
if (isLoggedIn) {
|
||||||
dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(),
|
await dispatch(fetchDailyNutritionData(targetDate)).unwrap()
|
||||||
]);
|
}
|
||||||
|
await dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('NutritionRadarCard: Failed to get nutrition card data:', error);
|
console.error('NutritionRadarCard: Failed to get nutrition card data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -133,7 +136,7 @@ export function NutritionRadarCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadNutritionCardData();
|
loadNutritionCardData();
|
||||||
}, [selectedDate, dispatch]);
|
}, [selectedDate, dispatch, isLoggedIn]);
|
||||||
|
|
||||||
const nutritionStats = useMemo(() => {
|
const nutritionStats = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ async function doFetch<T>(path: string, options: ApiRequestOptions = {}): Promis
|
|||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: options.method ?? 'GET',
|
method: options.method ?? 'GET',
|
||||||
headers,
|
headers,
|
||||||
@@ -128,8 +129,6 @@ async function doFetch<T>(path: string, options: ApiRequestOptions = {}): Promis
|
|||||||
|
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
console.log('json', json);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// 检查是否为401未授权
|
// 检查是否为401未授权
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getAuthToken } from '@/services/api';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
@@ -167,9 +168,13 @@ export class PushNotificationManager {
|
|||||||
if (!isRegistered || storedToken !== token) {
|
if (!isRegistered || storedToken !== token) {
|
||||||
await this.registerDeviceToken(token);
|
await this.registerDeviceToken(token);
|
||||||
} else {
|
} else {
|
||||||
// 令牌已注册且未变化,更新用户ID绑定关系
|
// 令牌已注册且未变化
|
||||||
|
// 只有在用户已登录的情况下才更新用户ID绑定关系
|
||||||
|
const authToken = await getAuthToken();
|
||||||
|
if (authToken) {
|
||||||
await this.updateTokenUserId(token);
|
await this.updateTokenUserId(token);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查和注册设备令牌失败:', error);
|
console.error('检查和注册设备令牌失败:', error);
|
||||||
this.config.onError?.(error as Error);
|
this.config.onError?.(error as Error);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { BadgeDto, BadgeRarity } from '@/services/badges';
|
|||||||
import { getAvailableBadges } from '@/services/badges';
|
import { getAvailableBadges } from '@/services/badges';
|
||||||
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
import type { RootState } from './index';
|
import type { RootState } from './index';
|
||||||
|
|
||||||
@@ -19,11 +20,20 @@ const initialState: BadgesState = {
|
|||||||
lastFetched: null,
|
lastFetched: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建节流版本的 fetchAvailableBadges 内部函数
|
||||||
|
const throttledFetchAvailableBadges = throttle(
|
||||||
|
async (): Promise<BadgeDto[]> => {
|
||||||
|
return await getAvailableBadges();
|
||||||
|
},
|
||||||
|
2000, // 2秒节流
|
||||||
|
{ leading: true, trailing: false }
|
||||||
|
);
|
||||||
|
|
||||||
export const fetchAvailableBadges = createAsyncThunk<BadgeDto[], void, { rejectValue: string }>(
|
export const fetchAvailableBadges = createAsyncThunk<BadgeDto[], void, { rejectValue: string }>(
|
||||||
'badges/fetchAvailable',
|
'badges/fetchAvailable',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
return await getAvailableBadges();
|
return await throttledFetchAvailableBadges();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.message ?? '获取勋章列表失败';
|
const message = error?.message ?? '获取勋章列表失败';
|
||||||
return rejectWithValue(message);
|
return rejectWithValue(message);
|
||||||
|
|||||||
Reference in New Issue
Block a user