feat: 新增语音记录饮食功能与开发者调试模块

- 集成 @react-native-voice/voice 实现中文语音识别,支持“一句话记录”餐食
- 新增语音录制页面,含波形动画、音量反馈与识别结果确认
- FloatingFoodOverlay 新增语音入口,打通拍照/库/语音三种记录方式
- 添加麦克风与语音识别权限描述(iOS Info.plist 与 Android manifest)
- 实现开发者模式:连续三次点击用户名激活,含日志查看、导出与清除
- 新增 logger 工具类,统一日志存储(AsyncStorage)与按级别输出
- 重构 BackgroundTaskManager 为单例并支持 Promise 初始化,避免重复注册
- 移除 sleep-detail 多余渐变背景,改用 ThemedView 统一主题
- 新增通用 haptic 反馈函数,支持多种震动类型(iOS only)
- 升级 expo-background-task、expo-notifications、expo-task-manager 至兼容版本
This commit is contained in:
richarjiang
2025-09-11 19:11:09 +08:00
parent 35d6b74451
commit 3a4a55b78e
17 changed files with 1427 additions and 75 deletions

View File

@@ -1,17 +1,19 @@
import ActivityHeatMap from '@/components/ActivityHeatMap';
import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree';
import { ROUTES } from '@/constants/Routes';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useNotifications } from '@/hooks/useNotifications';
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
import { log } from '@/utils/logger';
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -31,6 +33,11 @@ export default function PersonalScreen() {
const [notificationEnabled, setNotificationEnabled] = useState(false);
// 开发者模式相关状态
const [showDeveloperSection, setShowDeveloperSection] = useState(false);
const clickTimestamps = useRef<number[]>([]);
const clickTimeoutRef = useRef<number | null>(null);
// 计算底部间距
const bottomPadding = useMemo(() => {
return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0);
@@ -86,6 +93,32 @@ export default function PersonalScreen() {
loadNotificationPreference();
}, []);
// 处理用户名连续点击
const handleUserNamePress = () => {
const now = Date.now();
clickTimestamps.current.push(now);
// 清除之前的超时
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
}
// 只保留最近1秒内的点击
clickTimestamps.current = clickTimestamps.current.filter(timestamp => now - timestamp <= 1000);
// 检查是否有3次连续点击
if (clickTimestamps.current.length >= 3) {
setShowDeveloperSection(true);
clickTimestamps.current = []; // 清空点击记录
log.info('开发者模式已激活');
} else {
// 1秒后清空点击记录
clickTimeoutRef.current = setTimeout(() => {
clickTimestamps.current = [];
}, 1000);
}
};
// 处理通知开关变化
const handleNotificationToggle = async (value: boolean) => {
if (value) {
@@ -148,7 +181,9 @@ export default function PersonalScreen() {
/>
</View>
<View style={styles.userDetails}>
<Text style={styles.userName}>{displayName}</Text>
<TouchableOpacity onPress={handleUserNamePress} activeOpacity={0.7}>
<Text style={styles.userName}>{displayName}</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={styles.editButtonText}></Text>
@@ -238,6 +273,17 @@ export default function PersonalScreen() {
},
],
},
// 开发者section需要连续点击三次用户名激活
...(showDeveloperSection ? [{
title: '开发者',
items: [
{
icon: 'code-slash-outline' as const,
title: '开发者选项',
onPress: () => pushIfAuthedElseLogin(ROUTES.DEVELOPER),
},
],
}] : []),
{
title: '其他',
items: [

View File

@@ -13,7 +13,7 @@ import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
@@ -493,7 +493,7 @@ export default function ExploreScreen() {
style={styles.debugButton}
onPress={async () => {
console.log('🔧 手动触发后台任务测试...');
await backgroundTaskManager.triggerTaskForTesting();
await BackgroundTaskManager.getInstance().triggerTaskForTesting();
}}
>
<Text style={styles.debugButtonText}>🔧</Text>