From aee291bb696c21703119d0a0dfd7f6762a8461a6 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 5 Sep 2025 17:17:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E8=AE=B0=E5=BD=95=E9=A5=AE=E6=B0=B4=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.json | 11 ++++ app/_layout.tsx | 11 +++- hooks/useQuickActions.ts | 76 ++++++++++++++++++++++ ios/Podfile.lock | 6 ++ ios/digitalpilates/Info.plist | 48 ++++++++++++++ package-lock.json | 116 +++++++++++++++++++++++++++++++++- package.json | 3 +- services/quickActions.ts | 116 ++++++++++++++++++++++++++++++++++ 8 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 hooks/useQuickActions.ts create mode 100644 services/quickActions.ts diff --git a/app.json b/app.json index 7bd5ea3..aa51e43 100644 --- a/app.json +++ b/app.json @@ -68,6 +68,17 @@ "./assets/sounds/notification.wav" ] } + ], + [ + "expo-quick-actions", + { + "androidIcons": { + "drink_water": "./assets/images/icons/IconGlass.png" + }, + "iosIcons": { + "drink_water": "./assets/images/icons/IconGlass.png" + } + } ] ], "experiments": { diff --git a/app/_layout.tsx b/app/_layout.tsx index fc7c1ee..f5ae919 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -7,9 +7,11 @@ import 'react-native-reanimated'; import PrivacyConsentModal from '@/components/PrivacyConsentModal'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useQuickActions } from '@/hooks/useQuickActions'; import { clearAiCoachSessionCache } from '@/services/aiCoachSession'; import { backgroundTaskManager } from '@/services/backgroundTaskManager'; import { notificationService } from '@/services/notifications'; +import { setupQuickActions } from '@/services/quickActions'; import { store } from '@/store'; import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice'; import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers'; @@ -26,6 +28,9 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { const [showPrivacyModal, setShowPrivacyModal] = React.useState(false); const [userDataLoaded, setUserDataLoaded] = React.useState(false); + // 初始化快捷动作处理 + useQuickActions(); + React.useEffect(() => { const loadUserData = async () => { await dispatch(rehydrateUser()); @@ -40,8 +45,12 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { // 初始化后台任务管理器 await backgroundTaskManager.initialize(); console.log('后台任务管理器初始化成功'); + + // 初始化快捷动作 + await setupQuickActions(); + console.log('快捷动作初始化成功'); } catch (error) { - console.error('通知服务或后台任务管理器初始化失败:', error); + console.error('通知服务、后台任务管理器或快捷动作初始化失败:', error); } }; diff --git a/hooks/useQuickActions.ts b/hooks/useQuickActions.ts new file mode 100644 index 0000000..d814a7e --- /dev/null +++ b/hooks/useQuickActions.ts @@ -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(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, + }; +}; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b94256e..557c29b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -92,6 +92,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoQuickActions (5.0.0): + - ExpoModulesCore - ExpoSplashScreen (0.30.10): - ExpoModulesCore - ExpoSymbols (0.4.5): @@ -2011,6 +2013,7 @@ DEPENDENCIES: - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoQuickActions (from `../node_modules/expo-quick-actions/ios`) - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) - ExpoSymbols (from `../node_modules/expo-symbols/ios`) - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) @@ -2165,6 +2168,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-linking/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" + ExpoQuickActions: + :path: "../node_modules/expo-quick-actions/ios" ExpoSplashScreen: :path: "../node_modules/expo-splash-screen/ios" ExpoSymbols: @@ -2366,6 +2371,7 @@ SPEC CHECKSUMS: ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902 ExpoModulesCore: 272bc6c06ddd9c4bee2048acc57891cab3700627 + ExpoQuickActions: fdbda7f5874aed3dd2b1d891ec00ab3300dc7541 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index a7c4107..3dae8a1 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -89,5 +89,53 @@ Light UIViewControllerBasedStatusBarAppearance + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconFile + IconGlass + UIApplicationShortcutItemTitle + 喝水 + UIApplicationShortcutItemSubtitle + 快速记录饮水 + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).drink_water + UIApplicationShortcutItemUserInfo + + amount + 250 + + + + UIApplicationShortcutItemIconFile + IconGlass + UIApplicationShortcutItemTitle + 喝水 100ml + UIApplicationShortcutItemSubtitle + 快速记录饮水 + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).drink_water_100 + UIApplicationShortcutItemUserInfo + + amount + 100 + + + + UIApplicationShortcutItemIconFile + IconGlass + UIApplicationShortcutItemTitle + 喝水 200ml + UIApplicationShortcutItemSubtitle + 快速记录饮水 + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).drink_water_200 + UIApplicationShortcutItemUserInfo + + amount + 200 + + + diff --git a/package-lock.json b/package-lock.json index 557171f..264a922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "expo-linear-gradient": "^14.1.5", "expo-linking": "~7.1.7", "expo-notifications": "~0.31.4", + "expo-quick-actions": "^5.0.0", "expo-router": "~5.1.5", "expo-splash-screen": "~0.30.10", "expo-status-bar": "~2.2.3", @@ -3919,7 +3920,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -4651,6 +4651,42 @@ "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": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", @@ -7286,6 +7322,20 @@ "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": { "version": "5.1.5", "resolved": "https://mirrors.tencent.com/npm/expo-router/-/expo-router-5.1.5.tgz", @@ -7470,6 +7520,21 @@ "dev": true, "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": { "version": "4.5.0", "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==", "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": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/package.json b/package.json index e9abea4..7519bb7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "expo-linear-gradient": "^14.1.5", "expo-linking": "~7.1.7", "expo-notifications": "~0.31.4", + "expo-quick-actions": "^5.0.0", "expo-router": "~5.1.5", "expo-splash-screen": "~0.30.10", "expo-status-bar": "~2.2.3", @@ -78,4 +79,4 @@ "typescript": "~5.8.3" }, "private": true -} \ No newline at end of file +} diff --git a/services/quickActions.ts b/services/quickActions.ts new file mode 100644 index 0000000..aeac99b --- /dev/null +++ b/services/quickActions.ts @@ -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; +} + +// 饮水快捷动作配置 +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; + } +};