import ActivityHeatMap from '@/components/ActivityHeatMap'; import { BadgeShowcaseModal } from '@/components/badges/BadgeShowcaseModal'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { ROUTES } from '@/constants/Routes'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import type { BadgeDto } from '@/services/badges'; import { reportBadgeShowcaseDisplayed } from '@/services/badges'; import { fetchAvailableBadges, selectBadgeCounts, selectBadgePreview, selectSortedBadges } from '@/store/badgesSlice'; import { selectActiveMembershipPlanName } from '@/store/membershipSlice'; import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice'; import { getItem, setItem } from '@/utils/kvStore'; import { log } from '@/utils/logger'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Linking, Modal, RefreshControl, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { AppLanguage, changeAppLanguage, getNormalizedLanguage } from '@/i18n'; const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg'; type MenuItem = { icon: React.ComponentProps['name']; title: string; onPress?: () => void; type?: 'switch'; switchValue?: boolean; onSwitchChange?: (value: boolean) => void; isDanger?: boolean; rightText?: string; }; type MenuSectionConfig = { title: string; items: MenuItem[]; }; type LanguageOption = { code: AppLanguage; label: string; description: string; }; export default function PersonalScreen() { const dispatch = useAppDispatch(); const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); const { openMembershipModal } = useMembershipModal(); const insets = useSafeAreaInsets(); const { t, i18n } = useTranslation(); const router = useRouter(); const isLgAvaliable = isLiquidGlassAvailable(); const [languageModalVisible, setLanguageModalVisible] = useState(false); const [isSwitchingLanguage, setIsSwitchingLanguage] = useState(false); const [refreshing, setRefreshing] = useState(false); const languageOptions = useMemo(() => ([ { code: 'zh' as AppLanguage, label: t('personal.language.options.zh.label'), description: t('personal.language.options.zh.description'), }, { code: 'en' as AppLanguage, label: t('personal.language.options.en.label'), description: t('personal.language.options.en.description'), }, ]), [t]); const activeLanguageCode = getNormalizedLanguage(i18n.language); const activeLanguageLabel = languageOptions.find((option) => option.code === activeLanguageCode)?.label ?? ''; const handleLanguageSelect = useCallback(async (language: AppLanguage) => { setLanguageModalVisible(false); if (language === activeLanguageCode || isSwitchingLanguage) { return; } try { setIsSwitchingLanguage(true); await changeAppLanguage(language); } catch (error) { log.warn('语言切换失败', error); } finally { setIsSwitchingLanguage(false); } }, [activeLanguageCode, isSwitchingLanguage]); // 推送通知设置仅在独立页面管理 // 开发者模式相关状态 const [showDeveloperSection, setShowDeveloperSection] = useState(false); const clickTimestamps = useRef([]); const clickTimeoutRef = useRef(null); const handleMembershipPress = useCallback(async () => { const ok = await ensureLoggedIn(); if (!ok) { return; } openMembershipModal(); }, [ensureLoggedIn, openMembershipModal]); const handleBadgesPress = useCallback(() => { router.push(ROUTES.BADGES); }, [router]); // 计算底部间距 const bottomPadding = useMemo(() => { return getTabBarBottomPadding(60) + (insets?.bottom ?? 0); }, [insets?.bottom]); // 直接使用 Redux 中的用户信息,避免重复状态管理 const userProfile = useAppSelector((state) => state.user.profile); const activeMembershipPlanName = useAppSelector(selectActiveMembershipPlanName); const badgePreview = useAppSelector(selectBadgePreview); const badgeCounts = useAppSelector(selectBadgeCounts); const sortedBadges = useAppSelector(selectSortedBadges); const [showcaseBadge, setShowcaseBadge] = useState(null); const autoShownBadgeCodes = useRef>(new Set()); const openBadgeShowcase = useCallback((badge: BadgeDto) => { autoShownBadgeCodes.current.add(badge.code); setShowcaseBadge(badge); }, []); useEffect(() => { if (showcaseBadge) { return; } const nextBadgeToShow = sortedBadges.find( (badge) => badge.isAwarded && badge.isShow === false && !autoShownBadgeCodes.current.has(badge.code) ); if (nextBadgeToShow) { openBadgeShowcase(nextBadgeToShow); } }, [openBadgeShowcase, showcaseBadge, sortedBadges]); const handleBadgeShowcaseClose = useCallback(async () => { if (!showcaseBadge) { return; } const badgeCode = showcaseBadge.code; setShowcaseBadge(null); try { await reportBadgeShowcaseDisplayed(badgeCode); } catch (error) { log.warn('report-badge-showcase-failed', error); } }, [showcaseBadge]); console.log('badgePreview', badgePreview); // 首次加载时获取用户信息和数据 useEffect(() => { dispatch(fetchMyProfile()); dispatch(fetchActivityHistory()); dispatch(fetchAvailableBadges()); }, [dispatch]); // 页面聚焦时智能刷新(依赖 Redux 的缓存策略) useFocusEffect( useCallback(() => { // 徽章数据由 Redux 的缓存策略控制,只有过期才会重新请求 dispatch(fetchAvailableBadges()); }, [dispatch]) ); // 手动刷新处理 const onRefresh = useCallback(async () => { setRefreshing(true); try { // 并行刷新所有数据 await Promise.all([ dispatch(fetchMyProfile()).unwrap(), dispatch(fetchActivityHistory()).unwrap(), dispatch(fetchAvailableBadges()).unwrap(), ]); } catch (error) { log.warn('刷新数据失败', error); } finally { setRefreshing(false); } }, [dispatch]); // 移除 loadNotificationPreference 函数,因为已移到通知设置页面 // 加载开发者模式状态 const loadDeveloperModeState = async () => { try { const enabled = await getItem('developer_mode_enabled'); if (enabled === 'true') { setShowDeveloperSection(true); } } catch (error) { console.error('加载开发者模式状态失败:', error); } }; // 保存开发者模式状态 const saveDeveloperModeState = async (enabled: boolean) => { try { await setItem('developer_mode_enabled', enabled.toString()); } catch (error) { console.error('保存开发者模式状态失败:', error); } }; // 数据格式化函数 const formatHeight = () => { if (userProfile.height == null) return '--'; return `${parseFloat(userProfile.height).toFixed(1)}cm`; }; const formatWeight = () => { if (userProfile.weight == null) return '--'; return `${parseFloat(userProfile.weight).toFixed(1)}kg`; }; const formatAge = () => { if (!userProfile.birthDate) return '--'; const birthDate = new Date(userProfile.birthDate); const today = new Date(); const age = today.getFullYear() - birthDate.getFullYear(); return `${age}${t('personal.stats.ageSuffix')}`; }; // 显示名称 const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME; const profileActionLabel = isLoggedIn ? t('personal.edit') : t('personal.login'); const aiUsageValue = userProfile.isVip ? t('personal.aiUsageUnlimited') : userProfile.freeUsageCount ?? 0; // 初始化时只加载开发者模式状态 useEffect(() => { loadDeveloperModeState(); }, []); // 处理用户名连续点击 const handleUserNamePress = () => { const now = Date.now(); clickTimestamps.current.push(now); // 清除之前的超时 if (clickTimeoutRef.current) { clearTimeout(clickTimeoutRef.current); } // 只保留最近1秒内的点击 clickTimestamps.current = clickTimestamps.current.filter(timestamp => now - timestamp <= 1000); // 检查是否有3次连续点击 if (clickTimestamps.current.length >= 3) { setShowDeveloperSection(true); saveDeveloperModeState(true); // 持久化保存开发者模式状态 clickTimestamps.current = []; // 清空点击记录 log.info('开发者模式已激活'); } else { // 1秒后清空点击记录 clickTimeoutRef.current = setTimeout(() => { clickTimestamps.current = []; }, 1000); } }; // 移除 handleNotificationToggle 函数,因为已移到通知设置页面 // 用户信息头部 const UserHeader = () => ( {displayName} {userProfile.memberNumber && ( {t('personal.memberNumber', { number: userProfile.memberNumber })} )} {userProfile.freeUsageCount !== undefined && ( {t('personal.aiUsage', { value: aiUsageValue })} )} {isLgAvaliable ? ( pushIfAuthedElseLogin('/profile/edit')}> {profileActionLabel} ) : ( pushIfAuthedElseLogin('/profile/edit')}> {profileActionLabel} )} ); const MembershipBanner = () => ( { void handleMembershipPress(); }} > ); const VipMembershipCard = () => { const fallbackProfile = userProfile as Record; const fallbackExpire = ['membershipExpiration', 'vipExpiredAt', 'vipExpiresAt', 'vipExpireDate'] .map((key) => fallbackProfile[key]) .find((value): value is string => typeof value === 'string' && value.trim().length > 0); const rawExpireDate = userProfile.membershipExpiration ?? fallbackExpire; let formattedExpire = t('personal.membership.validForever'); if (typeof rawExpireDate === 'string' && rawExpireDate.trim().length > 0) { const parsed = dayjs(rawExpireDate); formattedExpire = parsed.isValid() ? parsed.format(t('personal.membership.dateFormat')) : rawExpireDate; } const planName = activeMembershipPlanName?.trim() || userProfile.vipPlanName?.trim() || t('personal.membership.planFallback'); return ( {t('personal.membership.badge')} {planName} {t('personal.membership.expiryLabel')} {formattedExpire} { void handleMembershipPress(); }} > {t('personal.membership.changeButton')} ); }; // 数据统计部分 const StatsSection = () => ( {formatHeight()} {t('personal.stats.height')} {formatWeight()} {t('personal.stats.weight')} {formatAge()} {t('personal.stats.age')} ); const BadgesPreviewSection = () => { const previewBadges = badgePreview.slice(0, 3); const hasBadges = previewBadges.length > 0; const extraCount = Math.max(0, badgeCounts.total - previewBadges.length); return ( {t('personal.badgesPreview.title')} {hasBadges ? ( {previewBadges.map((badge, index) => ( {badge.imageUrl ? ( ) : ( {badge.icon ?? '🏅'} )} {!badge.isAwarded && ( )} ))} {extraCount > 0 && ( +{extraCount} )} ) : ( {t('personal.badgesPreview.empty')} )} ); }; // 菜单项组件 const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => ( {title} {items.map((item, index) => ( {item.title} {item.type === 'switch' ? ( { })} trackColor={{ false: '#E5E5E5', true: '#9370DB' }} thumbColor="#FFFFFF" style={styles.switch} /> ) : ( {item.rightText ? ( {item.rightText} ) : null} )} ))} ); // 菜单项配置 const menuSections: MenuSectionConfig[] = [ { title: t('personal.sections.healthData'), items: [ { icon: 'medkit-outline' as React.ComponentProps['name'], title: t('personal.menu.healthDataPermissions'), onPress: () => router.push(ROUTES.HEALTH_DATA_PERMISSIONS), }, ], }, { title: t('personal.sections.notifications'), items: [ { icon: 'notifications-outline' as React.ComponentProps['name'], title: t('personal.menu.notificationSettings'), onPress: () => pushIfAuthedElseLogin(ROUTES.NOTIFICATION_SETTINGS), }, ], }, { title: t('personal.sections.medicalSources'), items: [ { icon: 'medkit-outline' as React.ComponentProps['name'], title: t('personal.menu.whoSource'), onPress: () => Linking.openURL('https://www.who.int'), }, ], }, { title: t('personal.language.title'), items: [ { icon: 'language-outline' as React.ComponentProps['name'], title: t('personal.language.menuTitle'), onPress: () => setLanguageModalVisible(true), rightText: activeLanguageLabel, }, ], }, // 开发者section(需要连续点击三次用户名激活) ...(showDeveloperSection ? [{ title: t('personal.sections.developer'), items: [ { icon: 'code-slash-outline' as React.ComponentProps['name'], title: t('personal.menu.developerOptions'), onPress: () => pushIfAuthedElseLogin(ROUTES.DEVELOPER), }, { icon: 'settings-outline' as React.ComponentProps['name'], title: t('personal.menu.pushSettings'), onPress: () => pushIfAuthedElseLogin(ROUTES.PUSH_NOTIFICATION_SETTINGS), }, ], }] : []), { title: t('personal.sections.other'), items: [ { icon: 'shield-checkmark-outline' as React.ComponentProps['name'], title: t('personal.menu.privacyPolicy'), onPress: () => Linking.openURL(PRIVACY_POLICY_URL), }, { icon: 'chatbubble-ellipses-outline' as React.ComponentProps['name'], title: t('personal.menu.feedback'), onPress: () => Linking.openURL('mailto:richardwei1995@gmail.com'), }, { icon: 'document-text-outline' as React.ComponentProps['name'], title: t('personal.menu.userAgreement'), onPress: () => Linking.openURL(USER_AGREEMENT_URL), }, ], }, // 只有登录用户才显示账号与安全菜单 ...(isLoggedIn ? [{ title: t('personal.sections.account'), items: [ { icon: 'log-out-outline' as React.ComponentProps['name'], title: t('personal.menu.logout'), onPress: confirmLogout, isDanger: false, }, { icon: 'trash-outline' as React.ComponentProps['name'], title: t('personal.menu.deleteAccount'), onPress: confirmDeleteAccount, isDanger: true, }, ], }] : []), ]; const LanguageSelectorModal = () => ( setLanguageModalVisible(false)} > setLanguageModalVisible(false)}> {t('personal.language.modalTitle')} {t('personal.language.modalSubtitle')} {languageOptions.map((option) => { const isSelected = option.code === activeLanguageCode; return ( handleLanguageSelect(option.code)} disabled={isSwitchingLanguage} > {option.label} {option.description} {isSelected && ( )} ); })} setLanguageModalVisible(false)} activeOpacity={0.8} > {t('personal.language.cancel')} ); return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} } > {userProfile.isVip ? : } {/* */} {t('personal.fishRecord')} {menuSections.map((section, index) => ( ))} ); } const styles = StyleSheet.create({ container: { flex: 1, }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, decorativeCircle1: { position: 'absolute', top: 40, right: 20, width: 60, height: 60, borderRadius: 30, backgroundColor: '#0EA5E9', opacity: 0.1, }, decorativeCircle2: { position: 'absolute', bottom: -15, left: -15, width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', opacity: 0.05, }, scrollView: { flex: 1, }, // 部分容器 sectionContainer: { marginBottom: 20, }, sectionTitle: { fontSize: 16, fontWeight: 'bold', color: '#2C3E50', marginBottom: 10, paddingHorizontal: 4, }, // 卡片容器 cardContainer: { backgroundColor: '#FFFFFF', borderRadius: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2, overflow: 'hidden', }, membershipBannerImage: { width: '100%', height: 180, borderRadius: 16, }, vipCard: { borderRadius: 20, padding: 20, overflow: 'hidden', shadowColor: '#4C3AFF', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.2, shadowRadius: 20, elevation: 6, }, vipCardDecorationLarge: { position: 'absolute', right: -40, top: -30, width: 160, height: 160, borderRadius: 80, backgroundColor: 'rgba(255,255,255,0.12)', }, vipCardDecorationSmall: { position: 'absolute', left: -30, bottom: -30, width: 140, height: 140, borderRadius: 70, backgroundColor: 'rgba(255,255,255,0.08)', }, vipCardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', }, vipCardHeaderLeft: { flex: 1, paddingRight: 16, }, vipBadge: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', backgroundColor: 'rgba(255,255,255,0.18)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 14, }, vipBadgeText: { color: '#FFD361', fontSize: 12, fontWeight: '600', marginLeft: 4, }, vipCardTitle: { color: '#FFFFFF', fontSize: 18, fontWeight: '700', marginTop: 12, }, vipCardSubtitle: { color: 'rgba(255,255,255,0.88)', fontSize: 12, lineHeight: 18, marginTop: 6, }, vipCardIllustration: { width: 72, height: 72, borderRadius: 36, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center', }, vipCardFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, }, vipExpiryInfo: { flex: 1, }, vipExpiryLabel: { color: 'rgba(255,255,255,0.72)', fontSize: 12, marginBottom: 6, }, vipExpiryRow: { flexDirection: 'row', alignItems: 'center', }, vipExpiryValue: { color: '#FFFFFF', fontSize: 15, fontWeight: '600', marginLeft: 6, }, vipChangeButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFFFF', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 22, }, vipChangeButtonText: { color: '#2F1767', fontSize: 14, fontWeight: '600', marginLeft: 6, }, // 用户信息区域 userInfoContainer: { flexDirection: 'row', alignItems: 'center', padding: 16, }, avatarContainer: { marginRight: 12, }, avatar: { width: 60, height: 60, borderRadius: 30, borderWidth: 2, borderColor: '#9370DB', }, userDetails: { flex: 1, }, userName: { fontSize: 18, fontWeight: 'bold', color: '#2C3E50', marginBottom: 4, }, userRole: { fontSize: 14, color: '#9370DB', fontWeight: '500', }, userMemberNumber: { fontSize: 10, color: '#6C757D', marginTop: 4, }, aiUsageContainer: { flexDirection: 'row', alignItems: 'center', marginTop: 4, }, aiUsageText: { fontSize: 10, color: '#9370DB', marginLeft: 2, fontWeight: '500', }, editButton: { backgroundColor: '#9370DB', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 16, }, editButtonGlass: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 16, justifyContent: 'center', alignItems: 'center', }, editButtonText: { color: 'white', fontSize: 14, fontWeight: '600', }, editButtonTextGlass: { color: 'rgba(147, 112, 219, 1)', fontSize: 14, fontWeight: '600', }, // 数据统计 statsContainer: { flexDirection: 'row', justifyContent: 'space-between', padding: 16, }, statItem: { alignItems: 'center', flex: 1, }, statValue: { fontSize: 18, fontWeight: 'bold', color: '#9370DB', marginBottom: 4, }, statLabel: { fontSize: 12, color: '#6C757D', fontWeight: '500', }, badgesRowCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 16, borderRadius: 16, backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.05, shadowRadius: 10, elevation: 2, }, badgesRowTitle: { fontSize: 16, fontWeight: '700', color: '#111827', }, badgesRowContent: { flexDirection: 'row', alignItems: 'center', }, badgesStack: { flexDirection: 'row', alignItems: 'center', }, badgeCompactBubble: { width: 32, height: 32, borderRadius: 22, alignItems: 'center', justifyContent: 'center', marginLeft: 8, overflow: 'hidden', position: 'relative', }, badgeCompactBubbleEarned: { backgroundColor: 'rgba(16, 185, 129, 0.18)', }, badgeCompactBubbleLocked: { backgroundColor: 'rgba(226, 232, 240, 0.9)', }, badgeCompactImage: { width: '100%', height: '100%', }, badgeCompactFallback: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', }, badgeCompactFallbackText: { fontSize: 18, fontWeight: '600', color: '#475467', }, badgeCompactOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(15, 23, 42, 0.45)', alignItems: 'center', justifyContent: 'center', }, badgeExtraBubble: { backgroundColor: 'rgba(124, 58, 237, 0.12)', borderWidth: 1, borderColor: 'rgba(124, 58, 237, 0.35)', }, badgeExtraBubbleInline: { marginLeft: 12, }, badgeExtraText: { fontSize: 14, fontWeight: '700', color: '#5B21B6', }, badgesRowEmpty: { fontSize: 13, color: '#6B7280', fontWeight: '500', }, // 菜单项 menuItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 14, paddingHorizontal: 16, borderBottomWidth: 1, borderBottomColor: '#F1F3F4', }, menuItemLeft: { flexDirection: 'row', alignItems: 'center', flex: 1, }, menuRight: { flexDirection: 'row', alignItems: 'center', }, menuRightText: { fontSize: 13, color: '#6C757D', marginRight: 6, }, iconContainer: { width: 32, height: 32, borderRadius: 6, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, menuItemText: { fontSize: 15, color: '#2C3E50', fontWeight: '500', }, switch: { transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }], }, fishRecordContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', marginBottom: 10, }, fishRecordText: { fontSize: 16, fontWeight: 'bold', color: '#2C3E50', marginLeft: 4, }, languageModalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'stretch', padding: 24, }, languageModalBackdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.35)', }, languageModalContent: { backgroundColor: '#FFFFFF', borderRadius: 18, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.15, shadowRadius: 12, elevation: 6, }, languageModalTitle: { fontSize: 18, fontWeight: 'bold', color: '#2C3E50', }, languageModalSubtitle: { fontSize: 13, color: '#6C757D', marginBottom: 4, }, languageOption: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, paddingHorizontal: 4, }, languageOptionSelected: { backgroundColor: 'rgba(147, 112, 219, 0.08)', borderRadius: 12, paddingHorizontal: 12, }, languageOptionDisabled: { opacity: 0.5, }, languageOptionTextGroup: { flex: 1, paddingRight: 12, }, languageOptionLabel: { fontSize: 16, fontWeight: '600', color: '#2C3E50', }, languageOptionDescription: { fontSize: 12, color: '#6C757D', marginTop: 4, }, languageModalClose: { marginTop: 4, alignItems: 'center', }, languageModalCloseText: { fontSize: 15, fontWeight: '500', color: '#9370DB', }, });