feat: 更新 UI 样式以及消息通知
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 }]}>
|
||||
“今早吃了两个煎蛋、一片全麦面包和一杯牛奶”
|
||||
</Text>
|
||||
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
|
||||
“午饭吃了红烧肉约150克、米饭一小碗、青菜一份”
|
||||
</Text>
|
||||
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
|
||||
“晚饭吃了蒸蛋羹、紫菜蛋花汤、小米粥一碗”
|
||||
</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,
|
||||
|
||||
Reference in New Issue
Block a user