feat: 新增目标创建弹窗的重复周期选择功能

- 在目标创建弹窗中添加重复周期选择功能,支持用户选择每日、每周和每月的重复类型
- 实现每周和每月的具体日期选择,用户可自定义选择周几和每月的日期
- 更新相关样式,提升用户体验和界面美观性
- 新增图标资源,替换原有文本图标,增强视觉效果
This commit is contained in:
2025-08-23 15:58:37 +08:00
parent 8a7599f630
commit 4382fb804f
7 changed files with 305 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -7,6 +7,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Alert, Alert,
Image,
Modal, Modal,
Platform, Platform,
ScrollView, ScrollView,
@@ -31,7 +32,6 @@ const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
{ value: 'daily', label: '每日' }, { value: 'daily', label: '每日' },
{ value: 'weekly', label: '每周' }, { value: 'weekly', label: '每周' },
{ value: 'monthly', label: '每月' }, { value: 'monthly', label: '每月' },
{ value: 'custom', label: '自定义' },
]; ];
const FREQUENCY_OPTIONS = Array.from({ length: 30 }, (_, i) => i + 1); const FREQUENCY_OPTIONS = Array.from({ length: 30 }, (_, i) => i + 1);
@@ -60,6 +60,11 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
const [showTimePicker, setShowTimePicker] = useState(false); const [showTimePicker, setShowTimePicker] = useState(false);
const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null); const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null);
// 周几选择状态
const [selectedWeekdays, setSelectedWeekdays] = useState<number[]>([1, 2, 3, 4, 5]); // 默认周一到周五
// 每月日期选择状态
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>([1, 15]); // 默认1号和15号
// 重置表单 // 重置表单
const resetForm = () => { const resetForm = () => {
setTitle(''); setTitle('');
@@ -70,6 +75,8 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
setReminderTime('20:00'); setReminderTime('20:00');
setCategory(''); setCategory('');
setPriority(5); setPriority(5);
setSelectedWeekdays([1, 2, 3, 4, 5]);
setSelectedMonthDays([1, 15]);
}; };
// 处理关闭 // 处理关闭
@@ -105,10 +112,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
priority, priority,
hasReminder, hasReminder,
reminderTime: hasReminder ? reminderTime : undefined, reminderTime: hasReminder ? reminderTime : undefined,
reminderSettings: hasReminder ? { customRepeatRule: {
enabled: true, weekdays: repeatType === 'weekly' ? selectedWeekdays : [1, 2, 3, 4, 5, 6, 0], // 根据重复类型设置周几
weekdays: [1, 2, 3, 4, 5, 6, 0], // 默认每天 dayOfMonth: repeatType === 'monthly' ? selectedMonthDays : undefined, // 根据重复类型设置月几
} : undefined, },
startTime, startTime,
}; };
@@ -225,13 +232,25 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<TouchableOpacity style={styles.optionValue} onPress={() => setShowRepeatTypePicker(true)}> <TouchableOpacity style={styles.optionValue} onPress={() => setShowRepeatTypePicker(true)}>
<View style={styles.optionHeader}> <View style={styles.optionHeader}>
<View style={styles.optionIcon}> <View style={styles.optionIcon}>
<Text style={styles.optionIconText}>🔄</Text> <Image
source={require('@/assets/images/icons/icon-calender.png')}
style={styles.optionIconImage}
/>
</View> </View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}> <Text style={[styles.optionLabel, { color: colorTokens.text }]}>
</Text> </Text>
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}> <Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
{REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label} {repeatType === 'weekly' && selectedWeekdays.length > 0
? selectedWeekdays.length <= 3
? `${REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label} ${selectedWeekdays.map(day => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][day]).join(' ')}`
: `${REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label} ${selectedWeekdays.slice(0, 2).map(day => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][day]).join(' ')}${selectedWeekdays.length}`
: repeatType === 'monthly' && selectedMonthDays.length > 0
? selectedMonthDays.length <= 3
? `${REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label} ${selectedMonthDays.map(day => `${day}`).join(' ')}`
: `${REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label} ${selectedMonthDays.slice(0, 2).map(day => `${day}`).join(' ')}${selectedMonthDays.length}`
: REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label
}
</Text> </Text>
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}> <Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
@@ -252,34 +271,117 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
activeOpacity={1} activeOpacity={1}
onPress={() => setShowRepeatTypePicker(false)} onPress={() => setShowRepeatTypePicker(false)}
/> />
<View style={styles.modalSheet}> <View style={styles.repeatTypeModalSheet}>
<View style={{ height: 200 }}> {/* 关闭按钮 */}
<WheelPickerExpo <TouchableOpacity
height={200} style={styles.modalCloseButton}
width={150} onPress={() => setShowRepeatTypePicker(false)}
initialSelectedIndex={REPEAT_TYPE_OPTIONS.findIndex(opt => opt.value === repeatType)} >
items={REPEAT_TYPE_OPTIONS.map(opt => ({ label: opt.label, value: opt.value }))} <Text style={styles.modalCloseButtonText}>×</Text>
onChange={({ item }) => setRepeatType(item.value)} </TouchableOpacity>
backgroundColor={colorTokens.card}
haptics {/* 标题 */}
/> <Text style={styles.repeatTypeModalTitle}></Text>
{/* 重复类型选择 */}
<View style={styles.repeatTypeOptions}>
{REPEAT_TYPE_OPTIONS.map((option) => (
<TouchableOpacity
key={option.value}
style={[
styles.repeatTypeButton,
repeatType === option.value && styles.repeatTypeButtonSelected
]}
onPress={() => setRepeatType(option.value)}
>
<Text style={[
styles.repeatTypeButtonText,
repeatType === option.value && styles.repeatTypeButtonTextSelected
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View> </View>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}> {/* 周几选择 - 仅在选择每周时显示 */}
{repeatType === 'weekly' && (
<View style={styles.weekdaySelection}>
<View style={styles.weekdayOptions}>
{[
{ value: 0, label: '周日' },
{ value: 1, label: '周一' },
{ value: 2, label: '周二' },
{ value: 3, label: '周三' },
{ value: 4, label: '周四' },
{ value: 5, label: '周五' },
{ value: 6, label: '周六' },
].map((weekday) => (
<TouchableOpacity <TouchableOpacity
style={[styles.modalBtn]} key={weekday.value}
onPress={() => setShowRepeatTypePicker(false)} style={[
styles.weekdayButton,
selectedWeekdays.includes(weekday.value) && styles.weekdayButtonSelected
]}
onPress={() => {
if (selectedWeekdays.includes(weekday.value)) {
setSelectedWeekdays(selectedWeekdays.filter(day => day !== weekday.value));
} else {
setSelectedWeekdays([...selectedWeekdays, weekday.value]);
}
}}
> >
<Text style={styles.modalBtnText}></Text> <Text style={[
</TouchableOpacity> styles.weekdayButtonText,
<TouchableOpacity selectedWeekdays.includes(weekday.value) && styles.weekdayButtonTextSelected
style={[styles.modalBtn, styles.modalBtnPrimary]} ]}>
onPress={() => setShowRepeatTypePicker(false)} {weekday.label}
> </Text>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</TouchableOpacity> </TouchableOpacity>
))}
</View>
</View> </View>
)} )}
{/* 每月日期选择 - 仅在选择每月时显示 */}
{repeatType === 'monthly' && (
<View style={styles.monthDaySelection}>
<View style={styles.monthDayGrid}>
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
<TouchableOpacity
key={day}
style={[
styles.monthDayGridItem,
selectedMonthDays.includes(day) && styles.monthDayGridItemSelected
]}
onPress={() => {
if (selectedMonthDays.includes(day)) {
setSelectedMonthDays(selectedMonthDays.filter(d => d !== day));
} else {
setSelectedMonthDays([...selectedMonthDays, day]);
}
}}
activeOpacity={0.8}
>
<Text style={[
styles.monthDayGridText,
selectedMonthDays.includes(day) && styles.monthDayGridTextSelected
]}>
{day}
</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
{/* 完成按钮 */}
<TouchableOpacity
style={styles.repeatTypeCompleteButton}
onPress={() => setShowRepeatTypePicker(false)}
>
<Text style={styles.repeatTypeCompleteButtonText}></Text>
</TouchableOpacity>
</View> </View>
</Modal> </Modal>
@@ -289,7 +391,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<View style={styles.optionHeader}> <View style={styles.optionHeader}>
<View style={styles.optionIcon}> <View style={styles.optionIcon}>
<Text style={styles.optionIconText}>📊</Text> <Image
source={require('@/assets/images/icons/icon-fire.png')}
style={styles.optionIconImage}
/>
</View> </View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}> <Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -352,7 +457,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}> <View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<View style={styles.optionHeader}> <View style={styles.optionHeader}>
<View style={styles.optionIcon}> <View style={styles.optionIcon}>
<Text style={styles.optionIconText}>🔔</Text> <Image
source={require('@/assets/images/icons/icon-bell.png')}
style={styles.optionIconImage}
/>
</View> </View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}> <Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -370,7 +478,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}> <View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<View style={styles.optionHeader}> <View style={styles.optionHeader}>
<View style={styles.optionIcon}> <View style={styles.optionIcon}>
<Text style={styles.optionIconText}></Text> <Image
source={require('@/assets/images/icons/icon-clock.png')}
style={styles.optionIconImage}
/>
</View> </View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}> <Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -577,6 +688,11 @@ const styles = StyleSheet.create({
optionIconText: { optionIconText: {
fontSize: 16, fontSize: 16,
}, },
optionIconImage: {
width: 20,
height: 20,
resizeMode: 'contain',
},
optionLabel: { optionLabel: {
flex: 1, flex: 1,
fontSize: 16, fontSize: 16,
@@ -655,6 +771,146 @@ const styles = StyleSheet.create({
color: '#FFFFFF', color: '#FFFFFF',
fontWeight: '700', fontWeight: '700',
}, },
// 重复类型选择器弹窗样式
repeatTypeModalSheet: {
position: 'absolute',
left: 20,
right: 20,
top: '50%',
transform: [{ translateY: -300 }],
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 24,
alignItems: 'center',
maxHeight: 500,
},
modalCloseButton: {
position: 'absolute',
top: 16,
left: 16,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
modalCloseButtonText: {
fontSize: 20,
color: '#666666',
fontWeight: '300',
},
repeatTypeModalTitle: {
fontSize: 18,
fontWeight: '600',
color: '#333333',
marginBottom: 20,
textAlign: 'center',
},
repeatTypeOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 8,
marginBottom: 20,
},
repeatTypeButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: '#F5F5F5',
borderWidth: 1,
borderColor: '#E5E5E5',
marginHorizontal: 4,
},
repeatTypeButtonSelected: {
backgroundColor: '#E6E6FA',
borderColor: '#8A2BE2',
},
repeatTypeButtonText: {
fontSize: 14,
color: '#666666',
fontWeight: '500',
},
repeatTypeButtonTextSelected: {
color: '#8A2BE2',
fontWeight: '600',
},
weekdaySelection: {
width: '100%',
marginBottom: 20,
},
weekdayOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 6,
},
weekdayButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 16,
backgroundColor: '#F5F5F5',
borderWidth: 1,
borderColor: '#E5E5E5',
marginHorizontal: 2,
},
weekdayButtonSelected: {
backgroundColor: '#8A2BE2',
borderColor: '#8A2BE2',
},
weekdayButtonText: {
fontSize: 12,
color: '#666666',
fontWeight: '500',
},
weekdayButtonTextSelected: {
color: '#FFFFFF',
fontWeight: '600',
},
// 每月日期选择样式 - 日历网格布局
monthDaySelection: {
width: '100%',
marginBottom: 20,
},
monthDayGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-start',
paddingHorizontal: 8,
},
monthDayGridItem: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#F5F5F5',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10,
marginRight: 8,
},
monthDayGridItemSelected: {
backgroundColor: '#8A2BE2',
},
monthDayGridText: {
fontSize: 12,
fontWeight: '600',
color: '#666666',
},
monthDayGridTextSelected: {
color: '#FFFFFF',
},
repeatTypeCompleteButton: {
width: '100%',
backgroundColor: '#8A2BE2',
borderRadius: 20,
paddingVertical: 14,
alignItems: 'center',
marginTop: 10,
},
repeatTypeCompleteButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
}); });
export default CreateGoalModal; export default CreateGoalModal;

View File

@@ -1,6 +1,6 @@
// 目标管理相关类型定义 // 目标管理相关类型定义
export type RepeatType = 'daily' | 'weekly' | 'monthly' | 'custom'; export type RepeatType = 'daily' | 'weekly' | 'monthly';
export type GoalStatus = 'active' | 'paused' | 'completed' | 'cancelled'; export type GoalStatus = 'active' | 'paused' | 'completed' | 'cancelled';
@@ -8,16 +8,15 @@ export type GoalPriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
// 自定义重复规则 // 自定义重复规则
export interface CustomRepeatRule { export interface CustomRepeatRule {
type: 'weekly' | 'monthly';
weekdays?: number[]; // 0-60为周日 weekdays?: number[]; // 0-60为周日
monthDays?: number[]; // 1-31 dayOfMonth?: number[]; // 1-31
interval?: number; // 间隔周数或月数
} }
// 提醒设置 // 提醒设置
export interface ReminderSettings { export interface ReminderSettings {
enabled: boolean; enabled: boolean;
weekdays?: number[]; // 0-60为周日 weekdays?: number[]; // 0-60为周日
monthDays?: number[]; // 1-31每月几号
sound?: string; sound?: string;
vibration?: boolean; vibration?: boolean;
} }