feat: 更新心情记录功能和界面

- 调整启动画面中的图片宽度,提升视觉效果
- 移除引导页面相关组件,简化应用结构
- 新增心情统计页面,支持用户查看和分析心情数据
- 优化心情卡片组件,增强用户交互体验
- 更新登录页面标题,提升品牌一致性
- 新增心情日历和编辑功能,支持用户记录和管理心情
This commit is contained in:
richarjiang
2025-08-21 17:59:22 +08:00
parent a7607e0f74
commit 72e75b602e
24 changed files with 2964 additions and 1238 deletions

View File

@@ -1,76 +1,111 @@
import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
import dayjs from 'dayjs';
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ThemedText } from './ThemedText';
import { ThemedView } from './ThemedView';
interface MoodCardProps {
moodCheckin: MoodCheckin | null;
onPress: () => void;
isLoading?: boolean;
}
export function MoodCard({ onPress }: MoodCardProps) {
export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardProps) {
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
return (
<ThemedView style={styles.container}>
<View style={styles.header}>
<ThemedText style={styles.title}></ThemedText>
<ThemedText style={styles.subtitle}></ThemedText>
<TouchableOpacity onPress={onPress} style={styles.moodCardContent} disabled={isLoading}>
<View style={styles.cardHeaderRow}>
<View style={styles.moodIconContainer}>
{moodCheckin ? (
<Text style={styles.moodIcon}>
{moodConfig?.emoji || '😊'}
</Text>
) : (
<Text style={styles.moodIcon}>😊</Text>
)}
</View>
<Text style={styles.cardTitle}></Text>
</View>
<TouchableOpacity style={styles.content} onPress={onPress}>
<View style={styles.moodIcon}>
<Text style={styles.emoji}>😊</Text>
<Text style={styles.moodSubtitle}></Text>
{isLoading ? (
<View style={styles.moodPreview}>
<Text style={styles.moodLoadingText}>...</Text>
</View>
<ThemedText style={styles.moodText}></ThemedText>
</TouchableOpacity>
</ThemedView>
) : moodCheckin ? (
<View style={styles.moodPreview}>
<Text style={styles.moodPreviewText}>
{moodConfig?.label || moodCheckin.moodType}
</Text>
<Text style={styles.moodPreviewTime}>
{dayjs(moodCheckin.createdAt).format('HH:mm')}
</Text>
</View>
) : (
<Text style={styles.moodEmptyText}></Text>
)}
</TouchableOpacity>
);
}
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,
moodCardContent: {
width: '100%',
},
header: {
marginBottom: 12,
},
title: {
fontSize: 18,
fontWeight: '600',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
opacity: 0.6,
},
content: {
cardHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
marginBottom: 12,
},
moodIconContainer: {
width: 24,
height: 24,
borderRadius: 8,
backgroundColor: '#DCFCE7',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
moodIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
fontSize: 14,
},
cardTitle: {
fontSize: 14,
fontWeight: '800',
color: '#192126',
},
moodSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 4,
marginBottom: 8,
},
moodPreview: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginRight: 12,
marginTop: 4,
},
emoji: {
fontSize: 20,
moodPreviewText: {
fontSize: 14,
color: '#059669',
fontWeight: '600',
},
moodText: {
fontSize: 16,
flex: 1,
moodPreviewTime: {
fontSize: 12,
color: '#6B7280',
},
moodEmptyText: {
fontSize: 12,
color: '#9CA3AF',
fontStyle: 'italic',
marginTop: 4,
},
moodLoadingText: {
fontSize: 12,
color: '#9CA3AF',
fontStyle: 'italic',
marginTop: 4,
},
});

View File

@@ -0,0 +1,199 @@
import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
import dayjs from 'dayjs';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
interface MoodHistoryCardProps {
moodCheckins: MoodCheckin[];
title?: string;
}
export function MoodHistoryCard({ moodCheckins, title = '心情记录' }: MoodHistoryCardProps) {
// 计算心情统计
const moodStats = React.useMemo(() => {
const stats = {
total: moodCheckins.length,
averageIntensity: 0,
moodDistribution: {} as Record<string, number>,
mostFrequentMood: '',
};
if (moodCheckins.length === 0) return stats;
// 计算平均强度
const totalIntensity = moodCheckins.reduce((sum, checkin) => sum + checkin.intensity, 0);
stats.averageIntensity = Math.round(totalIntensity / moodCheckins.length);
// 计算心情分布
moodCheckins.forEach(checkin => {
const moodLabel = getMoodConfig(checkin.moodType)?.label || checkin.moodType;
stats.moodDistribution[moodLabel] = (stats.moodDistribution[moodLabel] || 0) + 1;
});
// 找出最频繁的心情
const sortedMoods = Object.entries(stats.moodDistribution)
.sort(([, a], [, b]) => b - a);
stats.mostFrequentMood = sortedMoods[0]?.[0] || '';
return stats;
}, [moodCheckins]);
// 获取最近的心情记录
const recentMoods = moodCheckins
.sort((a, b) => dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf())
.slice(0, 5);
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
{moodCheckins.length === 0 ? (
<View style={styles.emptyState}>
<Text style={styles.emptyText}></Text>
</View>
) : (
<>
{/* 统计信息 */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{moodStats.total}</Text>
<Text style={styles.statLabel}></Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{moodStats.averageIntensity}</Text>
<Text style={styles.statLabel}></Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{moodStats.mostFrequentMood}</Text>
<Text style={styles.statLabel}></Text>
</View>
</View>
{/* 最近记录 */}
<View style={styles.recentContainer}>
<Text style={styles.sectionTitle}></Text>
{recentMoods.map((checkin, index) => {
const moodConfig = getMoodConfig(checkin.moodType);
return (
<View key={checkin.id} style={styles.moodItem}>
<View style={styles.moodInfo}>
<Text style={styles.moodEmoji}>{moodConfig?.emoji}</Text>
<View style={styles.moodDetails}>
<Text style={styles.moodLabel}>{moodConfig?.label}</Text>
<Text style={styles.moodDate}>
{dayjs(checkin.createdAt).format('MM月DD日 HH:mm')}
</Text>
</View>
</View>
<View style={styles.moodIntensity}>
<Text style={styles.intensityText}> {checkin.intensity}</Text>
</View>
</View>
);
})}
</View>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#192126',
marginBottom: 16,
},
emptyState: {
alignItems: 'center',
paddingVertical: 32,
},
emptyText: {
fontSize: 14,
color: '#9CA3AF',
fontStyle: 'italic',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 20,
paddingVertical: 16,
backgroundColor: '#F8F9FA',
borderRadius: 12,
},
statItem: {
alignItems: 'center',
},
statValue: {
fontSize: 20,
fontWeight: '700',
color: '#192126',
marginBottom: 4,
},
statLabel: {
fontSize: 12,
color: '#6B7280',
},
recentContainer: {
marginTop: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#192126',
marginBottom: 12,
},
moodItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#F3F4F6',
},
moodInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
moodEmoji: {
fontSize: 20,
marginRight: 12,
},
moodDetails: {
flex: 1,
},
moodLabel: {
fontSize: 14,
fontWeight: '500',
color: '#192126',
marginBottom: 2,
},
moodDate: {
fontSize: 12,
color: '#6B7280',
},
moodIntensity: {
alignItems: 'flex-end',
},
intensityText: {
fontSize: 12,
color: '#6B7280',
fontWeight: '500',
},
});

View File

@@ -1,422 +0,0 @@
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<string>('');
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 (
<View style={[styles.moodIconContainer, { backgroundColor: mood?.color }]}>
<View style={styles.bearIcon}>
<Text style={styles.bearEmoji}>🐻</Text>
</View>
</View>
);
}
return (
<View style={styles.defaultMoodIcon}>
<Text style={styles.defaultMoodEmoji}>😊</Text>
</View>
);
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose}>
<Text style={styles.backButton}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>{year}{monthNames[month - 1]}</Text>
<TouchableOpacity>
<Text style={styles.nextButton}></Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content}>
{/* 日历视图 */}
<View style={styles.calendar}>
<View style={styles.weekHeader}>
{weekDays.map((day, index) => (
<Text key={index} style={styles.weekDay}>{day}</Text>
))}
</View>
{calendar.map((week, weekIndex) => (
<View key={weekIndex} style={styles.weekRow}>
{week.map((day, dayIndex) => (
<View key={dayIndex} style={styles.dayContainer}>
{day && (
<>
<Text style={[
styles.dayNumber,
day === today && styles.todayNumber
]}>
{day.toString().padStart(2, '0')}
</Text>
{renderMoodIcon(day, day === today)}
</>
)}
</View>
))}
</View>
))}
</View>
{/* 心情选择 */}
<View style={styles.moodSection}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.moodOptions}>
{moodOptions.map((mood, index) => (
<TouchableOpacity
key={index}
style={[
styles.moodOption,
selectedMood === mood.label && styles.selectedMoodOption
]}
onPress={() => setSelectedMood(mood.label)}
>
<Text style={styles.moodEmoji}>{mood.emoji}</Text>
<Text style={styles.moodLabel}>{mood.label}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* 近期记录 */}
<View style={styles.recentSection}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.recentDate}>{year}{month}{today}</Text>
{selectedMood && (
<View style={styles.recentRecord}>
<View style={styles.recordIcon}>
<View style={styles.bearIcon}>
<Text style={styles.bearEmoji}>🐻</Text>
</View>
</View>
<Text style={styles.recordMood}>{selectedMood}</Text>
<View style={styles.spacer} />
<Text style={styles.recordTime}>
{new Date().getHours()}:{new Date().getMinutes().toString().padStart(2, '0')}
</Text>
</View>
)}
</View>
</ScrollView>
{/* 保存按钮 */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, !selectedMood && styles.disabledButton]}
onPress={handleSave}
disabled={!selectedMood}
>
<Text style={styles.saveButtonText}></Text>
</TouchableOpacity>
</View>
{/* 添加按钮 */}
<TouchableOpacity style={styles.addButton} onPress={handleSave}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</SafeAreaView>
</Modal>
);
}
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',
},
});

View File

@@ -40,8 +40,8 @@ export default function PrivacyConsentModal({
>
<View style={styles.overlay}>
<View style={styles.container}>
<Text style={styles.title}></Text>
<Text style={styles.title}>Sealife</Text>
<View style={styles.contentContainer}>
<Text style={styles.description}>
"同意并继续"
@@ -69,11 +69,11 @@ export default function PrivacyConsentModal({
</TouchableOpacity>
</View>
</View>
<TouchableOpacity style={styles.agreeButton} onPress={onAgree}>
<Text style={styles.agreeButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.disagreeButton} onPress={onDisagree}>
<Text style={styles.disagreeButtonText}>退</Text>
</TouchableOpacity>