feat(fasting): 新增轻断食功能模块
新增完整的轻断食功能,包括: - 断食计划列表和详情页面,支持12-12、14-10、16-8、18-6四种计划 - 断食状态实时追踪和倒计时显示 - 自定义开始时间选择器 - 断食通知提醒功能 - Redux状态管理和数据持久化 - 新增tab导航入口和路由配置
This commit is contained in:
274
components/fasting/FastingOverviewCard.tsx
Normal file
274
components/fasting/FastingOverviewCard.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import { CircularRing } from '@/components/CircularRing';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import type { FastingPlan } from '@/constants/Fasting';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
type FastingOverviewCardProps = {
|
||||
plan?: FastingPlan;
|
||||
phaseLabel: string;
|
||||
countdownLabel: string;
|
||||
countdownValue: string;
|
||||
startDayLabel: string;
|
||||
startTimeLabel: string;
|
||||
endDayLabel: string;
|
||||
endTimeLabel: string;
|
||||
onAdjustStartPress: () => void;
|
||||
onViewMealsPress: () => void;
|
||||
progress: number;
|
||||
};
|
||||
|
||||
export function FastingOverviewCard({
|
||||
plan,
|
||||
phaseLabel,
|
||||
countdownLabel,
|
||||
countdownValue,
|
||||
startDayLabel,
|
||||
startTimeLabel,
|
||||
endDayLabel,
|
||||
endTimeLabel,
|
||||
onAdjustStartPress,
|
||||
onViewMealsPress,
|
||||
progress,
|
||||
}: FastingOverviewCardProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colors = Colors[theme];
|
||||
const themeColors = plan?.theme;
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
themeColors?.accentSecondary ?? colors.heroSurfaceTint,
|
||||
themeColors?.backdrop ?? colors.pageBackgroundEmphasis,
|
||||
]}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<View>
|
||||
<Text style={styles.planLabel}>轻断食计划</Text>
|
||||
{plan?.id && (
|
||||
<View style={styles.planTag}>
|
||||
<Text style={[styles.planTagText, { color: themeColors?.accent ?? colors.primary }]}>
|
||||
{plan.id}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{plan?.badge && (
|
||||
<View style={[styles.badge, { backgroundColor: `${themeColors?.accent ?? colors.primary}20` }]}>
|
||||
<Text style={[styles.badgeText, { color: themeColors?.accent ?? colors.primary }]}>
|
||||
{plan.badge}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.scheduleRow}>
|
||||
<View style={styles.scheduleCell}>
|
||||
<Text style={styles.scheduleLabel}>断食开始时间</Text>
|
||||
<Text style={styles.scheduleDay}>{startDayLabel}</Text>
|
||||
<Text style={styles.scheduleTime}>{startTimeLabel}</Text>
|
||||
</View>
|
||||
<View style={styles.separator} />
|
||||
<View style={styles.scheduleCell}>
|
||||
<Text style={styles.scheduleLabel}>断食结束时间</Text>
|
||||
<Text style={styles.scheduleDay}>{endDayLabel}</Text>
|
||||
<Text style={styles.scheduleTime}>{endTimeLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<View style={styles.ringContainer}>
|
||||
<CircularRing
|
||||
size={168}
|
||||
strokeWidth={14}
|
||||
progress={progress}
|
||||
progressColor={themeColors?.ringProgress ?? colors.primary}
|
||||
trackColor={themeColors?.ringTrack ?? 'rgba(0,0,0,0.05)'}
|
||||
showCenterText={false}
|
||||
startAngleDeg={-90}
|
||||
resetToken={phaseLabel}
|
||||
/>
|
||||
<View style={styles.ringContent}>
|
||||
<Text style={styles.phaseText}>{phaseLabel}</Text>
|
||||
<Text style={styles.countdownLabel}>{countdownLabel}</Text>
|
||||
<Text style={styles.countdownValue}>{countdownValue}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionsRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
|
||||
onPress={onAdjustStartPress}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
|
||||
提前开始断食
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* <TouchableOpacity
|
||||
style={[styles.primaryButton, { backgroundColor: themeColors?.accent ?? colors.primary }]}
|
||||
onPress={onViewMealsPress}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>查看食谱</Text>
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 28,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 24,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 16 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 24,
|
||||
elevation: 6,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
planLabel: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
marginBottom: 6,
|
||||
},
|
||||
planTag: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
},
|
||||
planTagText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
badge: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 18,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
scheduleRow: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
scheduleCell: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
scheduleLabel: {
|
||||
fontSize: 13,
|
||||
color: '#70808E',
|
||||
marginBottom: 6,
|
||||
fontWeight: '500',
|
||||
},
|
||||
scheduleDay: {
|
||||
fontSize: 16,
|
||||
color: '#2E3142',
|
||||
fontWeight: '600',
|
||||
},
|
||||
scheduleTime: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
marginTop: 4,
|
||||
},
|
||||
separator: {
|
||||
width: 1,
|
||||
height: 52,
|
||||
backgroundColor: 'rgba(112,128,142,0.22)',
|
||||
},
|
||||
statusRow: {
|
||||
marginTop: 26,
|
||||
alignItems: 'center',
|
||||
},
|
||||
ringContainer: {
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 90,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
ringContent: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
phaseText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
marginBottom: 8,
|
||||
},
|
||||
countdownLabel: {
|
||||
fontSize: 12,
|
||||
color: '#6F7D87',
|
||||
marginBottom: 4,
|
||||
},
|
||||
countdownValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
actionsRow: {
|
||||
marginTop: 24,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
secondaryButton: {
|
||||
paddingHorizontal: 20,
|
||||
borderWidth: 1.2,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 14,
|
||||
marginRight: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.92)',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
primaryButton: {
|
||||
flex: 1,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#2E3142',
|
||||
},
|
||||
primaryButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
185
components/fasting/FastingPlanList.tsx
Normal file
185
components/fasting/FastingPlanList.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { FastingPlan } from '@/constants/Fasting';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
type FastingPlanListProps = {
|
||||
plans: FastingPlan[];
|
||||
activePlanId?: string | null;
|
||||
onSelectPlan: (plan: FastingPlan) => void;
|
||||
};
|
||||
|
||||
const difficultyLabel: Record<FastingPlan['difficulty'], string> = {
|
||||
新手: '适合入门',
|
||||
进阶: '脂代提升',
|
||||
强化: '平台突破',
|
||||
};
|
||||
|
||||
export function FastingPlanList({ plans, activePlanId, onSelectPlan }: FastingPlanListProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colors = Colors[theme];
|
||||
|
||||
const sortedPlans = useMemo(
|
||||
() => plans.slice().sort((a, b) => a.fastingHours - b.fastingHours),
|
||||
[plans]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.headerTitle}>单日计划</Text>
|
||||
<View style={styles.headerBadge}>
|
||||
<Text style={styles.headerBadgeText}>精选</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{sortedPlans.map((plan) => {
|
||||
const isActive = plan.id === activePlanId;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={plan.id}
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: plan.theme.backdrop,
|
||||
borderColor: isActive ? plan.theme.accent : 'transparent',
|
||||
},
|
||||
]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => onSelectPlan(plan)}
|
||||
>
|
||||
<View style={styles.cardTopRow}>
|
||||
<View style={[styles.difficultyPill, { backgroundColor: `${plan.theme.accent}1A` }]}>
|
||||
<Text style={[styles.difficultyText, { color: plan.theme.accent }]}>
|
||||
{plan.difficulty}
|
||||
</Text>
|
||||
</View>
|
||||
{plan.badge && (
|
||||
<View style={[styles.badgePill, { backgroundColor: `${plan.theme.accent}26` }]}>
|
||||
<Text style={[styles.badgeText, { color: plan.theme.accent }]}>
|
||||
{plan.badge}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.cardTitle}>{plan.title}</Text>
|
||||
<Text style={styles.cardSubtitle}>{plan.subtitle}</Text>
|
||||
<View style={styles.metaRow}>
|
||||
<View style={styles.metaItem}>
|
||||
<IconSymbol name="chart.pie.fill" color={colors.icon} size={16} />
|
||||
<Text style={styles.metaText}>{plan.fastingHours} 小时断食</Text>
|
||||
</View>
|
||||
<View style={styles.metaItem}>
|
||||
<IconSymbol name="flag.fill" color={colors.icon} size={16} />
|
||||
<Text style={styles.metaText}>{difficultyLabel[plan.difficulty]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
marginTop: 32,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
},
|
||||
headerBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 14,
|
||||
backgroundColor: 'rgba(53, 52, 69, 0.08)',
|
||||
},
|
||||
headerBadgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#353445',
|
||||
},
|
||||
scrollContent: {
|
||||
paddingRight: 20,
|
||||
},
|
||||
card: {
|
||||
width: 220,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 18,
|
||||
marginRight: 16,
|
||||
borderWidth: 2,
|
||||
},
|
||||
cardTopRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
difficultyPill: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
},
|
||||
difficultyText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
badgePill: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 14,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#2E3142',
|
||||
marginBottom: 6,
|
||||
},
|
||||
cardSubtitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#5B6572',
|
||||
marginBottom: 12,
|
||||
},
|
||||
metaRow: {
|
||||
marginTop: 'auto',
|
||||
},
|
||||
metaItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
},
|
||||
metaText: {
|
||||
marginLeft: 6,
|
||||
fontSize: 12,
|
||||
color: '#5B6572',
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
242
components/fasting/FastingStartPickerModal.tsx
Normal file
242
components/fasting/FastingStartPickerModal.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import WheelPickerExpo from 'react-native-wheel-picker-expo';
|
||||
import dayjs from 'dayjs';
|
||||
import { FloatingSelectionCard } from '@/components/ui/FloatingSelectionCard';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
type FastingStartPickerModalProps = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
initialDate?: Date | null;
|
||||
recommendedDate?: Date | null;
|
||||
onConfirm: (date: Date) => void;
|
||||
};
|
||||
|
||||
type DayOption = {
|
||||
label: string;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
const buildDayOptions = (now: dayjs.Dayjs): DayOption[] => ([
|
||||
{ offset: -1, label: '昨天' },
|
||||
{ offset: 0, label: '今天' },
|
||||
{ offset: 1, label: '明天' },
|
||||
].map((item) => ({
|
||||
...item,
|
||||
label: item.offset === -1
|
||||
? '昨天'
|
||||
: item.offset === 0
|
||||
? '今天'
|
||||
: '明天',
|
||||
})));
|
||||
|
||||
const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
||||
const MINUTES = Array.from({ length: 12 }, (_, i) => i * 5);
|
||||
|
||||
export function FastingStartPickerModal({
|
||||
visible,
|
||||
onClose,
|
||||
initialDate,
|
||||
recommendedDate,
|
||||
onConfirm,
|
||||
}: FastingStartPickerModalProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colors = Colors[theme];
|
||||
const now = useMemo(() => dayjs(), []);
|
||||
const dayOptions = useMemo(() => buildDayOptions(now), [now]);
|
||||
|
||||
const deriveInitialIndexes = (source?: Date | null) => {
|
||||
const seed = source ? dayjs(source) : now;
|
||||
const dayDiff = seed.startOf('day').diff(now.startOf('day'), 'day');
|
||||
const dayIndex = dayOptions.findIndex((option) => option.offset === dayDiff);
|
||||
const hourIndex = HOURS.findIndex((hour) => hour === seed.hour());
|
||||
const snappedMinute = seed.minute() - (seed.minute() % 5);
|
||||
const minuteIndex = MINUTES.findIndex((minute) => minute === snappedMinute);
|
||||
|
||||
return {
|
||||
dayIndex: dayIndex === -1 ? 1 : dayIndex,
|
||||
hourIndex: hourIndex === -1 ? now.hour() : hourIndex,
|
||||
minuteIndex:
|
||||
minuteIndex === -1
|
||||
? Math.max(0, Math.min(MINUTES.length - 1, Math.floor(seed.minute() / 5)))
|
||||
: minuteIndex,
|
||||
};
|
||||
};
|
||||
|
||||
const defaultBaseRef = useRef(new Date());
|
||||
|
||||
const baseDate = useMemo(
|
||||
() => initialDate ?? recommendedDate ?? defaultBaseRef.current,
|
||||
[initialDate, recommendedDate]
|
||||
);
|
||||
|
||||
const baseTimestamp = baseDate.getTime();
|
||||
|
||||
const [{ dayIndex, hourIndex, minuteIndex }, setIndexes] = useState(() =>
|
||||
deriveInitialIndexes(baseDate)
|
||||
);
|
||||
const [pickerKey, setPickerKey] = useState(0);
|
||||
const lastAppliedTimestamp = useRef<number | null>(null);
|
||||
const wasVisibleRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
wasVisibleRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldReset =
|
||||
!wasVisibleRef.current ||
|
||||
lastAppliedTimestamp.current !== baseTimestamp;
|
||||
|
||||
if (shouldReset) {
|
||||
const nextIndexes = deriveInitialIndexes(baseDate);
|
||||
setIndexes(nextIndexes);
|
||||
setPickerKey((prev) => prev + 1);
|
||||
lastAppliedTimestamp.current = baseTimestamp;
|
||||
}
|
||||
|
||||
wasVisibleRef.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, baseTimestamp]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
const selectedDay = dayOptions[dayIndex] ?? dayOptions[1];
|
||||
const base = now.startOf('day').add(selectedDay?.offset ?? 0, 'day');
|
||||
const hour = HOURS[hourIndex] ?? now.hour();
|
||||
const minute = MINUTES[minuteIndex] ?? 0;
|
||||
const result = base.hour(hour).minute(minute).second(0).millisecond(0);
|
||||
onConfirm(result.toDate());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleUseRecommended = () => {
|
||||
if (!recommendedDate) return;
|
||||
setIndexes(deriveInitialIndexes(recommendedDate));
|
||||
setPickerKey((prev) => prev + 1);
|
||||
lastAppliedTimestamp.current = recommendedDate.getTime();
|
||||
};
|
||||
|
||||
const pickerIndicatorStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: `${colors.primary}12`,
|
||||
borderRadius: 12,
|
||||
}),
|
||||
[colors.primary]
|
||||
);
|
||||
|
||||
const textStyle = {
|
||||
fontSize: 18,
|
||||
fontWeight: '600' as const,
|
||||
color: '#2E3142',
|
||||
};
|
||||
|
||||
return (
|
||||
<FloatingSelectionCard
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
title="断食开始时间"
|
||||
>
|
||||
<View style={styles.pickerRow}>
|
||||
<WheelPickerExpo
|
||||
key={`day-${pickerKey}`}
|
||||
height={180}
|
||||
width={110}
|
||||
initialSelectedIndex={dayIndex}
|
||||
items={dayOptions.map((item) => ({ label: item.label, value: item.offset }))}
|
||||
onChange={({ index }) => setIndexes((prev) => ({ ...prev, dayIndex: index }))}
|
||||
backgroundColor="transparent"
|
||||
itemTextStyle={textStyle}
|
||||
selectedIndicatorStyle={pickerIndicatorStyle}
|
||||
haptics
|
||||
/>
|
||||
<WheelPickerExpo
|
||||
key={`hour-${pickerKey}`}
|
||||
height={180}
|
||||
width={110}
|
||||
initialSelectedIndex={hourIndex}
|
||||
items={HOURS.map((hour) => ({ label: hour.toString().padStart(2, '0'), value: hour }))}
|
||||
onChange={({ index }) => setIndexes((prev) => ({ ...prev, hourIndex: index }))}
|
||||
backgroundColor="transparent"
|
||||
itemTextStyle={textStyle}
|
||||
selectedIndicatorStyle={pickerIndicatorStyle}
|
||||
haptics
|
||||
/>
|
||||
<WheelPickerExpo
|
||||
key={`minute-${pickerKey}`}
|
||||
height={180}
|
||||
width={110}
|
||||
initialSelectedIndex={minuteIndex}
|
||||
items={MINUTES.map((minute) => ({
|
||||
label: minute.toString().padStart(2, '0'),
|
||||
value: minute,
|
||||
}))}
|
||||
onChange={({ index }) => setIndexes((prev) => ({ ...prev, minuteIndex: index }))}
|
||||
backgroundColor="transparent"
|
||||
itemTextStyle={textStyle}
|
||||
selectedIndicatorStyle={pickerIndicatorStyle}
|
||||
haptics
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.recommendButton, { borderColor: colors.primary }]}
|
||||
onPress={handleUseRecommended}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.recommendButtonText, { color: colors.primary }]}>使用推荐时间</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.confirmButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleConfirm}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>确定</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</FloatingSelectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pickerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
marginBottom: 24,
|
||||
},
|
||||
footerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
recommendButton: {
|
||||
flex: 1,
|
||||
borderWidth: 1.2,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 12,
|
||||
marginRight: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
},
|
||||
recommendButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
confirmButton: {
|
||||
flex: 1,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
confirmButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user