Files
digital-pilates/components/CreateGoalModal.tsx
richarjiang 259f10540e feat: 新增任务管理功能及相关组件
- 将目标页面改为任务列表,支持任务的创建、完成和跳过功能
- 新增任务卡片和任务进度卡片组件,展示任务状态和进度
- 实现任务数据的获取和管理,集成Redux状态管理
- 更新API服务,支持任务相关的CRUD操作
- 编写任务管理功能实现文档,详细描述功能和组件架构
2025-08-22 17:30:14 +08:00

653 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { CreateGoalRequest, GoalPriority, RepeatType } from '@/types/goals';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react';
import {
Alert,
Modal,
Platform,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import WheelPickerExpo from 'react-native-wheel-picker-expo';
interface CreateGoalModalProps {
visible: boolean;
onClose: () => void;
onSubmit: (goalData: CreateGoalRequest) => void;
loading?: boolean;
}
const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
{ value: 'daily', label: '每日' },
{ value: 'weekly', label: '每周' },
{ value: 'monthly', label: '每月' },
{ value: 'custom', label: '自定义' },
];
const FREQUENCY_OPTIONS = Array.from({ length: 30 }, (_, i) => i + 1);
export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
visible,
onClose,
onSubmit,
loading = false,
}) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
// 表单状态
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [repeatType, setRepeatType] = useState<RepeatType>('daily');
const [frequency, setFrequency] = useState(1);
const [hasReminder, setHasReminder] = useState(false);
const [showFrequencyPicker, setShowFrequencyPicker] = useState(false);
const [showRepeatTypePicker, setShowRepeatTypePicker] = useState(false);
const [reminderTime, setReminderTime] = useState('20:00');
const [category, setCategory] = useState('');
const [priority, setPriority] = useState<GoalPriority>(5);
const [showTimePicker, setShowTimePicker] = useState(false);
const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null);
// 重置表单
const resetForm = () => {
setTitle('');
setDescription('');
setRepeatType('daily');
setFrequency(1);
setHasReminder(false);
setReminderTime('19:00');
setCategory('');
setPriority(5);
};
// 处理关闭
const handleClose = () => {
if (!loading) {
resetForm();
onClose();
}
};
// 处理提交
const handleSubmit = () => {
if (!title.trim()) {
Alert.alert('提示', '请输入目标标题');
return;
}
// 计算startTime从reminderTime中获取小时和分钟转换为当天的分钟数
let startTime: number | undefined;
if (reminderTime) {
const [hours, minutes] = reminderTime.split(':').map(Number);
startTime = hours * 60 + minutes;
}
const goalData: CreateGoalRequest = {
title: title.trim(),
description: description.trim() || undefined,
repeatType,
frequency,
category: category.trim() || undefined,
priority,
hasReminder,
reminderTime: hasReminder ? reminderTime : undefined,
reminderSettings: hasReminder ? {
enabled: true,
weekdays: [1, 2, 3, 4, 5, 6, 0], // 默认每天
} : undefined,
startTime,
};
console.log('goalData', goalData);
onSubmit(goalData);
};
// 时间选择器
const handleTimeChange = (event: any, selectedDate?: Date) => {
if (Platform.OS === 'android') {
// Android: 用户点击系统确认按钮后自动关闭
if (event.type === 'set' && selectedDate) {
const hours = selectedDate.getHours().toString().padStart(2, '0');
const minutes = selectedDate.getMinutes().toString().padStart(2, '0');
setReminderTime(`${hours}:${minutes}`);
}
setShowTimePicker(false);
} else {
// iOS: 只在用户点击自定义确认按钮时更新
if (selectedDate) {
setTempSelectedTime(selectedDate);
}
}
};
const handleConfirmTime = () => {
setShowTimePicker(false);
if (tempSelectedTime) {
const hours = tempSelectedTime.getHours().toString().padStart(2, '0');
const minutes = tempSelectedTime.getMinutes().toString().padStart(2, '0');
setReminderTime(`${hours}:${minutes}`);
}
setTempSelectedTime(null);
};
const handleCancelTime = () => {
setShowTimePicker(false);
setTempSelectedTime(null);
};
const showTimePickerModal = () => {
setShowTimePicker(true);
};
// 获取当前时间对应的Date对象
const getCurrentTimeDate = () => {
const [hours, minutes] = reminderTime.split(':').map(Number);
const date = new Date();
date.setHours(hours, minutes, 0, 0);
return date;
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={handleClose}
>
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
{/* 渐变背景 */}
<LinearGradient
colors={['#F0F9FF', '#E0F2FE']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
{/* 头部 */}
<View style={styles.header}>
<TouchableOpacity onPress={handleClose} disabled={loading}>
<Ionicons name="close" size={24} color={colorTokens.text} />
</TouchableOpacity>
<Text style={[styles.title, { color: colorTokens.text }]}>
</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* 目标标题输入 */}
<View style={styles.section}>
<View style={styles.iconTitleContainer}>
{/* <View style={styles.iconPlaceholder}>
<Text style={styles.iconText}>图标</Text>
</View> */}
<TextInput
style={[styles.titleInput, { color: colorTokens.text }]}
placeholder="写点什么..."
// placeholderTextColor={colorTokens.textSecondary}
value={title}
onChangeText={setTitle}
multiline
maxLength={100}
/>
</View>
{/* 装饰图案 */}
<View style={styles.decorationContainer}>
<View style={styles.decoration} />
</View>
</View>
{/* 目标重复周期 */}
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<TouchableOpacity style={styles.optionValue} onPress={() => setShowRepeatTypePicker(true)}>
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Text style={styles.optionIconText}>🔄</Text>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
</Text>
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
{REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label}
</Text>
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
</Text>
</View>
</TouchableOpacity>
</View>
{/* 重复周期选择器弹窗 */}
<Modal
visible={showRepeatTypePicker}
transparent
animationType="fade"
onRequestClose={() => setShowRepeatTypePicker(false)}
>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={() => setShowRepeatTypePicker(false)}
/>
<View style={styles.modalSheet}>
<View style={{ height: 200 }}>
<WheelPickerExpo
height={200}
width={150}
initialSelectedIndex={REPEAT_TYPE_OPTIONS.findIndex(opt => opt.value === repeatType)}
items={REPEAT_TYPE_OPTIONS.map(opt => ({ label: opt.label, value: opt.value }))}
onChange={({ item }) => setRepeatType(item.value)}
backgroundColor={colorTokens.card}
haptics
/>
</View>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.modalBtn]}
onPress={() => setShowRepeatTypePicker(false)}
>
<Text style={styles.modalBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalBtn, styles.modalBtnPrimary]}
onPress={() => setShowRepeatTypePicker(false)}
>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</TouchableOpacity>
</View>
)}
</View>
</Modal>
{/* 频率设置 */}
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<TouchableOpacity style={styles.optionValue} onPress={() => setShowFrequencyPicker(true)}>
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Text style={styles.optionIconText}>📊</Text>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
</Text>
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
{frequency}
</Text>
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
</Text>
</View>
</TouchableOpacity>
</View>
{/* 频率选择器弹窗 */}
<Modal
visible={showFrequencyPicker}
transparent
animationType="fade"
onRequestClose={() => setShowFrequencyPicker(false)}
>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={() => setShowFrequencyPicker(false)}
/>
<View style={styles.modalSheet}>
<View style={{ height: 200 }}>
<WheelPickerExpo
height={200}
width={150}
initialSelectedIndex={frequency - 1}
items={FREQUENCY_OPTIONS.map(num => ({ label: num.toString(), value: num }))}
onChange={({ item }) => setFrequency(item.value)}
backgroundColor={colorTokens.card}
// selectedStyle={{ borderColor: colorTokens.primary, borderWidth: 2 }}
haptics
/>
</View>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.modalBtn]}
onPress={() => setShowFrequencyPicker(false)}
>
<Text style={styles.modalBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalBtn, styles.modalBtnPrimary]}
onPress={() => setShowFrequencyPicker(false)}
>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</TouchableOpacity>
</View>
)}
</View>
</Modal>
{/* 提醒设置 */}
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Text style={styles.optionIconText}>🔔</Text>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
</Text>
<Switch
value={hasReminder}
onValueChange={setHasReminder}
trackColor={{ false: '#E5E5E5', true: '#6366F1' }}
thumbColor={hasReminder ? '#FFFFFF' : '#FFFFFF'}
/>
</View>
</View>
{/* 时间设置 */}
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Text style={styles.optionIconText}></Text>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
</Text>
<TouchableOpacity
style={[styles.optionValue]}
onPress={showTimePickerModal}
>
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
{reminderTime}
</Text>
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
</Text>
</TouchableOpacity>
</View>
</View>
{/* 时间选择器弹窗 */}
<Modal
visible={showTimePicker}
transparent
animationType="fade"
onRequestClose={() => setShowTimePicker(false)}
>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={handleCancelTime}
/>
<View style={styles.modalSheet}>
<DateTimePicker
value={tempSelectedTime || getCurrentTimeDate()}
mode="time"
is24Hour={true}
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={handleTimeChange}
/>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.modalBtn]}
onPress={handleCancelTime}
>
<Text style={styles.modalBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalBtn, styles.modalBtnPrimary]}
onPress={handleConfirmTime}
>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</TouchableOpacity>
</View>
)}
</View>
</Modal>
{/* 描述输入(可选) */}
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
<TextInput
style={[styles.descriptionInput, { color: colorTokens.text }]}
placeholder="添加描述(可选)"
placeholderTextColor={colorTokens.textSecondary}
value={description}
onChangeText={setDescription}
multiline
maxLength={500}
/>
</View>
</ScrollView>
{/* 保存按钮 */}
<View style={styles.footer}>
<TouchableOpacity
style={[
styles.saveButton,
{ opacity: loading || !title.trim() ? 0.5 : 1 }
]}
onPress={handleSubmit}
disabled={loading || !title.trim()}
>
<Text style={styles.saveButtonText}>
{loading ? '保存中...' : '保存'}
</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
opacity: 0.6,
},
decorativeCircle1: {
position: 'absolute',
top: -20,
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,
},
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 60,
paddingBottom: 20,
},
cancelButton: {
fontSize: 24,
fontWeight: '600',
},
title: {
fontSize: 18,
fontWeight: '600',
},
content: {
flex: 1,
paddingHorizontal: 20,
},
section: {
},
iconTitleContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
},
iconPlaceholder: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#F3F4F6',
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
iconText: {
fontSize: 12,
color: '#9CA3AF',
fontWeight: '500',
},
titleInput: {
flex: 1,
fontSize: 16,
fontWeight: '500',
minHeight: 60,
textAlignVertical: 'top',
},
decorationContainer: {
alignItems: 'flex-end',
paddingRight: 20,
},
decoration: {
width: 80,
height: 60,
backgroundColor: '#E0E7FF',
borderRadius: 40,
opacity: 0.6,
},
optionCard: {
borderRadius: 16,
marginBottom: 12,
overflow: 'hidden',
},
optionHeader: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
optionIcon: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F3F4F6',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
optionIconText: {
fontSize: 16,
},
optionLabel: {
flex: 1,
fontSize: 16,
fontWeight: '500',
},
optionValue: {
flexDirection: 'row',
alignItems: 'center',
},
optionValueText: {
fontSize: 16,
fontWeight: '500',
marginRight: 8,
},
chevron: {
fontSize: 20,
fontWeight: '300',
},
descriptionInput: {
padding: 16,
fontSize: 16,
minHeight: 80,
textAlignVertical: 'top',
},
footer: {
padding: 20,
paddingBottom: 40,
},
saveButton: {
backgroundColor: '#6366F1',
borderRadius: 16,
paddingVertical: 16,
alignItems: 'center',
},
saveButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.35)',
},
modalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: 16,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 8,
gap: 12,
},
modalBtn: {
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 10,
backgroundColor: '#F1F5F9',
},
modalBtnPrimary: {
backgroundColor: '#6366F1',
},
modalBtnText: {
color: '#334155',
fontWeight: '700',
},
modalBtnTextPrimary: {
color: '#FFFFFF',
fontWeight: '700',
},
});
export default CreateGoalModal;