feat: 更新 CLAUDE.md 文件及多个组件以优化用户体验和功能

- 更新 CLAUDE.md 文件,重构架构部分,增加认证和数据层的描述
- 在 GoalsScreen 中新增目标模板选择功能,支持用户选择和创建目标
- 在 CreateGoalModal 中添加初始数据支持,优化目标创建体验
- 新增 GoalTemplateModal 组件,提供目标模板选择界面
- 更新 NotificationHelpers,支持构建深度链接以便于导航
- 在 CoachScreen 中处理路由参数,增强用户交互体验
- 更新多个组件的样式和逻辑,提升整体用户体验
- 删除不再使用的中文回复规则文档
This commit is contained in:
richarjiang
2025-08-26 15:04:04 +08:00
parent 7f2afdf671
commit 3f89023447
13 changed files with 1113 additions and 359 deletions

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 } from 'react';
import React, { useState, useEffect } from 'react';
import {
Alert,
Image,
@@ -26,6 +26,7 @@ interface CreateGoalModalProps {
onSubmit: (goalData: CreateGoalRequest) => void;
onSuccess?: () => void;
loading?: boolean;
initialData?: Partial<CreateGoalRequest>;
}
const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
@@ -42,28 +43,49 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
onSubmit,
onSuccess,
loading = false,
initialData,
}) => {
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 [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('20:00');
const [category, setCategory] = useState('');
const [priority, setPriority] = useState<GoalPriority>(5);
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[]>([1, 2, 3, 4, 5]); // 默认周一到周五
const [selectedWeekdays, setSelectedWeekdays] = useState<number[]>(
initialData?.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]
); // 默认周一到周五
// 每月日期选择状态
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>([1, 15]); // 默认1号和15号
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>(
initialData?.customRepeatRule?.dayOfMonth || [1, 15]
); // 默认1号和15号
// 当 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]);
}
}, [initialData]);
// 重置表单
const resetForm = () => {

View File

@@ -0,0 +1,295 @@
import { Colors } from '@/constants/Colors';
import { getTemplatesByCategory, goalCategories, GoalTemplate } from '@/constants/goalTemplates';
import { useColorScheme } from '@/hooks/useColorScheme';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import React, { useState } from 'react';
import {
Dimensions,
Image,
Modal,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
const { width: screenWidth } = Dimensions.get('window');
interface GoalTemplateModalProps {
visible: boolean;
onClose: () => void;
onSelectTemplate: (template: GoalTemplate) => void;
onCreateCustom: () => void;
}
export default function GoalTemplateModal({
visible,
onClose,
onSelectTemplate,
onCreateCustom
}: GoalTemplateModalProps) {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [selectedCategory, setSelectedCategory] = useState('recommended');
const currentTemplates = getTemplatesByCategory(selectedCategory);
// 渲染分类标签
const renderCategoryTab = (category: any) => {
const isSelected = selectedCategory === category.id;
return (
<TouchableOpacity
key={category.id}
style={[
styles.categoryTab,
isSelected && styles.categoryTabSelected
]}
onPress={() => setSelectedCategory(category.id)}
activeOpacity={0.8}
>
{category.isRecommended && (
<Image
source={require('@/assets/images/icons/icon-recommend.png')}
style={{ width: 24, height: 24 }}
/>
)}
<Text style={[
styles.categoryTabText,
isSelected && styles.categoryTabTextSelected
]}>
{category.title}
</Text>
</TouchableOpacity>
);
};
// 渲染模板项
const renderTemplateItem = (template: GoalTemplate) => (
<TouchableOpacity
key={template.id}
style={[styles.templateItem]}
onPress={() => onSelectTemplate(template)}
activeOpacity={0.8}
>
<View style={[styles.templateIcon]}>
<MaterialIcons
name={template.icon as any}
size={24}
color={template.iconColor}
/>
</View>
<Text style={[styles.templateTitle]}>
{template.title}
</Text>
</TouchableOpacity>
);
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
{/* 头部 */}
<View style={styles.header}>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="keyboard-arrow-down" size={24} color="#374151" />
</TouchableOpacity>
<Text style={styles.title}></Text>
<View style={{ width: 24 }} />
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 创建自定义目标 */}
<TouchableOpacity
style={styles.customGoalButton}
onPress={onCreateCustom}
activeOpacity={0.8}
>
<View style={styles.customGoalIcon}>
<MaterialIcons name="add" size={24} color="#7C3AED" />
</View>
<Text style={styles.customGoalText}></Text>
</TouchableOpacity>
{/* 分类选择器 */}
<View style={styles.categorySection}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.categoryScrollContent}
style={styles.categoryScrollView}
>
{goalCategories.map(renderCategoryTab)}
</ScrollView>
</View>
{/* 当前分类的模板 */}
<View style={styles.templatesSection}>
<View style={styles.templatesGrid}>
{currentTemplates.map(renderTemplateItem)}
</View>
</View>
</ScrollView>
</SafeAreaView>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
closeButton: {
padding: 4,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
textAlign: 'center',
},
content: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
customGoalButton: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 20,
marginTop: 20,
padding: 16,
backgroundColor: '#FFFFFF',
borderRadius: 16,
borderWidth: 1,
borderColor: '#E5E7EB',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
customGoalIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F3E8FF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
customGoalText: {
fontSize: 16,
fontWeight: '600',
color: '#374151',
},
categorySection: {
marginTop: 24,
},
categoryScrollView: {
paddingLeft: 20,
},
categoryScrollContent: {
paddingRight: 20,
},
categoryTab: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
marginRight: 12,
borderRadius: 20,
backgroundColor: '#F3F4F6',
},
categoryTabSelected: {
backgroundColor: '#7C3AED',
},
categoryTabText: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
},
categoryTabTextSelected: {
color: '#FFFFFF',
},
avatarGroup: {
flexDirection: 'row',
marginRight: 8,
},
avatar: {
width: 16,
height: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: '#FFFFFF',
},
templatesSection: {
marginTop: 24,
},
templatesGrid: {
paddingHorizontal: 20,
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
templateItem: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
minWidth: (screenWidth - 60) / 2, // 2列布局考虑间距和padding
maxWidth: (screenWidth - 60) / 2,
aspectRatio: 2.2,
borderRadius: 16,
padding: 16,
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
templateIcon: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
templateTitle: {
fontSize: 14,
color: '#374151',
fontWeight: '600',
lineHeight: 20,
marginLeft: 12,
},
});

View File

@@ -30,7 +30,7 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
{/* 标题区域 */}
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={styles.title}></Text>
<Text style={styles.title}></Text>
<TouchableOpacity
style={styles.goalsIconButton}
onPress={handleNavigateToGoals}
@@ -39,7 +39,7 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
</TouchableOpacity>
</View>
<View style={styles.headerActions}>
{headerButtons && (
<View style={styles.headerButtons}>
{headerButtons}

View File

@@ -20,6 +20,7 @@ const MAPPING = {
'chevron.right': 'chevron-right',
'person.fill': 'person',
'person.3.fill': 'people',
'message.fill': 'message',
} as IconMapping;
/**