feat: 更新心情记录功能及相关组件
- 在心情日历中新增心情圆环展示,显示心情强度 - 修改心情记录编辑页面,支持使用图标替代表情 - 优化心情类型配置,使用图片资源替代原有表情 - 新增多种心情图标,丰富用户选择 - 更新相关样式,提升用户体验和界面美观性 - 更新文档,详细描述新功能和使用方法
@@ -39,3 +39,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- `services/`: API service layer
|
- `services/`: API service layer
|
||||||
- `store/`: Redux store and slices
|
- `store/`: Redux store and slices
|
||||||
- `types/`: TypeScript type definitions
|
- `types/`: TypeScript type definitions
|
||||||
|
|
||||||
|
|
||||||
|
## rules
|
||||||
|
- 路由跳转使用 pushIfAuthedElseLogin
|
||||||
@@ -25,14 +25,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
|
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
||||||
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
import { api, getAuthToken, postTextStream } from '@/services/api';
|
import { api, getAuthToken, postTextStream } from '@/services/api';
|
||||||
import { updateProfile } from '@/store/userSlice';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { ActionSheet } from '../../components/ui/ActionSheet';
|
import { ActionSheet } from '../../components/ui/ActionSheet';
|
||||||
@@ -1345,7 +1344,7 @@ export default function CoachScreen() {
|
|||||||
{/* 标题部分 */}
|
{/* 标题部分 */}
|
||||||
<View style={styles.dietPlanHeader}>
|
<View style={styles.dietPlanHeader}>
|
||||||
<View style={styles.dietPlanTitleContainer}>
|
<View style={styles.dietPlanTitleContainer}>
|
||||||
<Ionicons name="restaurant-outline" size={20} color={theme.success} />
|
<Ionicons name="restaurant-outline" size={20} color={theme.success} />
|
||||||
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
||||||
@@ -1774,7 +1773,7 @@ export default function CoachScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 发送饮食记录消息
|
// 发送饮食记录消息
|
||||||
const dietMsg = `记录了今日饮食:${trimmedText}`;
|
const dietMsg = `#记饮食:${trimmedText}`;
|
||||||
await sendStream(dietMsg);
|
await sendStream(dietMsg);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[DIET] 提交饮食记录失败:', e);
|
console.error('[DIET] 提交饮食记录失败:', e);
|
||||||
@@ -1865,13 +1864,6 @@ export default function CoachScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.usageCountContainer}
|
style={styles.usageCountContainer}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// 临时测试:切换VIP状态
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
dispatch(updateProfile({
|
|
||||||
isVip: !userProfile?.isVip,
|
|
||||||
freeUsageCount: userProfile?.isVip ? 3 : 5,
|
|
||||||
maxUsageCount: userProfile?.isVip ? 5 : 10
|
|
||||||
}));
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
||||||
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
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 { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
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 theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const router = useRouter();
|
|
||||||
|
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||||||
|
|
||||||
const { showConfirm } = useGlobalDialog();
|
const { showConfirm } = useGlobalDialog();
|
||||||
|
|
||||||
// Redux状态
|
// Redux状态
|
||||||
@@ -58,8 +60,11 @@ export default function GoalsScreen() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
console.log('useFocusEffect - loading tasks');
|
console.log('useFocusEffect - loading tasks');
|
||||||
loadTasks();
|
|
||||||
checkAndShowGuide();
|
if (isLoggedIn) {
|
||||||
|
loadTasks();
|
||||||
|
checkAndShowGuide();
|
||||||
|
}
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -199,7 +204,7 @@ export default function GoalsScreen() {
|
|||||||
|
|
||||||
// 导航到任务列表页面
|
// 导航到任务列表页面
|
||||||
const handleNavigateToTasks = () => {
|
const handleNavigateToTasks = () => {
|
||||||
router.push('/task-list');
|
pushIfAuthedElseLogin('/task-list');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算各状态的任务数量
|
// 计算各状态的任务数量
|
||||||
@@ -349,12 +354,12 @@ export default function GoalsScreen() {
|
|||||||
{/* 标题区域 */}
|
{/* 标题区域 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
||||||
今日目标
|
今日目标
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
||||||
让我们检查你的目标!
|
让我们检查你的目标!
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export default function ExploreScreen() {
|
|||||||
setSleepDuration(data.sleepDuration);
|
setSleepDuration(data.sleepDuration);
|
||||||
// 更新健身圆环数据
|
// 更新健身圆环数据
|
||||||
setFitnessRingsData({
|
setFitnessRingsData({
|
||||||
activeCalories: data.activeCalories,
|
activeCalories: data.activeEnergyBurned,
|
||||||
activeCaloriesGoal: data.activeCaloriesGoal,
|
activeCaloriesGoal: data.activeCaloriesGoal,
|
||||||
exerciseMinutes: data.exerciseMinutes,
|
exerciseMinutes: data.exerciseMinutes,
|
||||||
exerciseMinutesGoal: data.exerciseMinutesGoal,
|
exerciseMinutesGoal: data.exerciseMinutesGoal,
|
||||||
@@ -300,20 +300,20 @@ export default function ExploreScreen() {
|
|||||||
}, [selectedIndex])
|
}, [selectedIndex])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
// 注册任务
|
// // 注册任务
|
||||||
registerTask({
|
// registerTask({
|
||||||
id: 'health-data-task',
|
// id: 'health-data-task',
|
||||||
name: 'health-data-task',
|
// name: 'health-data-task',
|
||||||
handler: async () => {
|
// handler: async () => {
|
||||||
try {
|
// try {
|
||||||
await loadHealthData();
|
// await loadHealthData();
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('健康数据任务执行失败:', error);
|
// console.error('健康数据任务执行失败:', error);
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
// 日期点击时,加载对应日期数据
|
// 日期点击时,加载对应日期数据
|
||||||
const onSelectDate = (index: number, date: Date) => {
|
const onSelectDate = (index: number, date: Date) => {
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="background-tasks-test" options={{ headerShown: true, title: '后台任务测试' }} />
|
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useMoodData } from '@/hooks/useMoodData';
|
import { useMoodData } from '@/hooks/useMoodData';
|
||||||
import { getMoodOptions } from '@/services/moodCheckins';
|
import { getMoodOptions, MoodOption } from '@/services/moodCheckins';
|
||||||
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
|
import { Image } from 'react-native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
|
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;
|
if (!day) return null;
|
||||||
|
|
||||||
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
|
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
|
||||||
@@ -189,20 +190,40 @@ export default function MoodCalendarScreen() {
|
|||||||
const dayRecords = moodRecords[dayDateString] || [];
|
const dayRecords = moodRecords[dayDateString] || [];
|
||||||
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
|
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) {
|
if (moodRecord) {
|
||||||
const mood = moodOptions.find(m => m.type === moodRecord.moodType);
|
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 (
|
return (
|
||||||
<View style={[styles.moodIconContainer, { backgroundColor: mood?.color }]}>
|
<View style={isToday ? styles.todayMoodRingContainer : styles.moodRingContainer}>
|
||||||
<View style={styles.moodIcon}>
|
<View style={[isToday ? styles.todayMoodRing : styles.moodRing, { borderColor: color }]}>
|
||||||
<Text style={styles.moodEmoji}>{mood?.emoji || '😊'}</Text>
|
<View style={[
|
||||||
|
styles.moodRingFill,
|
||||||
|
{
|
||||||
|
backgroundColor: color,
|
||||||
|
height: `${fillRatio * 100}%`,
|
||||||
|
opacity: 0.7,
|
||||||
|
}
|
||||||
|
]} />
|
||||||
|
<Text style={[styles.moodIntensityText, { color: '#fff', fontSize: isToday ? 7 : 8 }]}>
|
||||||
|
{intensity}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.defaultMoodIcon}>
|
<View style={isToday ? styles.todayDefaultMoodRing : styles.defaultMoodRing}>
|
||||||
<Text style={styles.defaultMoodEmoji}>😊</Text>
|
<View style={isToday ? styles.todayDefaultMoodRingBorder : styles.defaultMoodRingBorder} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -285,7 +306,7 @@ export default function MoodCalendarScreen() {
|
|||||||
]}>
|
]}>
|
||||||
{day.toString().padStart(2, '0')}
|
{day.toString().padStart(2, '0')}
|
||||||
</Text>
|
</Text>
|
||||||
{renderMoodIcon(day, isSelected)}
|
{renderMoodRing(day, isSelected)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
@@ -318,9 +339,10 @@ export default function MoodCalendarScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.recordIcon}>
|
<View style={styles.recordIcon}>
|
||||||
<View style={styles.moodIcon}>
|
<View style={styles.moodIcon}>
|
||||||
<Text style={styles.moodEmoji}>
|
<Image
|
||||||
{moodOptions.find(m => m.type === selectedDateMood.moodType)?.emoji || '😊'}
|
source={moodOptions.find(m => m.type === selectedDateMood.moodType)?.image}
|
||||||
</Text>
|
style={styles.moodIconImage}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.recordContent}>
|
<View style={styles.recordContent}>
|
||||||
@@ -524,8 +546,10 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
moodEmoji: {
|
moodIconImage: {
|
||||||
fontSize: 11,
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
},
|
},
|
||||||
defaultMoodIcon: {
|
defaultMoodIcon: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -545,6 +569,104 @@ const styles = StyleSheet.create({
|
|||||||
opacity: 0.4,
|
opacity: 0.4,
|
||||||
color: '#7a5af8',
|
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: {
|
selectedDateSection: {
|
||||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ import {
|
|||||||
selectMoodRecordsByDate,
|
selectMoodRecordsByDate,
|
||||||
updateMoodRecord
|
updateMoodRecord
|
||||||
} from '@/store/moodSlice';
|
} from '@/store/moodSlice';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert, Image,
|
||||||
SafeAreaView,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function MoodEditScreen() {
|
export default function MoodEditScreen() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
@@ -153,7 +154,7 @@ export default function MoodEditScreen() {
|
|||||||
{/* 装饰性圆圈 */}
|
{/* 装饰性圆圈 */}
|
||||||
<View style={styles.decorativeCircle1} />
|
<View style={styles.decorativeCircle1} />
|
||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title={existingMood ? '编辑心情' : '记录心情'}
|
title={existingMood ? '编辑心情' : '记录心情'}
|
||||||
onBack={() => router.back()}
|
onBack={() => router.back()}
|
||||||
@@ -183,7 +184,7 @@ export default function MoodEditScreen() {
|
|||||||
]}
|
]}
|
||||||
onPress={() => setSelectedMood(mood.type)}
|
onPress={() => setSelectedMood(mood.type)}
|
||||||
>
|
>
|
||||||
<Text style={styles.moodEmoji}>{mood.emoji}</Text>
|
<Image source={mood.image} style={styles.moodImage} />
|
||||||
<Text style={styles.moodLabel}>{mood.label}</Text>
|
<Text style={styles.moodLabel}>{mood.label}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -224,26 +225,27 @@ export default function MoodEditScreen() {
|
|||||||
|
|
||||||
{/* 底部按钮 */}
|
{/* 底部按钮 */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
{existingMood && (
|
<View style={styles.buttonRow}>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.deleteButton, isDeleting && styles.disabledButton]}
|
style={[styles.saveButton, (!selectedMood || isLoading) && styles.disabledButton]}
|
||||||
onPress={handleDelete}
|
onPress={handleSave}
|
||||||
disabled={isDeleting}
|
disabled={!selectedMood || isLoading}
|
||||||
>
|
>
|
||||||
<Text style={styles.deleteButtonText}>
|
<Text style={styles.saveButtonText}>
|
||||||
{isDeleting ? '删除中...' : '删除记录'}
|
{isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
{existingMood && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.saveButton, (!selectedMood || isLoading) && styles.disabledButton]}
|
style={[styles.deleteIconButton, isDeleting && styles.disabledButton]}
|
||||||
onPress={handleSave}
|
onPress={handleDelete}
|
||||||
disabled={!selectedMood || isLoading}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
<Text style={styles.saveButtonText}>
|
<Ionicons name="trash-outline" size={24} color="#f95555" />
|
||||||
{isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'}
|
</TouchableOpacity>
|
||||||
</Text>
|
)}
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
@@ -329,10 +331,10 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
moodOption: {
|
moodOption: {
|
||||||
width: '30%',
|
width: '18%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 20,
|
paddingVertical: 16,
|
||||||
marginBottom: 16,
|
marginBottom: 12,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
backgroundColor: 'rgba(122,90,248,0.05)',
|
backgroundColor: 'rgba(122,90,248,0.05)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -348,8 +350,9 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
moodEmoji: {
|
moodImage: {
|
||||||
fontSize: 28,
|
width: 40,
|
||||||
|
height: 40,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
moodLabel: {
|
moodLabel: {
|
||||||
@@ -401,30 +404,42 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: 20,
|
padding: 16,
|
||||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
position: 'absolute',
|
||||||
shadowColor: '#7a5af8',
|
bottom: 24,
|
||||||
shadowOffset: { width: 0, height: -4 },
|
right: 8,
|
||||||
shadowOpacity: 0.1,
|
},
|
||||||
shadowRadius: 12,
|
buttonRow: {
|
||||||
elevation: 6,
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
backgroundColor: '#7a5af8',
|
backgroundColor: '#7a5af8',
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
paddingVertical: 18,
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 24,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 12,
|
marginLeft: 12,
|
||||||
shadowColor: '#7a5af8',
|
shadowColor: '#7a5af8',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
|
deleteIconButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
backgroundColor: '#f95555',
|
backgroundColor: '#f95555',
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
paddingVertical: 18,
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 24,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
shadowColor: '#f95555',
|
shadowColor: '#f95555',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
@@ -439,12 +454,12 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
saveButtonText: {
|
saveButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
deleteButtonText: {
|
deleteButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
assets/images/icons/mood/jiaolv.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/icons/mood/kaixin.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/images/icons/mood/nanguo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/images/icons/mood/pingjing.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/images/icons/mood/shengqi.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/images/icons/mood/weiqu.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/icons/mood/xindong.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/icons/mood/xingfen.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/images/icons/mood/xinlei.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -1,17 +1,5 @@
|
|||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
|
|
||||||
// 心情类型定义
|
|
||||||
export type MoodType =
|
|
||||||
| 'happy' // 开心
|
|
||||||
| 'excited' // 心动
|
|
||||||
| 'thrilled' // 兴奋
|
|
||||||
| 'calm' // 平静
|
|
||||||
| 'anxious' // 焦虑
|
|
||||||
| 'sad' // 难过
|
|
||||||
| 'lonely' // 孤独
|
|
||||||
| 'wronged' // 委屈
|
|
||||||
| 'angry' // 生气
|
|
||||||
| 'tired'; // 心累
|
|
||||||
|
|
||||||
// 心情打卡记录类型
|
// 心情打卡记录类型
|
||||||
export type MoodCheckin = {
|
export type MoodCheckin = {
|
||||||
@@ -115,25 +103,55 @@ export async function getMoodStatistics(params: {
|
|||||||
|
|
||||||
// 心情类型配置
|
// 心情类型配置
|
||||||
export const MOOD_CONFIG = {
|
export const MOOD_CONFIG = {
|
||||||
happy: { emoji: '😊', label: '开心', color: '#4CAF50' },
|
happy: { image: require('@/assets/images/icons/mood/kaixin.png'), label: '开心', color: '#4CAF50' },
|
||||||
excited: { emoji: '💓', label: '心动', color: '#E91E63' },
|
excited: { image: require('@/assets/images/icons/mood/xindong.png'), label: '心动', color: '#E91E63' },
|
||||||
thrilled: { emoji: '🤩', label: '兴奋', color: '#FF9800' },
|
thrilled: { image: require('@/assets/images/icons/mood/xingfen.png'), label: '兴奋', color: '#FF9800' },
|
||||||
calm: { emoji: '😌', label: '平静', color: '#2196F3' },
|
calm: { image: require('@/assets/images/icons/mood/pingjing.png'), label: '平静', color: '#2196F3' },
|
||||||
anxious: { emoji: '😰', label: '焦虑', color: '#FF9800' },
|
anxious: { image: require('@/assets/images/icons/mood/jiaolv.png'), label: '焦虑', color: '#FF9800' },
|
||||||
sad: { emoji: '😢', label: '难过', color: '#2196F3' },
|
sad: { image: require('@/assets/images/icons/mood/nanguo.png'), label: '难过', color: '#2196F3' },
|
||||||
lonely: { emoji: '🥺', label: '孤独', color: '#9C27B0' },
|
lonely: { image: require('@/assets/images/icons/mood/weiqu.png'), label: '孤独', color: '#9C27B0' },
|
||||||
wronged: { emoji: '😔', label: '委屈', color: '#607D8B' },
|
wronged: { image: require('@/assets/images/icons/mood/weiqu.png'), label: '委屈', color: '#607D8B' },
|
||||||
angry: { emoji: '😡', label: '生气', color: '#F44336' },
|
angry: { image: require('@/assets/images/icons/mood/shengqi.png'), label: '生气', color: '#F44336' },
|
||||||
tired: { emoji: '😴', label: '心累', color: '#9C27B0' },
|
tired: { image: require('@/assets/images/icons/mood/xinlei.png'), label: '心累', color: '#9C27B0' },
|
||||||
} as const;
|
} 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) {
|
export function getMoodConfig(moodType: MoodType) {
|
||||||
return MOOD_CONFIG[moodType];
|
return MOOD_CONFIG[moodType];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有心情选项
|
// 获取所有心情选项
|
||||||
export function getMoodOptions() {
|
export function getMoodOptions(): MoodOption[] {
|
||||||
return Object.entries(MOOD_CONFIG).map(([type, config]) => ({
|
return Object.entries(MOOD_CONFIG).map(([type, config]) => ({
|
||||||
type: type as MoodType,
|
type: type as MoodType,
|
||||||
...config,
|
...config,
|
||||||
|
|||||||