diff --git a/CLAUDE.md b/CLAUDE.md index e438691..d9da12d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,4 +38,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `hooks/`: Custom React hooks - `services/`: API service layer - `store/`: Redux store and slices -- `types/`: TypeScript type definitions \ No newline at end of file +- `types/`: TypeScript type definitions + + +## rules +- 路由跳转使用 pushIfAuthedElseLogin \ No newline at end of file diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index e6356b8..57217ee 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -25,14 +25,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; -import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach'; import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession'; import { api, getAuthToken, postTextStream } from '@/services/api'; -import { updateProfile } from '@/store/userSlice'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { ActionSheet } from '../../components/ui/ActionSheet'; @@ -284,9 +283,9 @@ export default function CoachScreen() { { key: 'weight', label: '#记体重', action: () => insertWeightInputCard() }, { key: 'diet', label: '#记饮食', action: () => insertDietInputCard() }, { key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() }, - { - key: 'mood', - label: '#记心情', + { + key: 'mood', + label: '#记心情', action: () => { if (Platform.OS === 'ios') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); @@ -1345,7 +1344,7 @@ export default function CoachScreen() { {/* 标题部分 */} - + 我的饮食方案 MY DIET PLAN @@ -1774,7 +1773,7 @@ export default function CoachScreen() { }); // 发送饮食记录消息 - const dietMsg = `记录了今日饮食:${trimmedText}`; + const dietMsg = `#记饮食:${trimmedText}`; await sendStream(dietMsg); } catch (e: any) { console.error('[DIET] 提交饮食记录失败:', e); @@ -1865,13 +1864,6 @@ export default function CoachScreen() { { - // 临时测试:切换VIP状态 - const dispatch = useAppDispatch(); - dispatch(updateProfile({ - isVip: !userProfile?.isVip, - freeUsageCount: userProfile?.isVip ? 3 : 5, - maxUsageCount: userProfile?.isVip ? 5 : 10 - })); }} > {chips.map((c) => ( - {c.label} diff --git a/app/(tabs)/goals.tsx b/app/(tabs)/goals.tsx index c21a546..60e48da 100644 --- a/app/(tabs)/goals.tsx +++ b/app/(tabs)/goals.tsx @@ -8,6 +8,7 @@ import { useGlobalDialog } from '@/components/ui/DialogProvider'; import { Colors } from '@/constants/Colors'; import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { clearErrors, createGoal } from '@/store/goalsSlice'; import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice'; @@ -18,7 +19,6 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; -import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -26,7 +26,9 @@ export default function GoalsScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); - const router = useRouter(); + + const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); + const { showConfirm } = useGlobalDialog(); // Redux状态 @@ -58,8 +60,11 @@ export default function GoalsScreen() { useFocusEffect( useCallback(() => { console.log('useFocusEffect - loading tasks'); - loadTasks(); - checkAndShowGuide(); + + if (isLoggedIn) { + loadTasks(); + checkAndShowGuide(); + } }, [dispatch]) ); @@ -147,10 +152,10 @@ export default function GoalsScreen() { try { await dispatch(createGoal(goalData)).unwrap(); setShowCreateModal(false); - + // 获取用户名 const userName = userProfile?.name || '小海豹'; - + // 创建目标成功后,设置定时推送 try { const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( @@ -165,13 +170,13 @@ export default function GoalsScreen() { }, userName ); - + console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds); } catch (notificationError) { console.error('创建目标定时推送失败:', notificationError); // 通知创建失败不影响目标创建的成功 } - + // 使用确认弹窗显示成功消息 showConfirm( { @@ -187,7 +192,7 @@ export default function GoalsScreen() { console.log('用户确认了目标创建成功'); } ); - + // 创建目标后重新加载任务列表 loadTasks(); } catch (error) { @@ -199,7 +204,7 @@ export default function GoalsScreen() { // 导航到任务列表页面 const handleNavigateToTasks = () => { - router.push('/task-list'); + pushIfAuthedElseLogin('/task-list'); }; // 计算各状态的任务数量 @@ -213,7 +218,7 @@ export default function GoalsScreen() { // 根据筛选条件过滤任务,并将已完成的任务放到最后 const filteredTasks = React.useMemo(() => { let filtered: TaskListItem[] = []; - + switch (selectedFilter) { case 'pending': filtered = tasks.filter(task => task.status === 'pending'); @@ -228,7 +233,7 @@ export default function GoalsScreen() { filtered = tasks; break; } - + // 对所有筛选结果进行排序:已完成的任务放到最后 return [...filtered].sort((a, b) => { // 如果a已完成而b未完成,a排在后面 @@ -271,7 +276,7 @@ export default function GoalsScreen() { const renderEmptyState = () => { let title = '暂无任务'; let subtitle = '创建目标后,系统会自动生成相应的任务'; - + if (selectedFilter === 'pending') { title = '暂无待完成的任务'; subtitle = '当前没有待完成的任务'; @@ -282,7 +287,7 @@ export default function GoalsScreen() { title = '暂无已跳过的任务'; subtitle = '跳过一些任务后,它们会显示在这里'; } - + return ( - - 今日目标 - - - 让我们检查你的目标! - + + 今日目标 + + + 让我们检查你的目标! + {/* 任务进度卡片 */} - - + {/* 目标通知测试按钮 */} {__DEV__ && ( { - // 注册任务 - registerTask({ - id: 'health-data-task', - name: 'health-data-task', - handler: async () => { - try { - await loadHealthData(); - } catch (error) { - console.error('健康数据任务执行失败:', error); - } - }, - }); - }, []); + // useEffect(() => { + // // 注册任务 + // registerTask({ + // id: 'health-data-task', + // name: 'health-data-task', + // handler: async () => { + // try { + // await loadHealthData(); + // } catch (error) { + // console.error('健康数据任务执行失败:', error); + // } + // }, + // }); + // }, []); // 日期点击时,加载对应日期数据 const onSelectDate = (index: number, date: Date) => { @@ -474,8 +474,8 @@ export default function ExploreScreen() { oxygenSaturation={oxygenSaturation} /> {/* 测试按钮 - 开发时使用 */} - 测试血氧数据 diff --git a/app/_layout.tsx b/app/_layout.tsx index b060fce..7cc00e5 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -106,8 +106,6 @@ export default function RootLayout() { - - diff --git a/app/mood/calendar.tsx b/app/mood/calendar.tsx index 93a2025..1958040 100644 --- a/app/mood/calendar.tsx +++ b/app/mood/calendar.tsx @@ -3,8 +3,9 @@ import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useMoodData } from '@/hooks/useMoodData'; -import { getMoodOptions } from '@/services/moodCheckins'; +import { getMoodOptions, MoodOption } from '@/services/moodCheckins'; import { selectLatestMoodRecordByDate } from '@/store/moodSlice'; +import { Image } from 'react-native'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useFocusEffect, useLocalSearchParams } from 'expo-router'; @@ -181,7 +182,7 @@ export default function MoodCalendarScreen() { }); }; - const renderMoodIcon = (day: number | null, isSelected: boolean) => { + const renderMoodRing = (day: number | null, isSelected: boolean) => { if (!day) return null; // 检查该日期是否有心情记录 - 现在从 Redux store 中获取 @@ -189,20 +190,40 @@ export default function MoodCalendarScreen() { const dayRecords = moodRecords[dayDateString] || []; const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null; + const isToday = day === new Date().getDate() && + month === new Date().getMonth() + 1 && + year === new Date().getFullYear(); + if (moodRecord) { const mood = moodOptions.find(m => m.type === moodRecord.moodType); + const intensity = moodRecord.intensity; + const color = mood?.color || '#7a5af8'; + + // 计算圆环的填充比例 (0-1) + const fillRatio = intensity / 10; + return ( - - - {mood?.emoji || '😊'} + + + + + {intensity} + ); } return ( - - 😊 + + ); }; @@ -285,7 +306,7 @@ export default function MoodCalendarScreen() { ]}> {day.toString().padStart(2, '0')} - {renderMoodIcon(day, isSelected)} + {renderMoodRing(day, isSelected)} )} @@ -318,9 +339,10 @@ export default function MoodCalendarScreen() { > - - {moodOptions.find(m => m.type === selectedDateMood.moodType)?.emoji || '😊'} - + m.type === selectedDateMood.moodType)?.image} + style={styles.moodIconImage} + /> @@ -524,8 +546,10 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - moodEmoji: { - fontSize: 11, + moodIconImage: { + width: 18, + height: 18, + borderRadius: 9, }, defaultMoodIcon: { position: 'absolute', @@ -545,6 +569,104 @@ const styles = StyleSheet.create({ opacity: 0.4, color: '#7a5af8', }, + moodRingContainer: { + position: 'absolute', + bottom: 2, + width: 22, + height: 22, + justifyContent: 'center', + alignItems: 'center', + }, + moodRing: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 1.5, + justifyContent: 'flex-end', + alignItems: 'center', + overflow: 'hidden', + backgroundColor: 'rgba(255,255,255,0.95)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 1, + }, + moodRingFill: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + moodIntensityText: { + fontSize: 8, + fontWeight: '800', + textAlign: 'center', + position: 'absolute', + zIndex: 1, + textShadowColor: 'rgba(0,0,0,0.3)', + textShadowOffset: { width: 0, height: 0.5 }, + textShadowRadius: 1, + }, + defaultMoodRing: { + position: 'absolute', + bottom: 2, + width: 22, + height: 22, + justifyContent: 'center', + alignItems: 'center', + }, + defaultMoodRingBorder: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 1.5, + borderColor: 'rgba(122,90,248,0.3)', + borderStyle: 'dashed', + backgroundColor: 'rgba(122,90,248,0.05)', + }, + todayMoodRingContainer: { + position: 'absolute', + bottom: 1, + width: 20, + height: 20, + justifyContent: 'center', + alignItems: 'center', + }, + todayMoodRing: { + width: 18, + height: 18, + borderRadius: 9, + borderWidth: 1.5, + justifyContent: 'flex-end', + alignItems: 'center', + overflow: 'hidden', + backgroundColor: 'rgba(255,255,255,0.95)', + shadowColor: '#7a5af8', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 2, + elevation: 2, + }, + todayDefaultMoodRing: { + position: 'absolute', + bottom: 1, + width: 20, + height: 20, + justifyContent: 'center', + alignItems: 'center', + }, + todayDefaultMoodRingBorder: { + width: 18, + height: 18, + borderRadius: 9, + borderWidth: 1.5, + borderColor: 'rgba(122,90,248,0.4)', + borderStyle: 'dashed', + backgroundColor: 'rgba(122,90,248,0.08)', + }, selectedDateSection: { backgroundColor: 'rgba(255,255,255,0.95)', margin: 16, diff --git a/app/mood/edit.tsx b/app/mood/edit.tsx index ae6cc92..9ca2eba 100644 --- a/app/mood/edit.tsx +++ b/app/mood/edit.tsx @@ -11,20 +11,21 @@ import { selectMoodRecordsByDate, updateMoodRecord } from '@/store/moodSlice'; +import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { - Alert, - SafeAreaView, + Alert, Image, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, - View, + View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; export default function MoodEditScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; @@ -149,11 +150,11 @@ export default function MoodEditScreen() { start={{ x: 0, y: 0 }} end={{ x: 0, y: 1 }} /> - + {/* 装饰性圆圈 */} - + router.back()} @@ -183,7 +184,7 @@ export default function MoodEditScreen() { ]} onPress={() => setSelectedMood(mood.type)} > - {mood.emoji} + {mood.label} ))} @@ -224,26 +225,27 @@ export default function MoodEditScreen() { {/* 底部按钮 */} - {existingMood && ( + + - - {isDeleting ? '删除中...' : '删除记录'} + + {isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'} - )} - - - {isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'} - - + {existingMood && ( + + + + )} + @@ -329,10 +331,10 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', }, moodOption: { - width: '30%', + width: '18%', alignItems: 'center', - paddingVertical: 20, - marginBottom: 16, + paddingVertical: 16, + marginBottom: 12, borderRadius: 16, backgroundColor: 'rgba(122,90,248,0.05)', borderWidth: 1, @@ -348,8 +350,9 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 2, }, - moodEmoji: { - fontSize: 28, + moodImage: { + width: 40, + height: 40, marginBottom: 10, }, moodLabel: { @@ -401,30 +404,42 @@ const styles = StyleSheet.create({ fontWeight: '500', }, footer: { - padding: 20, - backgroundColor: 'rgba(255,255,255,0.95)', - shadowColor: '#7a5af8', - shadowOffset: { width: 0, height: -4 }, - shadowOpacity: 0.1, - shadowRadius: 12, - elevation: 6, + padding: 16, + position: 'absolute', + bottom: 24, + right: 8, + }, + buttonRow: { + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', }, saveButton: { backgroundColor: '#7a5af8', - borderRadius: 16, - paddingVertical: 18, + borderRadius: 12, + paddingVertical: 12, + paddingHorizontal: 24, alignItems: 'center', - marginTop: 12, + marginLeft: 12, shadowColor: '#7a5af8', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, + deleteIconButton: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + marginLeft: 12, + }, deleteButton: { backgroundColor: '#f95555', - borderRadius: 16, - paddingVertical: 18, + borderRadius: 12, + paddingVertical: 12, + paddingHorizontal: 24, alignItems: 'center', shadowColor: '#f95555', shadowOffset: { width: 0, height: 2 }, @@ -439,12 +454,12 @@ const styles = StyleSheet.create({ }, saveButtonText: { color: '#fff', - fontSize: 16, - fontWeight: '700', + fontSize: 14, + fontWeight: '600', }, deleteButtonText: { color: '#fff', - fontSize: 16, - fontWeight: '700', + fontSize: 14, + fontWeight: '600', }, }); diff --git a/assets/images/icons/mood/jiaolv.png b/assets/images/icons/mood/jiaolv.png new file mode 100644 index 0000000..ea937f9 Binary files /dev/null and b/assets/images/icons/mood/jiaolv.png differ diff --git a/assets/images/icons/mood/kaixin.png b/assets/images/icons/mood/kaixin.png new file mode 100644 index 0000000..57bc2bd Binary files /dev/null and b/assets/images/icons/mood/kaixin.png differ diff --git a/assets/images/icons/mood/nanguo.png b/assets/images/icons/mood/nanguo.png new file mode 100644 index 0000000..a55626e Binary files /dev/null and b/assets/images/icons/mood/nanguo.png differ diff --git a/assets/images/icons/mood/pingjing.png b/assets/images/icons/mood/pingjing.png new file mode 100644 index 0000000..99ae912 Binary files /dev/null and b/assets/images/icons/mood/pingjing.png differ diff --git a/assets/images/icons/mood/shengqi.png b/assets/images/icons/mood/shengqi.png new file mode 100644 index 0000000..60d5236 Binary files /dev/null and b/assets/images/icons/mood/shengqi.png differ diff --git a/assets/images/icons/mood/weiqu.png b/assets/images/icons/mood/weiqu.png new file mode 100644 index 0000000..f23c9af Binary files /dev/null and b/assets/images/icons/mood/weiqu.png differ diff --git a/assets/images/icons/mood/xindong.png b/assets/images/icons/mood/xindong.png new file mode 100644 index 0000000..d906c71 Binary files /dev/null and b/assets/images/icons/mood/xindong.png differ diff --git a/assets/images/icons/mood/xingfen.png b/assets/images/icons/mood/xingfen.png new file mode 100644 index 0000000..c06172c Binary files /dev/null and b/assets/images/icons/mood/xingfen.png differ diff --git a/assets/images/icons/mood/xinlei.png b/assets/images/icons/mood/xinlei.png new file mode 100644 index 0000000..e38946f Binary files /dev/null and b/assets/images/icons/mood/xinlei.png differ diff --git a/services/moodCheckins.ts b/services/moodCheckins.ts index c751576..d50708b 100644 --- a/services/moodCheckins.ts +++ b/services/moodCheckins.ts @@ -1,17 +1,5 @@ import { api } from './api'; -// 心情类型定义 -export type MoodType = - | 'happy' // 开心 - | 'excited' // 心动 - | 'thrilled' // 兴奋 - | 'calm' // 平静 - | 'anxious' // 焦虑 - | 'sad' // 难过 - | 'lonely' // 孤独 - | 'wronged' // 委屈 - | 'angry' // 生气 - | 'tired'; // 心累 // 心情打卡记录类型 export type MoodCheckin = { @@ -115,25 +103,55 @@ export async function getMoodStatistics(params: { // 心情类型配置 export const MOOD_CONFIG = { - happy: { emoji: '😊', label: '开心', color: '#4CAF50' }, - excited: { emoji: '💓', label: '心动', color: '#E91E63' }, - thrilled: { emoji: '🤩', label: '兴奋', color: '#FF9800' }, - calm: { emoji: '😌', label: '平静', color: '#2196F3' }, - anxious: { emoji: '😰', label: '焦虑', color: '#FF9800' }, - sad: { emoji: '😢', label: '难过', color: '#2196F3' }, - lonely: { emoji: '🥺', label: '孤独', color: '#9C27B0' }, - wronged: { emoji: '😔', label: '委屈', color: '#607D8B' }, - angry: { emoji: '😡', label: '生气', color: '#F44336' }, - tired: { emoji: '😴', label: '心累', color: '#9C27B0' }, + happy: { image: require('@/assets/images/icons/mood/kaixin.png'), label: '开心', color: '#4CAF50' }, + excited: { image: require('@/assets/images/icons/mood/xindong.png'), label: '心动', color: '#E91E63' }, + thrilled: { image: require('@/assets/images/icons/mood/xingfen.png'), label: '兴奋', color: '#FF9800' }, + calm: { image: require('@/assets/images/icons/mood/pingjing.png'), label: '平静', color: '#2196F3' }, + anxious: { image: require('@/assets/images/icons/mood/jiaolv.png'), label: '焦虑', color: '#FF9800' }, + sad: { image: require('@/assets/images/icons/mood/nanguo.png'), label: '难过', color: '#2196F3' }, + lonely: { image: require('@/assets/images/icons/mood/weiqu.png'), label: '孤独', color: '#9C27B0' }, + wronged: { image: require('@/assets/images/icons/mood/weiqu.png'), label: '委屈', color: '#607D8B' }, + angry: { image: require('@/assets/images/icons/mood/shengqi.png'), label: '生气', color: '#F44336' }, + tired: { image: require('@/assets/images/icons/mood/xinlei.png'), label: '心累', color: '#9C27B0' }, } as const; +// 心情类型定义 +export type MoodType = + | 'happy' // 开心 + | 'excited' // 心动 + | 'thrilled' // 兴奋 + | 'calm' // 平静 + | 'anxious' // 焦虑 + | 'sad' // 难过 + | 'lonely' // 孤独 + | 'wronged' // 委屈 + | 'angry' // 生气 + | 'tired'; // 心累 + +// 心情配置类型 +export type MoodConfig = { + [K in MoodType]: { + image: any; + label: string; + color: string; + } +}; + +// 单个心情选项类型 +export type MoodOption = { + type: MoodType; + image: any; + label: string; + color: string; +}; + // 获取心情配置 export function getMoodConfig(moodType: MoodType) { return MOOD_CONFIG[moodType]; } // 获取所有心情选项 -export function getMoodOptions() { +export function getMoodOptions(): MoodOption[] { return Object.entries(MOOD_CONFIG).map(([type, config]) => ({ type: type as MoodType, ...config,