perf: 优化 logo

This commit is contained in:
2025-09-13 10:12:49 +08:00
parent a9bb73e2a1
commit 24b144a0d1
22 changed files with 254 additions and 17 deletions

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Out Live", "name": "Out Live",
"slug": "digital-pilates", "slug": "digital-pilates",
"version": "1.0.5", "version": "1.0.11",
"orientation": "portrait", "orientation": "portrait",
"scheme": "digitalpilates", "scheme": "digitalpilates",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",

View File

@@ -1,6 +1,9 @@
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { useAppDispatch } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme'; import { useColorScheme } from '@/hooks/useColorScheme';
import { analyzeFoodFromText } from '@/services/foodRecognition';
import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice';
import { triggerHapticFeedback } from '@/utils/haptics'; import { triggerHapticFeedback } from '@/utils/haptics';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import Voice from '@react-native-voice/voice'; import Voice from '@react-native-voice/voice';
@@ -19,22 +22,26 @@ import {
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result'; type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result' | 'analyzing';
export default function VoiceRecordScreen() { export default function VoiceRecordScreen() {
const theme = useColorScheme() ?? 'light'; const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme]; const colorTokens = Colors[theme];
const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>(); const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>();
const dispatch = useAppDispatch();
// 状态管理 // 状态管理
const [recordState, setRecordState] = useState<VoiceRecordState>('idle'); const [recordState, setRecordState] = useState<VoiceRecordState>('idle');
const [recognizedText, setRecognizedText] = useState(''); const [recognizedText, setRecognizedText] = useState('');
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [analysisProgress, setAnalysisProgress] = useState(0);
// 动画相关 // 动画相关
const scaleAnimation = useRef(new Animated.Value(1)).current; const scaleAnimation = useRef(new Animated.Value(1)).current;
const pulseAnimation = useRef(new Animated.Value(1)).current; const pulseAnimation = useRef(new Animated.Value(1)).current;
const waveAnimation = useRef(new Animated.Value(0)).current; const waveAnimation = useRef(new Animated.Value(0)).current;
const glowAnimation = useRef(new Animated.Value(0)).current;
const progressAnimation = useRef(new Animated.Value(0)).current;
useEffect(() => { useEffect(() => {
// 初始化语音识别 // 初始化语音识别
@@ -80,13 +87,43 @@ export default function VoiceRecordScreen() {
).start(); ).start();
}; };
// 启动科幻分析动画
const startAnalysisAnimation = () => {
// 光环动画
Animated.loop(
Animated.sequence([
Animated.timing(glowAnimation, {
toValue: 1,
duration: 2000,
useNativeDriver: false,
}),
Animated.timing(glowAnimation, {
toValue: 0,
duration: 2000,
useNativeDriver: false,
}),
])
).start();
// 进度条动画
Animated.timing(progressAnimation, {
toValue: 1,
duration: 8000, // 8秒完成
useNativeDriver: false,
}).start();
};
// 停止所有动画 // 停止所有动画
const stopAnimations = () => { const stopAnimations = () => {
pulseAnimation.stopAnimation(); pulseAnimation.stopAnimation();
waveAnimation.stopAnimation(); waveAnimation.stopAnimation();
glowAnimation.stopAnimation();
progressAnimation.stopAnimation();
scaleAnimation.setValue(1); scaleAnimation.setValue(1);
pulseAnimation.setValue(1); pulseAnimation.setValue(1);
waveAnimation.setValue(0); waveAnimation.setValue(0);
glowAnimation.setValue(0);
progressAnimation.setValue(0);
}; };
// 语音识别回调 // 语音识别回调
@@ -165,11 +202,68 @@ export default function VoiceRecordScreen() {
startRecording(); startRecording();
}; };
// 确认并返回结果 // 确认并分析食物文本
const confirmResult = () => { const confirmResult = async () => {
triggerHapticFeedback('impactMedium'); if (!recognizedText.trim()) {
// TODO: 处理识别结果,可以传递给食物分析页面 Alert.alert('提示', '请先进行语音识别');
router.back(); return;
}
try {
triggerHapticFeedback('impactMedium');
setRecordState('analyzing');
setAnalysisProgress(0);
// 启动科幻分析动画
startAnalysisAnimation();
// 模拟进度更新
const progressInterval = setInterval(() => {
setAnalysisProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval);
return prev;
}
return prev + Math.random() * 15;
});
}, 500);
// 调用文本分析API
dispatch(setLoading(true));
const result = await analyzeFoodFromText({ text: recognizedText });
// 清理进度定时器
clearInterval(progressInterval);
setAnalysisProgress(100);
// 生成识别结果ID并保存到Redux
const recognitionId = `text_${Date.now()}`;
dispatch(saveRecognitionResult({ id: recognitionId, result }));
// 停止动画并导航到结果页面
stopAnimations();
// 延迟一点让用户看到100%完成
setTimeout(() => {
router.replace({
pathname: '/food/analysis-result',
params: {
recognitionId,
mealType: mealType,
hideRecordBar: 'false'
}
});
}, 800);
} catch (error) {
console.error('食物分析失败:', error);
stopAnimations();
setRecordState('result');
const errorMessage = error instanceof Error ? error.message : '分析失败,请重试';
dispatch(setError(errorMessage));
Alert.alert('分析失败', errorMessage);
}
}; };
const handleBack = () => { const handleBack = () => {
@@ -188,6 +282,8 @@ export default function VoiceRecordScreen() {
return '正在聆听...'; return '正在聆听...';
case 'processing': case 'processing':
return 'AI处理中...'; return 'AI处理中...';
case 'analyzing':
return 'AI大模型分析中...';
case 'result': case 'result':
return '识别完成'; return '识别完成';
default: default:
@@ -219,6 +315,13 @@ export default function VoiceRecordScreen() {
icon: 'hourglass', icon: 'hourglass',
size: 80, size: 80,
}; };
case 'analyzing':
return {
onPress: () => { },
color: '#00D4AA',
icon: 'analytics',
size: 80,
};
case 'result': case 'result':
return { return {
onPress: confirmResult, onPress: confirmResult,
@@ -271,6 +374,52 @@ export default function VoiceRecordScreen() {
</> </>
)} )}
{/* 科幻分析特效 */}
{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 <Animated.View
style={[ style={[
@@ -288,7 +437,7 @@ export default function VoiceRecordScreen() {
style={styles.recordButtonInner} style={styles.recordButtonInner}
onPress={buttonConfig.onPress} onPress={buttonConfig.onPress}
activeOpacity={0.8} activeOpacity={0.8}
disabled={recordState === 'processing'} disabled={recordState === 'processing' || recordState === 'analyzing'}
> >
<Ionicons <Ionicons
name={buttonConfig.icon as any} name={buttonConfig.icon as any}
@@ -310,6 +459,30 @@ export default function VoiceRecordScreen() {
</Text> </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,
{
width: progressAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
},
]}
/>
</View>
<Text style={[styles.analysisHint, { color: colorTokens.textSecondary }]}>
AI正在深度分析您的食物描述...
</Text>
</View>
)}
</View> </View>
{/* 识别结果 */} {/* 识别结果 */}
@@ -468,4 +641,60 @@ const styles = StyleSheet.create({
fontWeight: '500', fontWeight: '500',
color: 'white', color: 'white',
}, },
// 科幻分析特效样式
glowRing: {
position: 'absolute',
width: 200,
height: 200,
borderRadius: 100,
backgroundColor: 'rgba(0, 212, 170, 0.1)',
borderWidth: 2,
borderColor: '#00D4AA',
shadowColor: '#00D4AA',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 20,
},
innerGlowRing: {
position: 'absolute',
width: 180,
height: 180,
borderRadius: 90,
backgroundColor: 'rgba(0, 212, 170, 0.05)',
borderWidth: 1,
borderColor: 'rgba(0, 212, 170, 0.3)',
},
analysisProgressContainer: {
alignItems: 'center',
marginTop: 20,
paddingHorizontal: 20,
},
progressText: {
fontSize: 16,
fontWeight: '600',
marginBottom: 12,
},
progressBarContainer: {
width: '100%',
height: 6,
backgroundColor: 'rgba(0, 212, 170, 0.2)',
borderRadius: 3,
overflow: 'hidden',
marginBottom: 8,
},
progressBar: {
height: '100%',
backgroundColor: '#00D4AA',
borderRadius: 3,
shadowColor: '#00D4AA',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 4,
},
analysisHint: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
fontStyle: 'italic',
},
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -7,7 +7,7 @@
"blend-mode": "normal", "blend-mode": "normal",
"glass": true, "glass": true,
"hidden": false, "hidden": false,
"image-name": "icon-1756312748268.png", "image-name": "icon-1756312748268.jpg",
"name": "icon-1756312748268", "name": "icon-1756312748268",
"opacity": 1 "opacity": 1
} }

View File

@@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon-1756312748268.png", "filename" : "icon-1756312748268.jpg",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,17 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon-1756312748268.png", "filename" : "icon-1756312748268.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "icon-1756312748268 1.png", "filename" : "icon-1756312748268 1.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "icon-1756312748268 2.png", "filename" : "icon-1756312748268 2.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,17 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon-1756312748268.png", "filename" : "icon-1756312748268.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "icon-1756312748268 1.png", "filename" : "icon-1756312748268 1.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "icon-1756312748268 2.png", "filename" : "icon-1756312748268 2.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -24,7 +24,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.10</string> <string>1.0.11</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@@ -21,6 +21,10 @@ export type FoodRecognitionRequest = {
imageUrls: string[]; imageUrls: string[];
}; };
export type TextFoodAnalysisRequest = {
text: string;
};
export type FoodRecognitionResponse = { export type FoodRecognitionResponse = {
items: FoodConfirmationOption[]; items: FoodConfirmationOption[];
analysisText: string; analysisText: string;
@@ -32,3 +36,7 @@ export type FoodRecognitionResponse = {
export async function recognizeFood(request: FoodRecognitionRequest): Promise<FoodRecognitionResponse> { export async function recognizeFood(request: FoodRecognitionRequest): Promise<FoodRecognitionResponse> {
return api.post<FoodRecognitionResponse>('/ai-coach/food-recognition', request); return api.post<FoodRecognitionResponse>('/ai-coach/food-recognition', request);
} }
export async function analyzeFoodFromText(request: TextFoodAnalysisRequest): Promise<FoodRecognitionResponse> {
return api.post<FoodRecognitionResponse>('/ai-coach/text-food-analysis', request);
}