- 在用药模块首次添加时显示医疗免责声明弹窗 - 新增断食参考文献页面,展示权威医学机构来源 - 在个人中心添加WHO医学来源入口 - 使用本地存储记录用户已读免责声明状态 - 支持Liquid Glass毛玻璃效果和降级方案 - 新增中英文国际化翻译支持
996 lines
29 KiB
TypeScript
996 lines
29 KiB
TypeScript
import ActivityHeatMap from '@/components/ActivityHeatMap';
|
||
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 { 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, 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<typeof Ionicons>['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 languageOptions = useMemo<LanguageOption[]>(() => ([
|
||
{
|
||
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<number[]>([]);
|
||
const clickTimeoutRef = useRef<number | null>(null);
|
||
|
||
const handleMembershipPress = useCallback(async () => {
|
||
const ok = await ensureLoggedIn();
|
||
if (!ok) {
|
||
return;
|
||
}
|
||
openMembershipModal();
|
||
}, [ensureLoggedIn, openMembershipModal]);
|
||
|
||
// 计算底部间距
|
||
const bottomPadding = useMemo(() => {
|
||
return getTabBarBottomPadding(60) + (insets?.bottom ?? 0);
|
||
}, [insets?.bottom]);
|
||
|
||
// 直接使用 Redux 中的用户信息,避免重复状态管理
|
||
const userProfile = useAppSelector((state) => state.user.profile);
|
||
const activeMembershipPlanName = useAppSelector(selectActiveMembershipPlanName);
|
||
|
||
|
||
// 页面聚焦时获取最新用户信息
|
||
useFocusEffect(
|
||
React.useCallback(() => {
|
||
dispatch(fetchMyProfile());
|
||
dispatch(fetchActivityHistory());
|
||
// 不再需要在这里加载推送偏好设置,因为已移到通知设置页面
|
||
// 加载开发者模式状态
|
||
loadDeveloperModeState();
|
||
}, [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 = () => (
|
||
<View style={[styles.sectionContainer, {
|
||
marginBottom: 0
|
||
}]}>
|
||
|
||
<View style={[styles.userInfoContainer,]}>
|
||
<View style={styles.avatarContainer}>
|
||
<Image
|
||
source={userProfile.avatar || DEFAULT_AVATAR_URL}
|
||
style={styles.avatar}
|
||
contentFit="cover"
|
||
transition={200}
|
||
cachePolicy="memory-disk"
|
||
/>
|
||
</View>
|
||
<View style={styles.userDetails}>
|
||
<TouchableOpacity onPress={handleUserNamePress} activeOpacity={0.7}>
|
||
<Text style={styles.userName}>{displayName}</Text>
|
||
</TouchableOpacity>
|
||
{userProfile.memberNumber && (
|
||
<Text style={styles.userMemberNumber}>
|
||
{t('personal.memberNumber', { number: userProfile.memberNumber })}
|
||
</Text>
|
||
)}
|
||
{userProfile.freeUsageCount !== undefined && (
|
||
<View style={styles.aiUsageContainer}>
|
||
<Ionicons name="sparkles-outline" as any size={12} color="#9370DB" />
|
||
<Text style={styles.aiUsageText}>
|
||
{t('personal.aiUsage', { value: aiUsageValue })}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
{isLgAvaliable ? (
|
||
<TouchableOpacity onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
||
<GlassView style={styles.editButtonGlass}>
|
||
<Text style={styles.editButtonTextGlass}>{profileActionLabel}</Text>
|
||
</GlassView>
|
||
</TouchableOpacity>
|
||
) : (
|
||
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
||
<Text style={styles.editButtonText}>{profileActionLabel}</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
|
||
</View>
|
||
|
||
</View>
|
||
);
|
||
|
||
const MembershipBanner = () => (
|
||
<View style={styles.sectionContainer}>
|
||
<TouchableOpacity
|
||
activeOpacity={0.9}
|
||
onPress={() => {
|
||
void handleMembershipPress();
|
||
}}
|
||
>
|
||
<Image
|
||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/banner/vip2.png' }}
|
||
style={styles.membershipBannerImage}
|
||
contentFit="cover"
|
||
transition={200}
|
||
cachePolicy="memory-disk"
|
||
/>
|
||
</TouchableOpacity>
|
||
</View>
|
||
);
|
||
|
||
const VipMembershipCard = () => {
|
||
const fallbackProfile = userProfile as Record<string, unknown>;
|
||
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 (
|
||
<View style={styles.sectionContainer}>
|
||
<LinearGradient
|
||
colors={['#5B4CFF', '#8D5BEA']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={styles.vipCard}
|
||
>
|
||
<View style={styles.vipCardDecorationLarge} />
|
||
<View style={styles.vipCardDecorationSmall} />
|
||
<View style={styles.vipCardHeader}>
|
||
<View style={styles.vipCardHeaderLeft}>
|
||
<View style={styles.vipBadge}>
|
||
<Ionicons name="sparkles-outline" as any size={16} color="#FFD361" />
|
||
<Text style={styles.vipBadgeText}>{t('personal.membership.badge')}</Text>
|
||
</View>
|
||
<Text style={styles.vipCardTitle}>{planName}</Text>
|
||
</View>
|
||
<View style={styles.vipCardIllustration}>
|
||
<Ionicons name="ribbon" as any size={32} color="rgba(255,255,255,0.88)" />
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.vipCardFooter}>
|
||
<View style={styles.vipExpiryInfo}>
|
||
<Text style={styles.vipExpiryLabel}>{t('personal.membership.expiryLabel')}</Text>
|
||
<View style={styles.vipExpiryRow}>
|
||
<Ionicons name="time-outline" as any size={16} color="rgba(255,255,255,0.85)" />
|
||
<Text style={styles.vipExpiryValue}>{formattedExpire}</Text>
|
||
</View>
|
||
</View>
|
||
<TouchableOpacity
|
||
style={styles.vipChangeButton}
|
||
activeOpacity={0.85}
|
||
onPress={() => {
|
||
void handleMembershipPress();
|
||
}}
|
||
>
|
||
<Ionicons name="swap-horizontal-outline" as any size={16} color="#2F1767" />
|
||
<Text style={styles.vipChangeButtonText}>{t('personal.membership.changeButton')}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</LinearGradient>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// 数据统计部分
|
||
const StatsSection = () => (
|
||
<View style={styles.sectionContainer}>
|
||
<View style={[styles.cardContainer, {
|
||
backgroundColor: 'unset'
|
||
}]}>
|
||
<View style={styles.statsContainer}>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{formatHeight()}</Text>
|
||
<Text style={styles.statLabel}>{t('personal.stats.height')}</Text>
|
||
</View>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{formatWeight()}</Text>
|
||
<Text style={styles.statLabel}>{t('personal.stats.weight')}</Text>
|
||
</View>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{formatAge()}</Text>
|
||
<Text style={styles.statLabel}>{t('personal.stats.age')}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
// 菜单项组件
|
||
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
||
<View style={styles.sectionContainer}>
|
||
<Text style={styles.sectionTitle}>{title}</Text>
|
||
<View style={styles.cardContainer}>
|
||
{items.map((item, index) => (
|
||
<TouchableOpacity
|
||
key={index}
|
||
style={[styles.menuItem, index === items.length - 1 && { borderBottomWidth: 0 }]}
|
||
onPress={item.type === 'switch' ? undefined : item.onPress}
|
||
disabled={item.type === 'switch'}
|
||
>
|
||
<View style={styles.menuItemLeft}>
|
||
<View style={[
|
||
styles.iconContainer,
|
||
]}>
|
||
<Ionicons
|
||
name={item.icon}
|
||
size={20}
|
||
color={item.isDanger ? '#FF4444' : '#9370DB'}
|
||
/>
|
||
</View>
|
||
<Text style={styles.menuItemText}>{item.title}</Text>
|
||
</View>
|
||
{item.type === 'switch' ? (
|
||
<Switch
|
||
value={item.switchValue || false}
|
||
onValueChange={item.onSwitchChange || (() => { })}
|
||
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||
thumbColor="#FFFFFF"
|
||
style={styles.switch}
|
||
/>
|
||
) : (
|
||
<View style={styles.menuRight}>
|
||
{item.rightText ? (
|
||
<Text style={styles.menuRightText}>{item.rightText}</Text>
|
||
) : null}
|
||
<Ionicons name="chevron-forward" as any size={20} color="#CCCCCC" />
|
||
</View>
|
||
)}
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
// 菜单项配置
|
||
const menuSections: MenuSectionConfig[] = [
|
||
{
|
||
title: t('personal.sections.healthData'),
|
||
items: [
|
||
{
|
||
icon: 'medkit-outline' as React.ComponentProps<typeof Ionicons>['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<typeof Ionicons>['name'],
|
||
title: t('personal.menu.notificationSettings'),
|
||
onPress: () => pushIfAuthedElseLogin(ROUTES.NOTIFICATION_SETTINGS),
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: t('personal.sections.medicalSources'),
|
||
items: [
|
||
{
|
||
icon: 'medkit-outline' as React.ComponentProps<typeof Ionicons>['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<typeof Ionicons>['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<typeof Ionicons>['name'],
|
||
title: t('personal.menu.developerOptions'),
|
||
onPress: () => pushIfAuthedElseLogin(ROUTES.DEVELOPER),
|
||
},
|
||
{
|
||
icon: 'settings-outline' as React.ComponentProps<typeof Ionicons>['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<typeof Ionicons>['name'],
|
||
title: t('personal.menu.privacyPolicy'),
|
||
onPress: () => Linking.openURL(PRIVACY_POLICY_URL),
|
||
},
|
||
{
|
||
icon: 'chatbubble-ellipses-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||
title: t('personal.menu.feedback'),
|
||
onPress: () => Linking.openURL('mailto:richardwei1995@gmail.com'),
|
||
},
|
||
{
|
||
icon: 'document-text-outline' as React.ComponentProps<typeof Ionicons>['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<typeof Ionicons>['name'],
|
||
title: t('personal.menu.logout'),
|
||
onPress: confirmLogout,
|
||
isDanger: false,
|
||
},
|
||
{
|
||
icon: 'trash-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||
title: t('personal.menu.deleteAccount'),
|
||
onPress: confirmDeleteAccount,
|
||
isDanger: true,
|
||
},
|
||
],
|
||
}] : []),
|
||
];
|
||
|
||
const LanguageSelectorModal = () => (
|
||
<Modal
|
||
animationType="fade"
|
||
transparent
|
||
visible={languageModalVisible}
|
||
onRequestClose={() => setLanguageModalVisible(false)}
|
||
>
|
||
<View style={styles.languageModalOverlay}>
|
||
<TouchableWithoutFeedback onPress={() => setLanguageModalVisible(false)}>
|
||
<View style={styles.languageModalBackdrop} />
|
||
</TouchableWithoutFeedback>
|
||
<View style={styles.languageModalContent}>
|
||
<Text style={styles.languageModalTitle}>{t('personal.language.modalTitle')}</Text>
|
||
<Text style={styles.languageModalSubtitle}>{t('personal.language.modalSubtitle')}</Text>
|
||
{languageOptions.map((option) => {
|
||
const isSelected = option.code === activeLanguageCode;
|
||
return (
|
||
<TouchableOpacity
|
||
key={option.code}
|
||
activeOpacity={0.85}
|
||
style={[
|
||
styles.languageOption,
|
||
isSelected && styles.languageOptionSelected,
|
||
isSwitchingLanguage && styles.languageOptionDisabled,
|
||
]}
|
||
onPress={() => handleLanguageSelect(option.code)}
|
||
disabled={isSwitchingLanguage}
|
||
>
|
||
<View style={styles.languageOptionTextGroup}>
|
||
<Text style={styles.languageOptionLabel}>{option.label}</Text>
|
||
<Text style={styles.languageOptionDescription}>{option.description}</Text>
|
||
</View>
|
||
{isSelected && (
|
||
<Ionicons name="checkmark-circle" as any size={20} color="#9370DB" />
|
||
)}
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
<TouchableOpacity
|
||
style={styles.languageModalClose}
|
||
onPress={() => setLanguageModalVisible(false)}
|
||
activeOpacity={0.8}
|
||
>
|
||
<Text style={styles.languageModalCloseText}>{t('personal.language.cancel')}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
|
||
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
|
||
{/* 装饰性圆圈 */}
|
||
<View style={styles.decorativeCircle1} />
|
||
<View style={styles.decorativeCircle2} />
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={{
|
||
paddingTop: insets.top,
|
||
paddingBottom: bottomPadding,
|
||
paddingHorizontal: 16,
|
||
}}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<UserHeader />
|
||
{userProfile.isVip ? <VipMembershipCard /> : <MembershipBanner />}
|
||
<StatsSection />
|
||
<View style={styles.fishRecordContainer}>
|
||
{/* <Image
|
||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-profile-fish.png' }}
|
||
contentFit="cover"
|
||
style={{ width: 16, height: 16, marginLeft: 6 }}
|
||
transition={200}
|
||
cachePolicy="memory-disk"
|
||
/> */}
|
||
<Text style={styles.fishRecordText}>{t('personal.fishRecord')}</Text>
|
||
</View>
|
||
<ActivityHeatMap />
|
||
{menuSections.map((section, index) => (
|
||
<MenuSection key={index} title={section.title} items={section.items} />
|
||
))}
|
||
</ScrollView>
|
||
<LanguageSelectorModal />
|
||
</View>
|
||
);
|
||
}
|
||
|
||
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: {
|
||
backgroundColor: '#ffffff',
|
||
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',
|
||
},
|
||
// 菜单项
|
||
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',
|
||
},
|
||
});
|