新增手腕温度健康数据追踪,支持Apple Watch睡眠手腕温度数据展示和30天历史趋势分析 实现经期数据与HealthKit的完整双向同步,支持读取、写入和删除经期记录 优化经期预测算法,基于历史数据计算更准确的周期和排卵日预测 重构经期UI组件为模块化结构,提升代码可维护性 添加完整的中英文国际化支持,覆盖所有新增功能界面
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
|
import { palette } from '@/constants/Colors';
|
|
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
|
import { useToast } from '@/contexts/ToastContext';
|
|
import { useI18n } from '@/hooks/useI18n';
|
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
|
import { useVipService } from '@/hooks/useVipService';
|
|
import {
|
|
getStatisticsCardOrder,
|
|
getStatisticsCardsVisibility,
|
|
setStatisticsCardOrder,
|
|
setStatisticsCardVisibility,
|
|
StatisticsCardsVisibility
|
|
} from '@/utils/userPreferences';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { router, useFocusEffect } from 'expo-router';
|
|
import React, { useCallback, useState } from 'react';
|
|
import { StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
|
import DraggableFlatList, { RenderItemParams, ScaleDecorator } from 'react-native-draggable-flatlist';
|
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
|
|
type CardItem = {
|
|
key: string;
|
|
title: string;
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
visible: boolean;
|
|
visibilityKey: keyof StatisticsCardsVisibility;
|
|
};
|
|
|
|
export default function StatisticsCustomizationScreen() {
|
|
const safeAreaTop = useSafeAreaTop(60);
|
|
const { t } = useI18n();
|
|
const { isVip } = useVipService();
|
|
const { openMembershipModal } = useMembershipModal();
|
|
const { showToast } = useToast();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [data, setData] = useState<CardItem[]>([]);
|
|
|
|
const CARD_CONFIG: Record<string, { icon: keyof typeof Ionicons.glyphMap; titleKey: string; visibilityKey: keyof StatisticsCardsVisibility }> = {
|
|
mood: { icon: 'happy-outline', titleKey: 'statisticsCustomization.items.mood', visibilityKey: 'showMood' },
|
|
steps: { icon: 'footsteps-outline', titleKey: 'statisticsCustomization.items.steps', visibilityKey: 'showSteps' },
|
|
stress: { icon: 'pulse-outline', titleKey: 'statisticsCustomization.items.stress', visibilityKey: 'showStress' },
|
|
sleep: { icon: 'moon-outline', titleKey: 'statisticsCustomization.items.sleep', visibilityKey: 'showSleep' },
|
|
fitness: { icon: 'fitness-outline', titleKey: 'statisticsCustomization.items.fitnessRings', visibilityKey: 'showFitnessRings' },
|
|
water: { icon: 'water-outline', titleKey: 'statisticsCustomization.items.water', visibilityKey: 'showWater' },
|
|
metabolism: { icon: 'flame-outline', titleKey: 'statisticsCustomization.items.basalMetabolism', visibilityKey: 'showBasalMetabolism' },
|
|
oxygen: { icon: 'water-outline', titleKey: 'statisticsCustomization.items.oxygenSaturation', visibilityKey: 'showOxygenSaturation' },
|
|
temperature: { icon: 'thermometer-outline', titleKey: 'statisticsCustomization.items.wristTemperature', visibilityKey: 'showWristTemperature' },
|
|
menstrual: { icon: 'rose-outline', titleKey: 'statisticsCustomization.items.menstrualCycle', visibilityKey: 'showMenstrualCycle' },
|
|
weight: { icon: 'scale-outline', titleKey: 'statisticsCustomization.items.weight', visibilityKey: 'showWeight' },
|
|
circumference: { icon: 'body-outline', titleKey: 'statisticsCustomization.items.circumference', visibilityKey: 'showCircumference' },
|
|
};
|
|
|
|
// 加载设置
|
|
const loadSettings = useCallback(async () => {
|
|
try {
|
|
const [visibility, order] = await Promise.all([
|
|
getStatisticsCardsVisibility(),
|
|
getStatisticsCardOrder(),
|
|
]);
|
|
|
|
// 确保 order 包含所有配置的 key (处理新增 key 的情况)
|
|
const allKeys = Object.keys(CARD_CONFIG);
|
|
const uniqueOrder = Array.from(new Set([...order, ...allKeys]));
|
|
|
|
const listData: CardItem[] = uniqueOrder
|
|
.filter(key => CARD_CONFIG[key]) // 过滤掉无效 key
|
|
.map(key => {
|
|
const config = CARD_CONFIG[key];
|
|
return {
|
|
key,
|
|
title: t(config.titleKey),
|
|
icon: config.icon,
|
|
visible: visibility[config.visibilityKey],
|
|
visibilityKey: config.visibilityKey,
|
|
};
|
|
});
|
|
|
|
setData(listData);
|
|
} catch (error) {
|
|
console.error('Failed to load statistics customization settings:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
// 页面聚焦时加载设置
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
loadSettings();
|
|
}, [loadSettings])
|
|
);
|
|
|
|
// 处理开关切换
|
|
const handleToggle = async (item: CardItem, value: boolean) => {
|
|
if (!isVip) {
|
|
showToast({
|
|
type: 'info',
|
|
message: t('statisticsCustomization.vipRequired'),
|
|
});
|
|
openMembershipModal();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 乐观更新 UI
|
|
setData(prev => prev.map(d => d.key === item.key ? { ...d, visible: value } : d));
|
|
|
|
await setStatisticsCardVisibility(item.visibilityKey, value);
|
|
} catch (error) {
|
|
console.error(`Failed to set ${item.key}:`, error);
|
|
// 回滚
|
|
setData(prev => prev.map(d => d.key === item.key ? { ...d, visible: !value } : d));
|
|
}
|
|
};
|
|
|
|
// 处理排序结束
|
|
const handleDragEnd = async ({ data: newData }: { data: CardItem[] }) => {
|
|
setData(newData);
|
|
const newOrder = newData.map(item => item.key);
|
|
try {
|
|
await setStatisticsCardOrder(newOrder);
|
|
} catch (error) {
|
|
console.error('Failed to save card order:', error);
|
|
}
|
|
};
|
|
|
|
const renderItem = useCallback(({ item, drag, isActive }: RenderItemParams<CardItem>) => {
|
|
const handleDrag = () => {
|
|
if (!isVip) {
|
|
showToast({
|
|
type: 'info',
|
|
message: t('statisticsCustomization.vipRequired'),
|
|
});
|
|
openMembershipModal();
|
|
return;
|
|
}
|
|
drag();
|
|
};
|
|
|
|
return (
|
|
<ScaleDecorator>
|
|
<TouchableOpacity
|
|
onLongPress={handleDrag}
|
|
disabled={isActive}
|
|
activeOpacity={1}
|
|
style={[
|
|
styles.rowItem,
|
|
isActive && styles.activeItem,
|
|
]}
|
|
>
|
|
<View style={styles.itemContent}>
|
|
<View style={styles.leftContent}>
|
|
<View style={styles.dragHandle}>
|
|
<Ionicons name="reorder-three-outline" size={24} color="#C7C7CC" />
|
|
</View>
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name={item.icon} size={24} color={'#9370DB'} />
|
|
</View>
|
|
<Text style={styles.itemTitle}>{item.title}</Text>
|
|
</View>
|
|
<Switch
|
|
value={item.visible}
|
|
onValueChange={(v) => handleToggle(item, v)}
|
|
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
|
thumbColor="#FFFFFF"
|
|
style={styles.switch}
|
|
/>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</ScaleDecorator>
|
|
);
|
|
}, [handleToggle, isVip, t, showToast, openMembershipModal]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
|
<LinearGradient
|
|
colors={[palette.purple[100], '#F5F5F5']}
|
|
start={{ x: 1, y: 0 }}
|
|
end={{ x: 0.3, y: 0.4 }}
|
|
style={styles.gradientBackground}
|
|
/>
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.loadingText}>{t('notificationSettings.loading')}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<GestureHandlerRootView style={styles.container}>
|
|
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
|
|
|
<LinearGradient
|
|
colors={[palette.purple[100], '#F5F5F5']}
|
|
start={{ x: 1, y: 0 }}
|
|
end={{ x: 0.3, y: 0.4 }}
|
|
style={styles.gradientBackground}
|
|
/>
|
|
|
|
<HeaderBar
|
|
title={t('statisticsCustomization.title')}
|
|
onBack={() => router.back()}
|
|
/>
|
|
|
|
<DraggableFlatList
|
|
data={data}
|
|
onDragEnd={handleDragEnd}
|
|
keyExtractor={(item) => item.key}
|
|
renderItem={renderItem}
|
|
contentContainerStyle={[
|
|
styles.scrollContent,
|
|
{ paddingTop: safeAreaTop }
|
|
]}
|
|
showsVerticalScrollIndicator={false}
|
|
ListHeaderComponent={() => (
|
|
<>
|
|
<View style={styles.headerSection}>
|
|
<Text style={styles.subtitle}>{t('notificationSettings.sections.description')}</Text>
|
|
<View style={styles.descriptionCard}>
|
|
<View style={styles.hintRow}>
|
|
<Ionicons name="information-circle-outline" size={20} color="#9370DB" />
|
|
<Text style={styles.descriptionText}>
|
|
{t('statisticsCustomization.description.text')}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>{t('statisticsCustomization.sectionTitle')}</Text>
|
|
</View>
|
|
</>
|
|
)}
|
|
/>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#F5F5F5',
|
|
},
|
|
gradientBackground: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
height: '60%',
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
fontSize: 16,
|
|
color: '#666',
|
|
},
|
|
scrollContent: {
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 40,
|
|
},
|
|
headerSection: {
|
|
marginBottom: 20,
|
|
},
|
|
subtitle: {
|
|
fontSize: 14,
|
|
color: '#6C757D',
|
|
marginBottom: 12,
|
|
marginLeft: 4,
|
|
},
|
|
descriptionCard: {
|
|
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
|
borderRadius: 12,
|
|
padding: 12,
|
|
gap: 8,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(147, 112, 219, 0.1)',
|
|
},
|
|
hintRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
},
|
|
descriptionText: {
|
|
flex: 1,
|
|
fontSize: 13,
|
|
color: '#2C3E50',
|
|
lineHeight: 18,
|
|
},
|
|
sectionHeader: {
|
|
marginBottom: 12,
|
|
marginLeft: 4,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#2C3E50',
|
|
},
|
|
rowItem: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 16,
|
|
marginBottom: 12,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.03,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
activeItem: {
|
|
backgroundColor: '#FAFAFA',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 8,
|
|
elevation: 4,
|
|
zIndex: 100,
|
|
transform: [{ scale: 1.02 }],
|
|
},
|
|
itemContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: 16,
|
|
height: 72,
|
|
},
|
|
leftContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
dragHandle: {
|
|
paddingRight: 12,
|
|
},
|
|
iconContainer: {
|
|
width: 40,
|
|
height: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(147, 112, 219, 0.05)',
|
|
borderRadius: 12,
|
|
marginRight: 12,
|
|
},
|
|
itemTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: '#2C3E50',
|
|
flex: 1,
|
|
},
|
|
switch: {
|
|
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
|
|
},
|
|
}); |