From a7607e0f742e79f3eac98eaf5b9d751e026d8071 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 21 Aug 2025 15:34:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BF=83=E6=83=85?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在统计页面新增心情卡片和弹窗组件,支持用户记录和查看每日心情状态。 --- app/(tabs)/statistics.tsx | 105 +++++++- components/MoodCard.tsx | 76 ++++++ components/MoodCardCompact.tsx | 63 +++++ components/MoodModal.tsx | 422 +++++++++++++++++++++++++++++++++ 4 files changed, 656 insertions(+), 10 deletions(-) create mode 100644 components/MoodCard.tsx create mode 100644 components/MoodCardCompact.tsx create mode 100644 components/MoodModal.tsx diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index a26c739..ea3016f 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -1,6 +1,7 @@ import { AnimatedNumber } from '@/components/AnimatedNumber'; import { BMICard } from '@/components/BMICard'; import { FitnessRingsCard } from '@/components/FitnessRingsCard'; +import { MoodModal } from '@/components/MoodModal'; import { NutritionRadarCard } from '@/components/NutritionRadarCard'; import { ProgressBar } from '@/components/ProgressBar'; import { StressMeter } from '@/components/StressMeter'; @@ -88,7 +89,7 @@ export default function ExploreScreen() { const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); - // 使用 dayjs:当月日期与默认选中“今天” + // 使用 dayjs:当月日期与默认选中"今天" const days = getMonthDaysZh(); const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); const tabBarHeight = useBottomTabBarHeight(); @@ -161,11 +162,29 @@ export default function ExploreScreen() { const [nutritionSummary, setNutritionSummary] = useState(null); const [isNutritionLoading, setIsNutritionLoading] = useState(false); - // 记录最近一次请求的“日期键”,避免旧请求覆盖新结果 + // 心情相关状态 + const [moodModalVisible, setMoodModalVisible] = useState(false); + const [moodRecords, setMoodRecords] = useState>([]); + + // 记录最近一次请求的"日期键",避免旧请求覆盖新结果 const latestRequestKeyRef = useRef(null); const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`; + // 心情保存处理函数 + const handleMoodSave = (mood: string, time: string) => { + const today = new Date(); + const dateString = `${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日`; + + const newRecord = { + mood, + date: dateString, + time + }; + + setMoodRecords(prev => [newRecord, ...prev]); + setMoodModalVisible(false); + }; const loadHealthData = async (targetDate?: Date) => { try { @@ -288,7 +307,6 @@ export default function ExploreScreen() { } }; - // 使用统一的渐变背景色 const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const; @@ -404,6 +422,26 @@ export default function ExploreScreen() { showLabel={false} /> + {/* 心情卡片 */} + + setMoodModalVisible(true)} style={styles.moodCardContent}> + + + 😊 + + 心情 + + 记录你的每日心情 + {moodRecords.length > 0 ? ( + + 今日:{moodRecords[0].mood} + {moodRecords[0].time} + + ) : ( + 点击记录心情 + )} + + {/* 右列 */} @@ -413,7 +451,6 @@ export default function ExploreScreen() { weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined} height={userProfile?.height ? parseFloat(userProfile.height) : undefined} style={styles.bmiCardOverride} - // compact={true} /> @@ -441,15 +478,19 @@ export default function ExploreScreen() { —— )} + + - - - - - + + {/* 心情弹窗 */} + setMoodModalVisible(false)} + onSave={handleMoodSave} + /> ); } @@ -580,7 +621,6 @@ const styles = StyleSheet.create({ trainingCard: { backgroundColor: '#EEE9FF', }, - cardTitleSecondary: { color: '#9AA3AE', fontSize: 10, @@ -823,4 +863,49 @@ const styles = StyleSheet.create({ top: 0, padding: 4, }, + moodCard: { + backgroundColor: '#F0FDF4', + }, + moodCardContent: { + width: '100%', + }, + moodIconContainer: { + width: 24, + height: 24, + borderRadius: 8, + backgroundColor: '#DCFCE7', + alignItems: 'center', + justifyContent: 'center', + marginRight: 10, + }, + moodIcon: { + fontSize: 14, + }, + moodSubtitle: { + fontSize: 12, + color: '#6B7280', + marginTop: 4, + marginBottom: 8, + }, + moodPreview: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 4, + }, + moodPreviewText: { + fontSize: 14, + color: '#059669', + fontWeight: '600', + }, + moodPreviewTime: { + fontSize: 12, + color: '#6B7280', + }, + moodEmptyText: { + fontSize: 12, + color: '#9CA3AF', + fontStyle: 'italic', + marginTop: 4, + }, }); diff --git a/components/MoodCard.tsx b/components/MoodCard.tsx new file mode 100644 index 0000000..a537c27 --- /dev/null +++ b/components/MoodCard.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { ThemedText } from './ThemedText'; +import { ThemedView } from './ThemedView'; + +interface MoodCardProps { + onPress: () => void; +} + +export function MoodCard({ onPress }: MoodCardProps) { + return ( + + + 心情 + 记录你的每日心情 + + + + + 😊 + + 点击记录今日心情 + + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#fff', + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + header: { + marginBottom: 12, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 4, + }, + subtitle: { + fontSize: 14, + opacity: 0.6, + }, + content: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + }, + moodIcon: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#f0f0f0', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + emoji: { + fontSize: 20, + }, + moodText: { + fontSize: 16, + flex: 1, + }, +}); \ No newline at end of file diff --git a/components/MoodCardCompact.tsx b/components/MoodCardCompact.tsx new file mode 100644 index 0000000..60fe481 --- /dev/null +++ b/components/MoodCardCompact.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +interface MoodCardCompactProps { + onPress: () => void; +} + +export function MoodCardCompact({ onPress }: MoodCardCompactProps) { + return ( + + + 心情 + + + + + 😊 + + 记录今日心情 + + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#E8F5E8', + borderRadius: 16, + padding: 16, + minHeight: 100, + }, + header: { + marginBottom: 8, + }, + title: { + fontSize: 14, + fontWeight: '800', + color: '#192126', + }, + content: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + moodIcon: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#4CAF50', + justifyContent: 'center', + alignItems: 'center', + marginRight: 8, + }, + emoji: { + fontSize: 16, + }, + moodText: { + fontSize: 12, + color: '#2E7D32', + fontWeight: '600', + flex: 1, + }, +}); \ No newline at end of file diff --git a/components/MoodModal.tsx b/components/MoodModal.tsx new file mode 100644 index 0000000..351373b --- /dev/null +++ b/components/MoodModal.tsx @@ -0,0 +1,422 @@ +import React, { useState } from 'react'; +import { + Dimensions, + Modal, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; + +const { width, height } = Dimensions.get('window'); + +interface MoodModalProps { + visible: boolean; + onClose: () => void; + onSave: (mood: string, date: string) => void; +} + +// 心情日历数据 +const generateCalendarData = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const firstDayOfWeek = new Date(year, month, 1).getDay(); + + const calendar = []; + const weeks = []; + + // 添加空白日期 + for (let i = 0; i < firstDayOfWeek; i++) { + weeks.push(null); + } + + // 添加实际日期 + for (let day = 1; day <= daysInMonth; day++) { + weeks.push(day); + } + + // 按周分组 + for (let i = 0; i < weeks.length; i += 7) { + calendar.push(weeks.slice(i, i + 7)); + } + + return { calendar, today: today.getDate(), month: month + 1, year }; +}; + +const moodOptions = [ + { emoji: '😊', label: '开心', color: '#4CAF50' }, + { emoji: '😢', label: '难过', color: '#2196F3' }, + { emoji: '😰', label: '焦虑', color: '#FF9800' }, + { emoji: '😴', label: '疲惫', color: '#9C27B0' }, + { emoji: '😡', label: '愤怒', color: '#F44336' }, + { emoji: '😐', label: '平静', color: '#607D8B' }, +]; + +export function MoodModal({ visible, onClose, onSave }: MoodModalProps) { + const [selectedMood, setSelectedMood] = useState(''); + const { calendar, today, month, year } = generateCalendarData(); + + const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; + const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; + + const handleSave = () => { + if (selectedMood) { + const now = new Date(); + const timeString = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`; + onSave(selectedMood, timeString); + onClose(); + setSelectedMood(''); + } + }; + + const renderMoodIcon = (day: number | null, isToday: boolean) => { + if (!day) return null; + + if (isToday && selectedMood) { + const mood = moodOptions.find(m => m.label === selectedMood); + return ( + + + 🐻 + + + ); + } + + return ( + + 😊 + + ); + }; + + return ( + + + + + + + {year}年{monthNames[month - 1]} + + + + + + + {/* 日历视图 */} + + + {weekDays.map((day, index) => ( + {day} + ))} + + + {calendar.map((week, weekIndex) => ( + + {week.map((day, dayIndex) => ( + + {day && ( + <> + + {day.toString().padStart(2, '0')} + + {renderMoodIcon(day, day === today)} + + )} + + ))} + + ))} + + + {/* 心情选择 */} + + 选择今日心情 + + {moodOptions.map((mood, index) => ( + setSelectedMood(mood.label)} + > + {mood.emoji} + {mood.label} + + ))} + + + + {/* 近期记录 */} + + 近期记录 + {year}年{month}月{today}日 + + {selectedMood && ( + + + + 🐻 + + + {selectedMood} + + + {new Date().getHours()}:{new Date().getMinutes().toString().padStart(2, '0')} + + + )} + + + + {/* 保存按钮 */} + + + 保存心情 + + + + {/* 添加按钮 */} + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 20, + paddingVertical: 16, + backgroundColor: '#fff', + }, + backButton: { + fontSize: 24, + color: '#666', + }, + headerTitle: { + fontSize: 20, + fontWeight: '600', + color: '#333', + }, + nextButton: { + fontSize: 24, + color: '#666', + }, + content: { + flex: 1, + }, + calendar: { + backgroundColor: '#fff', + margin: 16, + borderRadius: 16, + padding: 16, + }, + weekHeader: { + flexDirection: 'row', + justifyContent: 'space-around', + marginBottom: 16, + }, + weekDay: { + fontSize: 14, + color: '#666', + textAlign: 'center', + width: (width - 64) / 7, + }, + weekRow: { + flexDirection: 'row', + justifyContent: 'space-around', + marginBottom: 16, + }, + dayContainer: { + width: (width - 64) / 7, + alignItems: 'center', + }, + dayNumber: { + fontSize: 14, + color: '#999', + marginBottom: 8, + }, + todayNumber: { + color: '#333', + fontWeight: '600', + }, + moodIconContainer: { + width: 40, + height: 40, + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + }, + bearIcon: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: 'rgba(255,255,255,0.9)', + justifyContent: 'center', + alignItems: 'center', + }, + bearEmoji: { + fontSize: 12, + }, + defaultMoodIcon: { + width: 40, + height: 40, + borderRadius: 20, + borderWidth: 1, + borderColor: '#ddd', + borderStyle: 'dashed', + justifyContent: 'center', + alignItems: 'center', + }, + defaultMoodEmoji: { + fontSize: 16, + opacity: 0.3, + }, + moodSection: { + backgroundColor: '#fff', + margin: 16, + marginTop: 0, + borderRadius: 16, + padding: 16, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: '#333', + marginBottom: 16, + }, + moodOptions: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + moodOption: { + width: (width - 80) / 3, + alignItems: 'center', + paddingVertical: 16, + marginBottom: 16, + borderRadius: 12, + backgroundColor: '#f8f8f8', + }, + selectedMoodOption: { + backgroundColor: '#e8f5e8', + borderWidth: 2, + borderColor: '#4CAF50', + }, + moodEmoji: { + fontSize: 24, + marginBottom: 8, + }, + moodLabel: { + fontSize: 14, + color: '#333', + }, + recentSection: { + backgroundColor: '#fff', + margin: 16, + marginTop: 0, + borderRadius: 16, + padding: 16, + }, + recentDate: { + fontSize: 14, + color: '#999', + marginBottom: 16, + }, + recentRecord: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + }, + recordIcon: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: '#4CAF50', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + recordMood: { + fontSize: 16, + color: '#333', + fontWeight: '500', + }, + spacer: { + flex: 1, + }, + recordTime: { + fontSize: 14, + color: '#999', + }, + footer: { + padding: 16, + backgroundColor: '#fff', + }, + saveButton: { + backgroundColor: '#4CAF50', + borderRadius: 12, + paddingVertical: 16, + alignItems: 'center', + }, + disabledButton: { + backgroundColor: '#ccc', + }, + saveButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + addButton: { + position: 'absolute', + bottom: 100, + right: 20, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: '#00C853', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + addButtonText: { + color: '#fff', + fontSize: 24, + fontWeight: '300', + }, +}); \ No newline at end of file