feat(ui): 统一健康卡片标题图标并优化语音录音稳定性
- 为所有健康数据卡片添加对应功能图标,提升视觉一致性 - 将“小鱼干”文案统一为“能量值”,并更新获取说明 - 语音录音页面增加组件卸载保护、错误提示与资源清理逻辑 - 个人页支持毛玻璃按钮样式,默认用户名置空 - 新增血氧、饮食、心情、压力、睡眠、步数、体重等图标资源 - 升级 react-native-purchases 至 9.4.3 - 移除 useAuthGuard 调试日志
This commit is contained in:
@@ -9,7 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import Voice from '@react-native-voice/voice';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
@@ -33,6 +33,10 @@ export default function VoiceRecordScreen() {
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [analysisProgress, setAnalysisProgress] = useState(0);
|
||||
|
||||
// 用于跟踪组件是否已卸载,防止在组件卸载后设置状态
|
||||
const isMountedRef = useRef(true);
|
||||
const progressIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 动画相关
|
||||
const scaleAnimation = useRef(new Animated.Value(1)).current;
|
||||
const pulseAnimation = useRef(new Animated.Value(1)).current;
|
||||
@@ -108,50 +112,78 @@ export default function VoiceRecordScreen() {
|
||||
progressAnimation.setValue(0);
|
||||
};
|
||||
|
||||
// 语音识别回调
|
||||
const onSpeechStart = () => {
|
||||
// 语音识别回调 - 使用 useCallback 避免每次渲染重新创建
|
||||
const onSpeechStart = useCallback(() => {
|
||||
console.log('语音开始');
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setIsListening(true);
|
||||
setRecordState('listening');
|
||||
startPulseAnimation();
|
||||
startWaveAnimation();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSpeechRecognized = () => {
|
||||
const onSpeechRecognized = useCallback(() => {
|
||||
console.log('语音识别中...');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSpeechEnd = useCallback(() => {
|
||||
console.log('语音结束');
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const onSpeechEnd = () => {
|
||||
setIsListening(false);
|
||||
setRecordState('processing');
|
||||
stopAnimations();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSpeechError = (error: any) => {
|
||||
const onSpeechError = useCallback((error: any) => {
|
||||
console.log('语音识别错误:', error);
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setIsListening(false);
|
||||
setRecordState('idle');
|
||||
stopAnimations();
|
||||
};
|
||||
|
||||
const onSpeechResults = (event: any) => {
|
||||
// 显示更友好的错误信息
|
||||
if (error.error?.code === '7') {
|
||||
Alert.alert('提示', '没有检测到语音输入,请重试');
|
||||
} else if (error.error?.code === '2') {
|
||||
Alert.alert('提示', '网络连接异常,请检查网络后重试');
|
||||
} else {
|
||||
Alert.alert('提示', '语音识别出现问题,请重试');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSpeechResults = useCallback((event: any) => {
|
||||
console.log('语音识别结果:', event);
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const text = event.value?.[0] || '';
|
||||
setRecognizedText(text);
|
||||
setRecordState('result');
|
||||
if (text.trim()) {
|
||||
setRecognizedText(text);
|
||||
setRecordState('result');
|
||||
} else {
|
||||
setRecordState('idle');
|
||||
Alert.alert('提示', '未识别到有效内容,请重新录音');
|
||||
}
|
||||
stopAnimations();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSpeechPartialResults = useCallback((event: any) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const onSpeechPartialResults = (event: any) => {
|
||||
const text = event.value?.[0] || '';
|
||||
setRecognizedText(text);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSpeechVolumeChanged = useCallback((event: any) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const onSpeechVolumeChanged = (event: any) => {
|
||||
// 根据音量调整动画
|
||||
const volume = event.value || 0;
|
||||
const scale = 1 + (volume * 0.1);
|
||||
scaleAnimation.setValue(Math.min(scale, 1.5));
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化语音识别
|
||||
@@ -164,57 +196,91 @@ export default function VoiceRecordScreen() {
|
||||
Voice.onSpeechVolumeChanged = onSpeechVolumeChanged;
|
||||
|
||||
return () => {
|
||||
Voice.destroy().then(Voice.removeAllListeners);
|
||||
// 标记组件已卸载
|
||||
isMountedRef.current = false;
|
||||
|
||||
// 清理进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
|
||||
// 清理语音识别资源
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await Voice.stop();
|
||||
await Voice.destroy();
|
||||
Voice.removeAllListeners();
|
||||
} catch (error) {
|
||||
console.log('清理语音识别资源失败:', error);
|
||||
}
|
||||
};
|
||||
cleanup();
|
||||
};
|
||||
}, [onSpeechStart, onSpeechRecognized, onSpeechEnd, onSpeechError, onSpeechResults, onSpeechPartialResults, onSpeechVolumeChanged]);
|
||||
|
||||
|
||||
// 开始录音
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
// 重置状态
|
||||
setRecognizedText('');
|
||||
setRecordState('idle');
|
||||
triggerHapticFeedback('impactMedium');
|
||||
|
||||
await Voice.start('zh-CN'); // 设置为中文识别
|
||||
// 确保之前的识别已停止
|
||||
await Voice.stop();
|
||||
|
||||
// 添加短暂延迟确保资源清理完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 启动新的语音识别
|
||||
await Voice.start('zh-CN');
|
||||
|
||||
} catch (error) {
|
||||
console.log('启动语音识别失败:', error);
|
||||
setRecordState('idle');
|
||||
Alert.alert('录音失败', '无法启动语音识别,请检查权限设置');
|
||||
setIsListening(false);
|
||||
Alert.alert('录音失败', '无法启动语音识别,请检查麦克风权限设置');
|
||||
}
|
||||
};
|
||||
|
||||
// 停止录音
|
||||
const stopRecording = async () => {
|
||||
try {
|
||||
console.log('停止录音');
|
||||
setIsListening(false);
|
||||
await Voice.stop();
|
||||
triggerHapticFeedback('impactLight');
|
||||
} catch (error) {
|
||||
console.log('停止语音识别失败:', error);
|
||||
setIsListening(false);
|
||||
setRecordState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
// 重新录音
|
||||
const retryRecording = async () => {
|
||||
// 停止所有动画
|
||||
stopAnimations();
|
||||
|
||||
// 重置所有状态
|
||||
setRecognizedText('');
|
||||
setAnalysisProgress(0);
|
||||
setIsListening(false);
|
||||
setRecordState('idle');
|
||||
|
||||
// 确保语音识别已停止
|
||||
try {
|
||||
await Voice.stop();
|
||||
} catch {
|
||||
// 忽略停止错误,可能已经停止了
|
||||
}
|
||||
// 停止所有动画
|
||||
stopAnimations();
|
||||
|
||||
// 延迟一点再开始新的录音,确保状态完全重置
|
||||
setTimeout(() => {
|
||||
startRecording();
|
||||
}, 100);
|
||||
// 重置所有状态
|
||||
setRecognizedText('');
|
||||
setAnalysisProgress(0);
|
||||
setIsListening(false);
|
||||
setRecordState('idle');
|
||||
|
||||
// 确保语音识别已停止
|
||||
await Voice.stop();
|
||||
|
||||
// 延迟一点再开始新的录音,确保状态完全重置
|
||||
setTimeout(() => {
|
||||
startRecording();
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.log('重新录音失败:', error);
|
||||
setRecordState('idle');
|
||||
setIsListening(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 确认并分析食物文本
|
||||
@@ -233,10 +299,19 @@ export default function VoiceRecordScreen() {
|
||||
startAnalysisAnimation();
|
||||
|
||||
// 模拟进度更新
|
||||
const progressInterval = setInterval(() => {
|
||||
progressIntervalRef.current = setInterval(() => {
|
||||
if (!isMountedRef.current) {
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setAnalysisProgress(prev => {
|
||||
if (prev >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
return prev + Math.random() * 15;
|
||||
@@ -248,7 +323,12 @@ export default function VoiceRecordScreen() {
|
||||
const result = await analyzeFoodFromText({ text: recognizedText });
|
||||
|
||||
// 清理进度定时器
|
||||
clearInterval(progressInterval);
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setAnalysisProgress(100);
|
||||
|
||||
// 生成识别结果ID并保存到Redux
|
||||
@@ -272,8 +352,17 @@ export default function VoiceRecordScreen() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('食物分析失败:', error);
|
||||
|
||||
// 清理进度定时器
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
stopAnimations();
|
||||
setRecordState('result');
|
||||
dispatch(setLoading(false));
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : '分析失败,请重试';
|
||||
dispatch(setError(errorMessage));
|
||||
@@ -281,11 +370,24 @@ export default function VoiceRecordScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (isListening) {
|
||||
stopRecording();
|
||||
const handleBack = async () => {
|
||||
try {
|
||||
// 如果正在录音,先停止
|
||||
if (isListening) {
|
||||
await stopRecording();
|
||||
}
|
||||
|
||||
// 停止所有动画
|
||||
stopAnimations();
|
||||
|
||||
// 确保语音识别完全停止
|
||||
await Voice.stop();
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log('返回时清理资源失败:', error);
|
||||
router.back();
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 获取状态对应的UI文本
|
||||
|
||||
Reference in New Issue
Block a user