feat: 更新 UI 样式以及消息通知

This commit is contained in:
2025-09-14 21:41:33 +08:00
parent 24b144a0d1
commit 55d133c470
12 changed files with 801 additions and 610 deletions

View File

@@ -16,7 +16,7 @@ import { WaterRecordSource } from '@/services/waterRecords';
import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import { createWaterRecordAction } from '@/store/waterSlice';
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React from 'react';
@@ -54,6 +54,21 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
await notificationService.initialize();
console.log('通知服务初始化成功');
// 注册午餐提醒12:00
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
console.log('午餐提醒已注册');
// 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
console.log('晚餐提醒已注册');
// 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
console.log('心情提醒已注册');
await DailySummaryNotificationHelpers.scheduleDailySummaryNotification(profile.name || '')
// 初始化快捷动作
await setupQuickActions();
console.log('快捷动作初始化成功');
@@ -104,33 +119,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
}
}, [userDataLoaded, privacyAgreed]);
// 当用户数据加载完成且用户名存在时,注册所有提醒
React.useEffect(() => {
const registerAllReminders = async () => {
try {
await notificationService.initialize();
// 注册午餐提醒12:00
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
console.log('午餐提醒已注册');
// 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
console.log('晚餐提醒已注册');
// 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
console.log('心情提醒已注册');
console.log('喝水提醒后台任务已注册');
} catch (error) {
console.error('注册提醒失败:', error);
}
};
registerAllReminders();
}, [userDataLoaded, profile?.name]);
const handlePrivacyAgree = () => {
dispatch(setPrivacyAgreed());
setShowPrivacyModal(false);

View File

@@ -13,15 +13,12 @@ import React, { useEffect, useRef, useState } from 'react';
import {
Alert,
Animated,
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
const { width } = Dimensions.get('window');
type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result' | 'analyzing';
export default function VoiceRecordScreen() {
@@ -43,21 +40,6 @@ export default function VoiceRecordScreen() {
const glowAnimation = useRef(new Animated.Value(0)).current;
const progressAnimation = useRef(new Animated.Value(0)).current;
useEffect(() => {
// 初始化语音识别
Voice.onSpeechStart = onSpeechStart;
Voice.onSpeechRecognized = onSpeechRecognized;
Voice.onSpeechEnd = onSpeechEnd;
Voice.onSpeechError = onSpeechError;
Voice.onSpeechResults = onSpeechResults;
Voice.onSpeechPartialResults = onSpeechPartialResults;
Voice.onSpeechVolumeChanged = onSpeechVolumeChanged;
return () => {
Voice.destroy().then(Voice.removeAllListeners);
};
}, []);
// 启动脉动动画
const startPulseAnimation = () => {
Animated.loop(
@@ -149,7 +131,6 @@ export default function VoiceRecordScreen() {
setIsListening(false);
setRecordState('idle');
stopAnimations();
Alert.alert('录音失败', '请检查麦克风权限或稍后重试');
};
const onSpeechResults = (event: any) => {
@@ -171,16 +152,33 @@ export default function VoiceRecordScreen() {
scaleAnimation.setValue(Math.min(scale, 1.5));
};
useEffect(() => {
// 初始化语音识别
Voice.onSpeechStart = onSpeechStart;
Voice.onSpeechRecognized = onSpeechRecognized;
Voice.onSpeechEnd = onSpeechEnd;
Voice.onSpeechError = onSpeechError;
Voice.onSpeechResults = onSpeechResults;
Voice.onSpeechPartialResults = onSpeechPartialResults;
Voice.onSpeechVolumeChanged = onSpeechVolumeChanged;
return () => {
Voice.destroy().then(Voice.removeAllListeners);
};
}, [onSpeechStart, onSpeechRecognized, onSpeechEnd, onSpeechError, onSpeechResults, onSpeechPartialResults, onSpeechVolumeChanged]);
// 开始录音
const startRecording = async () => {
try {
setRecognizedText('');
setRecordState('idle');
triggerHapticFeedback('impactMedium');
await Voice.start('zh-CN'); // 设置为中文识别
} catch (error) {
console.log('启动语音识别失败:', error);
setRecordState('idle');
Alert.alert('录音失败', '无法启动语音识别,请检查权限设置');
}
};
@@ -196,10 +194,27 @@ export default function VoiceRecordScreen() {
};
// 重新录音
const retryRecording = () => {
const retryRecording = async () => {
// 停止所有动画
stopAnimations();
// 重置所有状态
setRecognizedText('');
setAnalysisProgress(0);
setIsListening(false);
setRecordState('idle');
startRecording();
// 确保语音识别已停止
try {
await Voice.stop();
} catch {
// 忽略停止错误,可能已经停止了
}
// 延迟一点再开始新的录音,确保状态完全重置
setTimeout(() => {
startRecording();
}, 100);
};
// 确认并分析食物文本
@@ -213,7 +228,7 @@ export default function VoiceRecordScreen() {
triggerHapticFeedback('impactMedium');
setRecordState('analyzing');
setAnalysisProgress(0);
// 启动科幻分析动画
startAnalysisAnimation();
@@ -242,7 +257,7 @@ export default function VoiceRecordScreen() {
// 停止动画并导航到结果页面
stopAnimations();
// 延迟一点让用户看到100%完成
setTimeout(() => {
router.replace({
@@ -259,7 +274,7 @@ export default function VoiceRecordScreen() {
console.error('食物分析失败:', error);
stopAnimations();
setRecordState('result');
const errorMessage = error instanceof Error ? error.message : '分析失败,请重试';
dispatch(setError(errorMessage));
Alert.alert('分析失败', errorMessage);
@@ -277,15 +292,15 @@ export default function VoiceRecordScreen() {
const getStatusText = () => {
switch (recordState) {
case 'idle':
return '点击开始录音';
return '轻触麦克风开始录音';
case 'listening':
return '正在聆听...';
return '正在聆听中,请开始说话...';
case 'processing':
return 'AI处理中...';
return 'AI正在处理语音内容...';
case 'analyzing':
return 'AI大模型分中...';
return 'AI大模型深度分析营养成分中...';
case 'result':
return '识别完成';
return '语音识别完成,请确认结果';
default:
return '';
}
@@ -344,145 +359,180 @@ export default function VoiceRecordScreen() {
/>
<View style={styles.content}>
{/* 录音动画区域 */}
<View style={styles.animationContainer}>
{/* 背景波浪效果 */}
{recordState === 'listening' && (
<>
{[1, 2, 3].map((index) => (
{/* 上半部分:介绍 */}
<View style={styles.topSection}>
<View style={styles.introContainer}>
<Text style={[styles.introDescription, { color: colorTokens.textSecondary }]}>
AI将智能分析营养成分和卡路里
</Text>
</View>
</View>
{/* 中间部分:录音动画区域 */}
<View style={styles.middleSection}>
<View style={styles.animationContainer}>
{/* 背景波浪效果 */}
{recordState === 'listening' && (
<>
{[1, 2, 3].map((index) => (
<Animated.View
key={index}
style={[
styles.waveRing,
{
transform: [
{
scale: waveAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 2 + index * 0.3],
}),
},
],
opacity: waveAnimation.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0.6, 0.3, 0],
}),
},
]}
/>
))}
</>
)}
{/* 科幻分析特效 */}
{recordState === 'analyzing' && (
<>
{/* 外光环 */}
<Animated.View
key={index}
style={[
styles.waveRing,
styles.glowRing,
{
opacity: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.8],
}),
transform: [
{
scale: waveAnimation.interpolate({
scale: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 2 + index * 0.3],
outputRange: [1.2, 1.6],
}),
},
],
opacity: waveAnimation.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0.6, 0.3, 0],
}),
},
]}
/>
))}
</>
)}
{/* 科幻分析特效 */}
{recordState === 'analyzing' && (
<>
{/* 外光环 */}
<Animated.View
style={[
styles.glowRing,
{
opacity: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.8],
}),
transform: [
{
scale: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1.2, 1.6],
}),
},
],
},
]}
/>
{/* 内光环 */}
<Animated.View
style={[
styles.innerGlowRing,
{
opacity: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.5, 1],
}),
transform: [
{
scale: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.9, 1.1],
}),
},
],
},
]}
/>
</>
)}
{/* 主录音按钮 */}
<Animated.View
style={[
styles.recordButton,
{
backgroundColor: buttonConfig.color,
transform: [
{ scale: scaleAnimation },
{ scale: pulseAnimation },
],
},
]}
>
<TouchableOpacity
style={styles.recordButtonInner}
onPress={buttonConfig.onPress}
activeOpacity={0.8}
disabled={recordState === 'processing' || recordState === 'analyzing'}
>
<Ionicons
name={buttonConfig.icon as any}
size={buttonConfig.size}
color="white"
/>
</TouchableOpacity>
</Animated.View>
</View>
{/* 状态文本 */}
<View style={styles.statusContainer}>
<Text style={[styles.statusText, { color: colorTokens.text }]}>
{getStatusText()}
</Text>
{recordState === 'listening' && (
<Text style={[styles.hintText, { color: colorTokens.textSecondary }]}>
</Text>
)}
{recordState === 'analyzing' && (
<View style={styles.analysisProgressContainer}>
<Text style={[styles.progressText, { color: colorTokens.text }]}>
: {Math.round(analysisProgress)}%
</Text>
<View style={styles.progressBarContainer}>
{/* 内光环 */}
<Animated.View
style={[
styles.progressBar,
styles.innerGlowRing,
{
width: progressAnimation.interpolate({
opacity: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
outputRange: [0.5, 1],
}),
transform: [
{
scale: glowAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.9, 1.1],
}),
},
],
},
]}
/>
</View>
<Text style={[styles.analysisHint, { color: colorTokens.textSecondary }]}>
AI正在深度分析您的食物描述...
</>
)}
{/* 主录音按钮 */}
<Animated.View
style={[
styles.recordButton,
{
backgroundColor: buttonConfig.color,
transform: [
{ scale: scaleAnimation },
{ scale: pulseAnimation },
],
},
]}
>
<TouchableOpacity
style={styles.recordButtonInner}
onPress={buttonConfig.onPress}
activeOpacity={0.8}
disabled={recordState === 'processing' || recordState === 'analyzing'}
>
<Ionicons
name={buttonConfig.icon as any}
size={buttonConfig.size}
color="white"
/>
</TouchableOpacity>
</Animated.View>
</View>
</View>
{/* 下半部分:状态文本和示例 */}
<View style={styles.bottomSection}>
<View style={styles.statusContainer}>
<Text style={[styles.statusText, { color: colorTokens.text }]}>
{getStatusText()}
</Text>
{recordState === 'listening' && (
<Text style={[styles.hintText, { color: colorTokens.textSecondary }]}>
</Text>
</View>
)}
)}
{/* 食物记录示例 */}
{recordState === 'idle' && (
<BlurView intensity={20} tint={theme} style={styles.examplesContainer}>
<View style={styles.examplesContent}>
<Text style={[styles.examplesTitle, { color: colorTokens.text }]}>
</Text>
<View style={styles.examplesList}>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;&rdquo;
</Text>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;150&rdquo;
</Text>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;&rdquo;
</Text>
</View>
</View>
</BlurView>
)}
{recordState === 'analyzing' && (
<View style={styles.analysisProgressContainer}>
<Text style={[styles.progressText, { color: colorTokens.text }]}>
: {Math.round(analysisProgress)}%
</Text>
<View style={styles.progressBarContainer}>
<Animated.View
style={[
styles.progressBar,
{
width: progressAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
},
]}
/>
</View>
<Text style={[styles.analysisHint, { color: colorTokens.textSecondary }]}>
AI正在深度分析您的食物描述...
</Text>
</View>
)}
</View>
</View>
{/* 识别结果 */}
@@ -528,17 +578,46 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 40,
},
topSection: {
alignItems: 'center',
paddingBottom: 20,
},
middleSection: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
minHeight: 200,
},
bottomSection: {
alignItems: 'center',
},
introContainer: {
alignItems: 'center',
paddingHorizontal: 20,
},
introTitle: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
textAlign: 'center',
},
introDescription: {
fontSize: 16,
lineHeight: 24,
textAlign: 'center',
paddingHorizontal: 10,
},
animationContainer: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 40,
height: 200,
width: 200,
flex: 1,
},
waveRing: {
position: 'absolute',
@@ -573,7 +652,7 @@ const styles = StyleSheet.create({
},
statusContainer: {
alignItems: 'center',
marginBottom: 30,
paddingHorizontal: 20,
},
statusText: {
fontSize: 18,
@@ -585,6 +664,35 @@ const styles = StyleSheet.create({
textAlign: 'center',
lineHeight: 20,
},
examplesContainer: {
marginTop: 24,
marginHorizontal: 20,
borderRadius: 16,
overflow: 'hidden',
},
examplesContent: {
padding: 20,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderRadius: 16,
alignItems: 'center',
},
examplesTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 16,
},
examplesList: {
paddingHorizontal: 10,
gap: 8,
},
exampleText: {
fontSize: 14,
lineHeight: 22,
textAlign: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
marginVertical: 4,
},
resultContainer: {
position: 'absolute',
bottom: 100,