feat: 添加快捷动作功能,支持快速记录饮水量,更新相关配置和服务

This commit is contained in:
richarjiang
2025-09-05 17:17:22 +08:00
parent 6af86800f2
commit aee291bb69
8 changed files with 384 additions and 3 deletions

View File

@@ -68,6 +68,17 @@
"./assets/sounds/notification.wav" "./assets/sounds/notification.wav"
] ]
} }
],
[
"expo-quick-actions",
{
"androidIcons": {
"drink_water": "./assets/images/icons/IconGlass.png"
},
"iosIcons": {
"drink_water": "./assets/images/icons/IconGlass.png"
}
}
] ]
], ],
"experiments": { "experiments": {

View File

@@ -7,9 +7,11 @@ import 'react-native-reanimated';
import PrivacyConsentModal from '@/components/PrivacyConsentModal'; import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useQuickActions } from '@/hooks/useQuickActions';
import { clearAiCoachSessionCache } from '@/services/aiCoachSession'; import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { backgroundTaskManager } from '@/services/backgroundTaskManager'; import { backgroundTaskManager } from '@/services/backgroundTaskManager';
import { notificationService } from '@/services/notifications'; import { notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { store } from '@/store'; import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice'; import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers'; import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
@@ -26,6 +28,9 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false); const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const [userDataLoaded, setUserDataLoaded] = React.useState(false); const [userDataLoaded, setUserDataLoaded] = React.useState(false);
// 初始化快捷动作处理
useQuickActions();
React.useEffect(() => { React.useEffect(() => {
const loadUserData = async () => { const loadUserData = async () => {
await dispatch(rehydrateUser()); await dispatch(rehydrateUser());
@@ -40,8 +45,12 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
// 初始化后台任务管理器 // 初始化后台任务管理器
await backgroundTaskManager.initialize(); await backgroundTaskManager.initialize();
console.log('后台任务管理器初始化成功'); console.log('后台任务管理器初始化成功');
// 初始化快捷动作
await setupQuickActions();
console.log('快捷动作初始化成功');
} catch (error) { } catch (error) {
console.error('通知服务后台任务管理器初始化失败:', error); console.error('通知服务后台任务管理器或快捷动作初始化失败:', error);
} }
}; };

76
hooks/useQuickActions.ts Normal file
View File

@@ -0,0 +1,76 @@
import { useWaterData } from '@/hooks/useWaterData';
import { getInitialQuickAction } from '@/services/quickActions';
import { Toast } from '@/utils/toast.utils';
import * as Haptics from 'expo-haptics';
import * as QuickActions from 'expo-quick-actions';
import { useCallback, useEffect, useRef } from 'react';
export const useQuickActions = () => {
const { addWaterRecord } = useWaterData();
const subscriptionRef = useRef<any>(null);
// 处理快捷动作
const handleQuickAction = useCallback(async (action: any) => {
console.log('处理快捷动作:', action);
// 触发震动反馈
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
// 检查是否是饮水快捷动作
if (action.id?.startsWith('drink_water')) {
const amount = action.params?.amount || 250; // 默认250ml
try {
// 添加饮水记录
const success = await addWaterRecord(amount);
if (success) {
Toast.success(`已记录饮水 ${amount}ml`);
} else {
Toast.error('添加饮水记录失败,请稍后重试');
}
} catch (error) {
console.error('快捷饮水失败:', error);
Toast.error('添加饮水记录失败,请稍后重试');
}
}
}, [addWaterRecord]);
// 设置快捷动作监听器
useEffect(() => {
const setupListener = () => {
try {
// 先处理初始快捷动作(如果应用是通过快捷动作启动的)
const initialAction = getInitialQuickAction();
if (initialAction) {
console.log('检测到初始快捷动作:', initialAction);
handleQuickAction(initialAction);
}
// 设置监听器处理后续的快捷动作
subscriptionRef.current = QuickActions.addListener(async (action) => {
await handleQuickAction(action);
});
console.log('快捷动作监听器已设置');
} catch (error) {
console.error('设置快捷动作监听器失败:', error);
}
};
setupListener();
// 清理函数
return () => {
if (subscriptionRef.current) {
subscriptionRef.current.remove();
console.log('快捷动作监听器已清理');
}
};
}, [handleQuickAction]);
return {
handleQuickAction,
};
};

View File

@@ -92,6 +92,8 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- ExpoQuickActions (5.0.0):
- ExpoModulesCore
- ExpoSplashScreen (0.30.10): - ExpoSplashScreen (0.30.10):
- ExpoModulesCore - ExpoModulesCore
- ExpoSymbols (0.4.5): - ExpoSymbols (0.4.5):
@@ -2011,6 +2013,7 @@ DEPENDENCIES:
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLinking (from `../node_modules/expo-linking/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoQuickActions (from `../node_modules/expo-quick-actions/ios`)
- ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`)
- ExpoSymbols (from `../node_modules/expo-symbols/ios`) - ExpoSymbols (from `../node_modules/expo-symbols/ios`)
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
@@ -2165,6 +2168,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-linking/ios" :path: "../node_modules/expo-linking/ios"
ExpoModulesCore: ExpoModulesCore:
:path: "../node_modules/expo-modules-core" :path: "../node_modules/expo-modules-core"
ExpoQuickActions:
:path: "../node_modules/expo-quick-actions/ios"
ExpoSplashScreen: ExpoSplashScreen:
:path: "../node_modules/expo-splash-screen/ios" :path: "../node_modules/expo-splash-screen/ios"
ExpoSymbols: ExpoSymbols:
@@ -2366,6 +2371,7 @@ SPEC CHECKSUMS:
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902 ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902
ExpoModulesCore: 272bc6c06ddd9c4bee2048acc57891cab3700627 ExpoModulesCore: 272bc6c06ddd9c4bee2048acc57891cab3700627
ExpoQuickActions: fdbda7f5874aed3dd2b1d891ec00ab3300dc7541
ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d

View File

@@ -89,5 +89,53 @@
<string>Light</string> <string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>IconGlass</string>
<key>UIApplicationShortcutItemTitle</key>
<string>喝水</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>快速记录饮水</string>
<key>UIApplicationShortcutItemType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).drink_water</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>amount</key>
<integer>250</integer>
</dict>
</dict>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>IconGlass</string>
<key>UIApplicationShortcutItemTitle</key>
<string>喝水 100ml</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>快速记录饮水</string>
<key>UIApplicationShortcutItemType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).drink_water_100</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>amount</key>
<integer>100</integer>
</dict>
</dict>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>IconGlass</string>
<key>UIApplicationShortcutItemTitle</key>
<string>喝水 200ml</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>快速记录饮水</string>
<key>UIApplicationShortcutItemType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).drink_water_200</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>amount</key>
<integer>200</integer>
</dict>
</dict>
</array>
</dict> </dict>
</plist> </plist>

116
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"expo-linear-gradient": "^14.1.5", "expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-notifications": "~0.31.4", "expo-notifications": "~0.31.4",
"expo-quick-actions": "^5.0.0",
"expo-router": "~5.1.5", "expo-router": "~5.1.5",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
@@ -3919,7 +3920,6 @@
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json5": { "node_modules/@types/json5": {
@@ -4651,6 +4651,42 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://mirrors.tencent.com/npm/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://mirrors.tencent.com/npm/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://mirrors.tencent.com/npm/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/anser": { "node_modules/anser": {
"version": "1.4.10", "version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -7286,6 +7322,20 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-quick-actions": {
"version": "5.0.0",
"resolved": "https://mirrors.tencent.com/npm/expo-quick-actions/-/expo-quick-actions-5.0.0.tgz",
"integrity": "sha512-NSsDhfbal11gXsHkJbvYVw7x0QUCKrEth2kBBKZUv03dX4J7ZPADSV89LyEpOVYXCkrw6LuanlEtKavg/BFaRA==",
"license": "MIT",
"dependencies": {
"@expo/image-utils": "~0.7.4",
"schema-utils": "^4.2.0",
"sf-symbols-typescript": "^2.1.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-router": { "node_modules/expo-router": {
"version": "5.1.5", "version": "5.1.5",
"resolved": "https://mirrors.tencent.com/npm/expo-router/-/expo-router-5.1.5.tgz", "resolved": "https://mirrors.tencent.com/npm/expo-router/-/expo-router-5.1.5.tgz",
@@ -7470,6 +7520,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://mirrors.tencent.com/npm/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/fast-xml-parser": { "node_modules/fast-xml-parser": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://mirrors.tencent.com/npm/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", "resolved": "https://mirrors.tencent.com/npm/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
@@ -12376,6 +12441,55 @@
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/schema-utils": {
"version": "4.3.2",
"resolved": "https://mirrors.tencent.com/npm/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://mirrors.tencent.com/npm/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://mirrors.tencent.com/npm/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://mirrors.tencent.com/npm/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",

View File

@@ -37,6 +37,7 @@
"expo-linear-gradient": "^14.1.5", "expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-notifications": "~0.31.4", "expo-notifications": "~0.31.4",
"expo-quick-actions": "^5.0.0",
"expo-router": "~5.1.5", "expo-router": "~5.1.5",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",

116
services/quickActions.ts Normal file
View File

@@ -0,0 +1,116 @@
import { getQuickWaterAmount } from '@/utils/userPreferences';
import * as QuickActions from 'expo-quick-actions';
import { Platform } from 'react-native';
// 快捷动作类型定义
export interface QuickActionItem {
id: string;
title: string;
subtitle?: string;
icon?: string;
params?: Record<string, any>;
}
// 饮水快捷动作配置
export const QUICK_ACTIONS = {
DRINK_WATER_100: {
id: 'drink_water_100',
title: '喝水 100ml',
subtitle: '快速记录饮水',
icon: Platform.OS === 'ios' ? 'drink_water' : 'drink_water',
params: { amount: 100 }
},
DRINK_WATER_200: {
id: 'drink_water_200',
title: '喝水 200ml',
subtitle: '快速记录饮水',
icon: Platform.OS === 'ios' ? 'drink_water' : 'drink_water',
params: { amount: 200 }
},
DRINK_WATER_250: {
id: 'drink_water_250',
title: '喝水 250ml',
subtitle: '快速记录饮水',
icon: Platform.OS === 'ios' ? 'drink_water' : 'drink_water',
params: { amount: 250 }
},
DRINK_WATER_CUSTOM: {
id: 'drink_water_custom',
title: '喝水',
subtitle: '自定义饮水量',
icon: Platform.OS === 'ios' ? 'drink_water' : 'drink_water',
params: { custom: true }
}
};
// 设置快捷动作
export const setupQuickActions = async () => {
try {
// 获取用户设置的快速饮水量
const quickAmount = await getQuickWaterAmount();
console.log('设置快捷动作,快速饮水量:', quickAmount);
// 创建快捷动作列表
const actions: QuickActionItem[] = [
// 使用用户设置的快速饮水量
{
...QUICK_ACTIONS.DRINK_WATER_CUSTOM,
title: `喝水 ${quickAmount}ml`,
subtitle: '快速记录饮水',
params: { amount: quickAmount }
},
// 固定选项
QUICK_ACTIONS.DRINK_WATER_100,
QUICK_ACTIONS.DRINK_WATER_200,
QUICK_ACTIONS.DRINK_WATER_250
];
// 设置快捷动作
await QuickActions.setItems(actions);
console.log('快捷动作设置成功');
} catch (error) {
console.error('设置快捷动作失败:', error);
}
};
// 清除所有快捷动作
export const clearQuickActions = async () => {
try {
await QuickActions.setItems([]);
console.log('快捷动作已清除');
} catch (error) {
console.error('清除快捷动作失败:', error);
}
};
// 获取初始快捷动作(如果应用是通过快捷动作启动的)
export const getInitialQuickAction = () => {
try {
return QuickActions.initial;
} catch (error) {
console.error('获取初始快捷动作失败:', error);
return null;
}
};
// 添加快捷动作事件监听器
export const addQuickActionListener = (callback: (action: QuickActionItem) => void) => {
const subscription = QuickActions.addListener(callback);
return subscription;
};
// 移除快捷动作事件监听器
export const removeQuickActionListener = (subscription: any) => {
if (subscription) {
subscription.remove();
}
};
// 检查快捷动作是否可用
export const isQuickActionsAvailable = async () => {
try {
return await QuickActions.isSupported();
} catch (error) {
return false;
}
};