feat: 更新心情记录功能及相关组件

- 在心情日历中新增心情圆环展示,显示心情强度
- 修改心情记录编辑页面,支持使用图标替代表情
- 优化心情类型配置,使用图片资源替代原有表情
- 新增多种心情图标,丰富用户选择
- 更新相关样式,提升用户体验和界面美观性
- 更新文档,详细描述新功能和使用方法
This commit is contained in:
richarjiang
2025-08-25 09:33:54 +08:00
parent 23aa15f76e
commit 4f2d47c23f
17 changed files with 298 additions and 144 deletions

View File

@@ -38,4 +38,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `hooks/`: Custom React hooks - `hooks/`: Custom React hooks
- `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

View File

@@ -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';
@@ -284,9 +283,9 @@ export default function CoachScreen() {
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() }, { key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() }, { key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() }, { key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
{ {
key: 'mood', key: 'mood',
label: '#记心情', label: '#记心情',
action: () => { action: () => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
@@ -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
@@ -1960,15 +1952,15 @@ export default function CoachScreen() {
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }} contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
> >
{chips.map((c) => ( {chips.map((c) => (
<TouchableOpacity <TouchableOpacity
key={c.key} key={c.key}
style={[ style={[
styles.chip, styles.chip,
{ {
borderColor: c.key === 'mood' ? `${theme.success}40` : `${theme.primary}40`, borderColor: c.key === 'mood' ? `${theme.success}40` : `${theme.primary}40`,
backgroundColor: c.key === 'mood' ? `${theme.success}15` : `${theme.primary}15` backgroundColor: c.key === 'mood' ? `${theme.success}15` : `${theme.primary}15`
} }
]} ]}
onPress={c.action} onPress={c.action}
> >
<Text style={[styles.chipText, { color: c.key === 'mood' ? theme.success : theme.text }]}>{c.label}</Text> <Text style={[styles.chipText, { color: c.key === 'mood' ? theme.success : theme.text }]}>{c.label}</Text>

View File

@@ -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])
); );
@@ -147,10 +152,10 @@ export default function GoalsScreen() {
try { try {
await dispatch(createGoal(goalData)).unwrap(); await dispatch(createGoal(goalData)).unwrap();
setShowCreateModal(false); setShowCreateModal(false);
// 获取用户名 // 获取用户名
const userName = userProfile?.name || '小海豹'; const userName = userProfile?.name || '小海豹';
// 创建目标成功后,设置定时推送 // 创建目标成功后,设置定时推送
try { try {
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
@@ -165,13 +170,13 @@ export default function GoalsScreen() {
}, },
userName userName
); );
console.log(`目标"${goalData.title}"的定时推送已创建通知ID`, notificationIds); console.log(`目标"${goalData.title}"的定时推送已创建通知ID`, notificationIds);
} catch (notificationError) { } catch (notificationError) {
console.error('创建目标定时推送失败:', notificationError); console.error('创建目标定时推送失败:', notificationError);
// 通知创建失败不影响目标创建的成功 // 通知创建失败不影响目标创建的成功
} }
// 使用确认弹窗显示成功消息 // 使用确认弹窗显示成功消息
showConfirm( showConfirm(
{ {
@@ -187,7 +192,7 @@ export default function GoalsScreen() {
console.log('用户确认了目标创建成功'); console.log('用户确认了目标创建成功');
} }
); );
// 创建目标后重新加载任务列表 // 创建目标后重新加载任务列表
loadTasks(); loadTasks();
} catch (error) { } catch (error) {
@@ -199,7 +204,7 @@ export default function GoalsScreen() {
// 导航到任务列表页面 // 导航到任务列表页面
const handleNavigateToTasks = () => { const handleNavigateToTasks = () => {
router.push('/task-list'); pushIfAuthedElseLogin('/task-list');
}; };
// 计算各状态的任务数量 // 计算各状态的任务数量
@@ -213,7 +218,7 @@ export default function GoalsScreen() {
// 根据筛选条件过滤任务,并将已完成的任务放到最后 // 根据筛选条件过滤任务,并将已完成的任务放到最后
const filteredTasks = React.useMemo(() => { const filteredTasks = React.useMemo(() => {
let filtered: TaskListItem[] = []; let filtered: TaskListItem[] = [];
switch (selectedFilter) { switch (selectedFilter) {
case 'pending': case 'pending':
filtered = tasks.filter(task => task.status === 'pending'); filtered = tasks.filter(task => task.status === 'pending');
@@ -228,7 +233,7 @@ export default function GoalsScreen() {
filtered = tasks; filtered = tasks;
break; break;
} }
// 对所有筛选结果进行排序:已完成的任务放到最后 // 对所有筛选结果进行排序:已完成的任务放到最后
return [...filtered].sort((a, b) => { return [...filtered].sort((a, b) => {
// 如果a已完成而b未完成a排在后面 // 如果a已完成而b未完成a排在后面
@@ -271,7 +276,7 @@ export default function GoalsScreen() {
const renderEmptyState = () => { const renderEmptyState = () => {
let title = '暂无任务'; let title = '暂无任务';
let subtitle = '创建目标后,系统会自动生成相应的任务'; let subtitle = '创建目标后,系统会自动生成相应的任务';
if (selectedFilter === 'pending') { if (selectedFilter === 'pending') {
title = '暂无待完成的任务'; title = '暂无待完成的任务';
subtitle = '当前没有待完成的任务'; subtitle = '当前没有待完成的任务';
@@ -282,7 +287,7 @@ export default function GoalsScreen() {
title = '暂无已跳过的任务'; title = '暂无已跳过的任务';
subtitle = '跳过一些任务后,它们会显示在这里'; subtitle = '跳过一些任务后,它们会显示在这里';
} }
return ( return (
<View style={styles.emptyState}> <View style={styles.emptyState}>
<Image <Image
@@ -349,19 +354,19 @@ 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>
{/* 任务进度卡片 */} {/* 任务进度卡片 */}
<View > <View >
<TaskProgressCard <TaskProgressCard
tasks={tasks} tasks={tasks}
headerButtons={ headerButtons={
<View style={styles.cardHeaderButtons}> <View style={styles.cardHeaderButtons}>
<TouchableOpacity <TouchableOpacity
@@ -436,7 +441,7 @@ export default function GoalsScreen() {
{/* 开发测试按钮 */} {/* 开发测试按钮 */}
<GuideTestButton visible={__DEV__} /> <GuideTestButton visible={__DEV__} />
{/* 目标通知测试按钮 */} {/* 目标通知测试按钮 */}
{__DEV__ && ( {__DEV__ && (
<TouchableOpacity <TouchableOpacity

View File

@@ -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,
@@ -231,7 +231,7 @@ export default function ExploreScreen() {
// 设置血氧饱和度和心率数据 // 设置血氧饱和度和心率数据
setOxygenSaturation(data.oxygenSaturation ?? null); setOxygenSaturation(data.oxygenSaturation ?? null);
setHeartRate(data.heartRate ?? null); setHeartRate(data.heartRate ?? null);
console.log('血氧饱和度数据:', data.oxygenSaturation); console.log('血氧饱和度数据:', data.oxygenSaturation);
console.log('心率数据:', data.heartRate); console.log('心率数据:', data.heartRate);
@@ -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) => {
@@ -474,8 +474,8 @@ export default function ExploreScreen() {
oxygenSaturation={oxygenSaturation} oxygenSaturation={oxygenSaturation}
/> />
{/* 测试按钮 - 开发时使用 */} {/* 测试按钮 - 开发时使用 */}
<Text <Text
style={styles.testButton} style={styles.testButton}
onPress={testOxygenData} onPress={testOxygenData}
> >

View File

@@ -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" />

View File

@@ -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,

View File

@@ -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';
@@ -149,11 +150,11 @@ export default function MoodEditScreen() {
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }} end={{ x: 0, y: 1 }}
/> />
{/* 装饰性圆圈 */} {/* 装饰性圆圈 */}
<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',
}, },
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -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,