- 在 CoachScreen 中优化欢迎消息的生成,整合用户配置文件数据,支持选择选项和表情 - 更新欢迎消息生成函数,返回包含内容、选择和交互类型的结构 - 在多个组件中调整样式,提升用户体验和界面一致性 - 在 Statistics 组件中添加记录更新时间,确保数据展示的准确性 - 在 FitnessRingsCard 中修正卡路里和运动时间的显示,确保数值四舍五入
1107 lines
35 KiB
TypeScript
1107 lines
35 KiB
TypeScript
import { Colors } from '@/constants/Colors';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import { CreateGoalRequest, GoalPriority, RepeatType, UpdateGoalRequest } from '@/types/goals';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Image,
|
||
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;
|
||
onUpdate?: (goalId: string, goalData: UpdateGoalRequest) => void;
|
||
onSuccess?: () => void;
|
||
loading?: boolean;
|
||
initialData?: Partial<CreateGoalRequest>;
|
||
editGoalId?: string;
|
||
}
|
||
|
||
const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
|
||
{ value: 'daily', label: '每日' },
|
||
{ value: 'weekly', label: '每周' },
|
||
{ value: 'monthly', label: '每月' },
|
||
];
|
||
|
||
const FREQUENCY_OPTIONS = Array.from({ length: 30 }, (_, i) => i + 1);
|
||
|
||
export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
|
||
visible,
|
||
onClose,
|
||
onSubmit,
|
||
onUpdate,
|
||
onSuccess,
|
||
loading = false,
|
||
initialData,
|
||
editGoalId,
|
||
}) => {
|
||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||
const colorTokens = Colors[theme];
|
||
|
||
// 表单状态
|
||
const [title, setTitle] = useState(initialData?.title || '');
|
||
const [description, setDescription] = useState(initialData?.description || '');
|
||
const [repeatType, setRepeatType] = useState<RepeatType>(initialData?.repeatType || 'daily');
|
||
const [frequency, setFrequency] = useState(initialData?.frequency || 1);
|
||
const [hasReminder, setHasReminder] = useState(initialData?.hasReminder || false);
|
||
const [showFrequencyPicker, setShowFrequencyPicker] = useState(false);
|
||
const [showRepeatTypePicker, setShowRepeatTypePicker] = useState(false);
|
||
const [reminderTime, setReminderTime] = useState(initialData?.reminderTime || '20:00');
|
||
const [category, setCategory] = useState(initialData?.category || '');
|
||
const [priority, setPriority] = useState<GoalPriority>(initialData?.priority || 5);
|
||
const [showTimePicker, setShowTimePicker] = useState(false);
|
||
const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null);
|
||
|
||
// 周几选择状态
|
||
const [selectedWeekdays, setSelectedWeekdays] = useState<number[]>(
|
||
initialData?.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]
|
||
); // 默认周一到周五
|
||
// 每月日期选择状态
|
||
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>(
|
||
initialData?.customRepeatRule?.dayOfMonth || [1, 15]
|
||
); // 默认1号和15号
|
||
|
||
// 结束日期选择状态
|
||
const [endDate, setEndDate] = useState<string | null>(initialData?.endDate || null);
|
||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||
const [tempSelectedDate, setTempSelectedDate] = useState<Date | null>(null);
|
||
|
||
// 当 initialData 变化时更新表单状态
|
||
useEffect(() => {
|
||
if (initialData) {
|
||
setTitle(initialData.title || '');
|
||
setDescription(initialData.description || '');
|
||
setRepeatType(initialData.repeatType || 'daily');
|
||
setFrequency(initialData.frequency || 1);
|
||
setHasReminder(initialData.hasReminder || false);
|
||
setReminderTime(initialData.reminderTime || '20:00');
|
||
setCategory(initialData.category || '');
|
||
setPriority(initialData.priority || 5);
|
||
setSelectedWeekdays(initialData.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]);
|
||
setSelectedMonthDays(initialData.customRepeatRule?.dayOfMonth || [1, 15]);
|
||
setEndDate(initialData.endDate || null);
|
||
}
|
||
}, [initialData]);
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
setTitle('');
|
||
setDescription('');
|
||
setRepeatType('daily');
|
||
setFrequency(1);
|
||
setHasReminder(false);
|
||
setReminderTime('20:00');
|
||
setCategory('');
|
||
setPriority(5);
|
||
setSelectedWeekdays([1, 2, 3, 4, 5]);
|
||
setSelectedMonthDays([1, 15]);
|
||
setEndDate(null);
|
||
};
|
||
|
||
// 处理关闭
|
||
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;
|
||
}
|
||
|
||
// 根据是否是编辑模式决定数据结构
|
||
if (editGoalId && onUpdate) {
|
||
// 更新模式:使用 UpdateGoalRequest 结构
|
||
const updateData: UpdateGoalRequest = {
|
||
title: title.trim(),
|
||
description: description.trim() || undefined,
|
||
repeatType,
|
||
frequency,
|
||
category: category.trim() || undefined,
|
||
priority,
|
||
hasReminder,
|
||
reminderTime: hasReminder ? reminderTime : undefined,
|
||
customRepeatRule: {
|
||
weekdays: repeatType === 'weekly' ? selectedWeekdays : [1, 2, 3, 4, 5, 6, 0],
|
||
dayOfMonth: repeatType === 'monthly' ? selectedMonthDays : undefined,
|
||
},
|
||
endDate: endDate || undefined,
|
||
};
|
||
|
||
console.log('updateData', updateData);
|
||
onUpdate(editGoalId, updateData);
|
||
} else {
|
||
// 创建模式:使用 CreateGoalRequest 结构
|
||
const goalData: CreateGoalRequest = {
|
||
title: title.trim(),
|
||
description: description.trim() || undefined,
|
||
repeatType,
|
||
frequency,
|
||
category: category.trim() || undefined,
|
||
priority,
|
||
hasReminder,
|
||
reminderTime: hasReminder ? reminderTime : undefined,
|
||
customRepeatRule: {
|
||
weekdays: repeatType === 'weekly' ? selectedWeekdays : [1, 2, 3, 4, 5, 6, 0],
|
||
dayOfMonth: repeatType === 'monthly' ? selectedMonthDays : undefined,
|
||
},
|
||
startTime,
|
||
endDate: endDate || undefined,
|
||
};
|
||
|
||
console.log('goalData', goalData);
|
||
onSubmit(goalData);
|
||
}
|
||
|
||
// 通知父组件提交成功
|
||
if (onSuccess) {
|
||
onSuccess();
|
||
}
|
||
};
|
||
|
||
// 时间选择器
|
||
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;
|
||
};
|
||
|
||
// 日期选择器处理
|
||
const handleDateChange = (event: any, selectedDate?: Date) => {
|
||
if (Platform.OS === 'android') {
|
||
if (event.type === 'set' && selectedDate) {
|
||
const isoDate = selectedDate.toISOString().split('T')[0];
|
||
setEndDate(isoDate);
|
||
}
|
||
setShowDatePicker(false);
|
||
} else {
|
||
if (selectedDate) {
|
||
setTempSelectedDate(selectedDate);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleConfirmDate = () => {
|
||
setShowDatePicker(false);
|
||
if (tempSelectedDate) {
|
||
const isoDate = tempSelectedDate.toISOString().split('T')[0];
|
||
setEndDate(isoDate);
|
||
}
|
||
setTempSelectedDate(null);
|
||
};
|
||
|
||
const handleCancelDate = () => {
|
||
setShowDatePicker(false);
|
||
setTempSelectedDate(null);
|
||
};
|
||
|
||
const showDatePickerModal = () => {
|
||
setShowDatePicker(true);
|
||
};
|
||
|
||
// 获取当前结束日期对应的Date对象
|
||
const getCurrentEndDate = () => {
|
||
if (endDate) {
|
||
return new Date(endDate + 'T00:00:00');
|
||
}
|
||
const tomorrow = new Date();
|
||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||
return tomorrow;
|
||
};
|
||
|
||
// 格式化显示日期
|
||
const formatDisplayDate = (dateString: string) => {
|
||
const date = new Date(dateString + 'T00:00:00');
|
||
return `${date.getFullYear()}年${(date.getMonth() + 1).toString().padStart(2, '0')}月${date.getDate().toString().padStart(2, '0')}日`;
|
||
};
|
||
|
||
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 }]}>
|
||
{editGoalId ? '编辑目标' : '创建新目标'}
|
||
</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}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-calender.png')}
|
||
style={styles.optionIconImage}
|
||
/>
|
||
</View>
|
||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||
重复周期
|
||
</Text>
|
||
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
|
||
{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 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.repeatTypeModalSheet}>
|
||
{/* 关闭按钮 */}
|
||
<TouchableOpacity
|
||
style={styles.modalCloseButton}
|
||
onPress={() => setShowRepeatTypePicker(false)}
|
||
>
|
||
<Text style={styles.modalCloseButtonText}>×</Text>
|
||
</TouchableOpacity>
|
||
|
||
{/* 标题 */}
|
||
<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>
|
||
|
||
{/* 周几选择 - 仅在选择每周时显示 */}
|
||
{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
|
||
key={weekday.value}
|
||
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.weekdayButtonText,
|
||
selectedWeekdays.includes(weekday.value) && styles.weekdayButtonTextSelected
|
||
]}>
|
||
{weekday.label}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</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>
|
||
</Modal>
|
||
|
||
{/* 频率设置 */}
|
||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||
<TouchableOpacity style={styles.optionValue} onPress={() => setShowFrequencyPicker(true)}>
|
||
|
||
<View style={styles.optionHeader}>
|
||
<View style={styles.optionIcon}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-fire.png')}
|
||
style={styles.optionIconImage}
|
||
/>
|
||
</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}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-bell.png')}
|
||
style={styles.optionIconImage}
|
||
/>
|
||
</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}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-clock.png')}
|
||
style={styles.optionIconImage}
|
||
/>
|
||
</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>
|
||
|
||
{/* 结束日期设置 */}
|
||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||
<View style={styles.optionHeader}>
|
||
<View style={styles.optionIcon}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-calender.png')}
|
||
style={styles.optionIconImage}
|
||
/>
|
||
</View>
|
||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||
结束日期
|
||
</Text>
|
||
<TouchableOpacity
|
||
style={[styles.optionValue]}
|
||
onPress={showDatePickerModal}
|
||
>
|
||
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
|
||
{endDate ? formatDisplayDate(endDate) : '无限制'}
|
||
</Text>
|
||
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
|
||
›
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
{endDate && (
|
||
<TouchableOpacity
|
||
style={styles.clearEndDateButton}
|
||
onPress={() => setEndDate(null)}
|
||
>
|
||
<Text style={styles.clearEndDateText}>清除结束日期</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</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>
|
||
|
||
{/* 日期选择器弹窗 */}
|
||
<Modal
|
||
visible={showDatePicker}
|
||
transparent
|
||
animationType="fade"
|
||
onRequestClose={() => setShowDatePicker(false)}
|
||
>
|
||
<TouchableOpacity
|
||
style={styles.modalBackdrop}
|
||
activeOpacity={1}
|
||
onPress={handleCancelDate}
|
||
/>
|
||
<View style={styles.modalSheet}>
|
||
<DateTimePicker
|
||
value={tempSelectedDate || getCurrentEndDate()}
|
||
mode="date"
|
||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||
onChange={handleDateChange}
|
||
minimumDate={new Date()}
|
||
locale="zh-CN"
|
||
/>
|
||
{Platform.OS === 'ios' && (
|
||
<View style={styles.modalActions}>
|
||
<TouchableOpacity
|
||
style={[styles.modalBtn]}
|
||
onPress={handleCancelDate}
|
||
>
|
||
<Text style={styles.modalBtnText}>取消</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.modalBtn, styles.modalBtnPrimary]}
|
||
onPress={handleConfirmDate}
|
||
>
|
||
<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 ? (editGoalId ? '更新中...' : '保存中...') : (editGoalId ? '更新' : '保存')}
|
||
</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: 20,
|
||
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,
|
||
},
|
||
optionIconImage: {
|
||
width: 20,
|
||
height: 20,
|
||
resizeMode: 'contain',
|
||
},
|
||
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',
|
||
},
|
||
// 重复类型选择器弹窗样式
|
||
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',
|
||
},
|
||
// 清除结束日期按钮样式
|
||
clearEndDateButton: {
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 8,
|
||
alignItems: 'center',
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#F1F5F9',
|
||
},
|
||
clearEndDateText: {
|
||
color: '#EF4444',
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
},
|
||
});
|
||
|
||
export default CreateGoalModal; |