feat: 更新目标创建功能及相关组件

- 在 CreateGoalModal 中新增目标创建表单,支持设置标题、描述、重复周期、频率、提醒时间和结束日期
- 更新 GoalCard 组件,增加显示结束日期的功能
- 修改 goals.tsx 文件,调整 CreateGoalModal 的导入路径
- 更新 eslint 配置,增加对 node_modules 的忽略设置,优化代码检查
This commit is contained in:
richarjiang
2025-08-26 15:35:10 +08:00
parent 3f89023447
commit 0610f287ee
4 changed files with 193 additions and 34 deletions

View File

@@ -1,10 +1,10 @@
import { CreateGoalModal } from '@/components/CreateGoalModal';
import GoalTemplateModal from '@/components/GoalTemplateModal';
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
import { GuideTestButton } from '@/components/GuideTestButton';
import { TaskCard } from '@/components/TaskCard';
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
import { TaskProgressCard } from '@/components/TaskProgressCard';
import { CreateGoalModal } from '@/components/model/CreateGoalModal';
import { useGlobalDialog } from '@/components/ui/DialogProvider';
import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';

View File

@@ -11,11 +11,11 @@ interface GoalCardProps {
showStatus?: boolean;
}
export const GoalCard: React.FC<GoalCardProps> = ({
goal,
onPress,
export const GoalCard: React.FC<GoalCardProps> = ({
goal,
onPress,
onDelete,
showStatus = true
showStatus = true
}) => {
const swipeableRef = useRef<Swipeable>(null);
@@ -70,7 +70,7 @@ export const GoalCard: React.FC<GoalCardProps> = ({
// 根据目标类别或标题返回不同的图标
const title = goal.title.toLowerCase();
const category = goal.category?.toLowerCase();
if (title.includes('运动') || title.includes('健身') || title.includes('跑步')) {
return 'fitness-center';
} else if (title.includes('喝水') || title.includes('饮水')) {
@@ -118,7 +118,9 @@ export const GoalCard: React.FC<GoalCardProps> = ({
onPress={handleDelete}
activeOpacity={0.8}
>
<MaterialIcons name="delete" size={24} color="#EF4444" />
<MaterialIcons style={{
marginBottom: 10
}} name="delete" size={24} color="#EF4444" />
</TouchableOpacity>
);
};
@@ -149,7 +151,7 @@ export const GoalCard: React.FC<GoalCardProps> = ({
<Text style={styles.goalTitle} numberOfLines={1}>
{goal.title}
</Text>
{/* 底部信息行 */}
<View style={styles.goalInfo}>
{/* 积分 */}
@@ -187,6 +189,19 @@ export const GoalCard: React.FC<GoalCardProps> = ({
<View style={styles.infoItem}>
<Text style={styles.infoText}>{getRepeatTypeText(goal)}</Text>
</View>
{/* 结束日期 */}
{goal.endDate && (
<View style={styles.infoItem}>
<MaterialIcons
name="calendar-month"
size={12}
color="#9CA3AF"
style={{ marginRight: 4 }}
/>
<Text style={styles.infoText}>{goal.endDate}</Text>
</View>
)}
</View>
</View>

View File

@@ -4,7 +4,7 @@ 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, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import {
Alert,
Image,
@@ -61,7 +61,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
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]
@@ -71,6 +71,11 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
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) {
@@ -84,6 +89,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
setPriority(initialData.priority || 5);
setSelectedWeekdays(initialData.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]);
setSelectedMonthDays(initialData.customRepeatRule?.dayOfMonth || [1, 15]);
setEndDate(initialData.endDate || null);
}
}, [initialData]);
@@ -99,6 +105,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
setPriority(5);
setSelectedWeekdays([1, 2, 3, 4, 5]);
setSelectedMonthDays([1, 15]);
setEndDate(null);
};
// 处理关闭
@@ -139,12 +146,13 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
dayOfMonth: repeatType === 'monthly' ? selectedMonthDays : undefined, // 根据重复类型设置月几
},
startTime,
endDate: endDate || undefined,
};
console.log('goalData', goalData);
onSubmit(goalData);
// 通知父组件提交成功
if (onSuccess) {
onSuccess();
@@ -196,6 +204,55 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
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}
@@ -254,7 +311,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<TouchableOpacity style={styles.optionValue} onPress={() => setShowRepeatTypePicker(true)}>
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Image
<Image
source={require('@/assets/images/icons/icon-calender.png')}
style={styles.optionIconImage}
/>
@@ -268,10 +325,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
? `${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
? 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 }]}>
@@ -295,16 +352,16 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
/>
<View style={styles.repeatTypeModalSheet}>
{/* 关闭按钮 */}
<TouchableOpacity
<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) => (
@@ -325,7 +382,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
</TouchableOpacity>
))}
</View>
{/* 周几选择 - 仅在选择每周时显示 */}
{repeatType === 'weekly' && (
<View style={styles.weekdaySelection}>
@@ -396,7 +453,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
</View>
</View>
)}
{/* 完成按钮 */}
<TouchableOpacity
style={styles.repeatTypeCompleteButton}
@@ -413,10 +470,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<View style={styles.optionHeader}>
<View style={styles.optionIcon}>
<Image
source={require('@/assets/images/icons/icon-fire.png')}
style={styles.optionIconImage}
/>
<Image
source={require('@/assets/images/icons/icon-fire.png')}
style={styles.optionIconImage}
/>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -479,10 +536,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<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}
/>
<Image
source={require('@/assets/images/icons/icon-bell.png')}
style={styles.optionIconImage}
/>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -500,10 +557,10 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
<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}
/>
<Image
source={require('@/assets/images/icons/icon-clock.png')}
style={styles.optionIconImage}
/>
</View>
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
@@ -522,6 +579,40 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
</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}
@@ -561,6 +652,46 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
</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' ? 'calendar' : '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
@@ -933,6 +1064,19 @@ const styles = StyleSheet.create({
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;

View File

@@ -5,6 +5,6 @@ const expoConfig = require('eslint-config-expo/flat');
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
ignores: ['dist/*', 'node_modules/*'],
},
]);