- 新增生理周期追踪页面及相关算法逻辑,支持经期记录与预测 - 新增首页统计卡片自定义页面,支持VIP用户调整卡片显示状态与顺序 - 重构首页统计页面布局逻辑,支持动态渲染与混合布局 - 引入 react-native-draggable-flatlist 用于实现拖拽排序功能 - 添加相关多语言配置及用户偏好设置存储接口
158 lines
3.5 KiB
TypeScript
158 lines
3.5 KiB
TypeScript
import { LinearGradient } from 'expo-linear-gradient';
|
|
import React, { useMemo } from 'react';
|
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
|
import { Colors } from '@/constants/Colors';
|
|
import { buildMenstrualTimeline } from '@/utils/menstrualCycle';
|
|
|
|
type Props = {
|
|
onPress?: () => void;
|
|
};
|
|
|
|
const RingIcon = () => (
|
|
<View style={styles.iconWrapper}>
|
|
<LinearGradient
|
|
colors={['#f572a7', '#f0a4ff', '#6f6ced']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.iconGradient}
|
|
>
|
|
<View style={styles.iconInner} />
|
|
</LinearGradient>
|
|
</View>
|
|
);
|
|
|
|
export const MenstrualCycleCard: React.FC<Props> = ({ onPress }) => {
|
|
const { todayInfo, periodLength } = useMemo(() => buildMenstrualTimeline(), []);
|
|
|
|
const summary = useMemo(() => {
|
|
if (!todayInfo) {
|
|
return {
|
|
state: '待记录',
|
|
dayText: '点击记录本次经期',
|
|
number: undefined,
|
|
};
|
|
}
|
|
|
|
if (todayInfo.status === 'period' || todayInfo.status === 'predicted-period') {
|
|
return {
|
|
state: todayInfo.status === 'period' ? '经期' : '预测经期',
|
|
dayText: '天',
|
|
number: todayInfo.dayOfCycle ?? 1,
|
|
};
|
|
}
|
|
|
|
if (todayInfo.status === 'ovulation-day') {
|
|
return {
|
|
state: '排卵日',
|
|
dayText: '易孕窗口',
|
|
number: undefined,
|
|
};
|
|
}
|
|
|
|
return {
|
|
state: '排卵期',
|
|
dayText: `距离排卵日${Math.max(periodLength - 1, 1)}天`,
|
|
number: undefined,
|
|
};
|
|
}, [periodLength, todayInfo]);
|
|
|
|
return (
|
|
<TouchableOpacity activeOpacity={0.92} onPress={onPress} style={styles.wrapper}>
|
|
<View style={styles.headerRow}>
|
|
<RingIcon />
|
|
<Text style={styles.title}>生理周期</Text>
|
|
<View style={styles.badgeOuter}>
|
|
<View style={styles.badgeInner} />
|
|
</View>
|
|
</View>
|
|
<View style={styles.content}>
|
|
<Text style={styles.stateText}>{summary.state}</Text>
|
|
<Text style={styles.dayRow}>
|
|
{summary.number !== undefined ? (
|
|
<>
|
|
第 <Text style={styles.dayNumber}>{summary.number}</Text> {summary.dayText}
|
|
</>
|
|
) : (
|
|
summary.dayText
|
|
)}
|
|
</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
wrapper: {
|
|
width: '100%',
|
|
},
|
|
headerRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
},
|
|
iconWrapper: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
iconGradient: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 11,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
iconInner: {
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
backgroundColor: '#fff',
|
|
},
|
|
title: {
|
|
fontSize: 14,
|
|
color: '#192126',
|
|
fontWeight: '600',
|
|
flex: 1,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
badgeOuter: {
|
|
width: 18,
|
|
height: 18,
|
|
borderRadius: 9,
|
|
borderWidth: 2,
|
|
borderColor: '#fbcfe8',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
badgeInner: {
|
|
width: 6,
|
|
height: 6,
|
|
borderRadius: 3,
|
|
backgroundColor: Colors.light.primary,
|
|
opacity: 0.35,
|
|
},
|
|
content: {
|
|
marginTop: 12,
|
|
},
|
|
stateText: {
|
|
fontSize: 12,
|
|
color: '#515558',
|
|
marginBottom: 4,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
dayRow: {
|
|
fontSize: 14,
|
|
color: '#192126',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
dayNumber: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#192126',
|
|
fontFamily: 'AliBold',
|
|
},
|
|
});
|