diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index df122a3..f487ad2 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -12,7 +12,9 @@ import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { ROUTES } from '@/constants/Routes'; import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar'; +import { useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { selectEnabledTabs } from '@/store/tabBarConfigSlice'; // Tab configuration type TabConfig = { @@ -34,6 +36,9 @@ export default function TabLayout() { const colorTokens = Colors[theme]; const pathname = usePathname(); const glassEffectAvailable = isLiquidGlassAvailable(); + + // 获取已启用的标签配置(按自定义顺序) + const enabledTabs = useAppSelector(selectEnabledTabs); // Helper function to determine if a tab is selected const isTabSelected = (routeName: string): boolean => { @@ -174,42 +179,45 @@ export default function TabLayout() { tabBarShowLabel: false, }); + // 根据配置渲染标签页 if (glassEffectAvailable) { - return - - - - - - - - - - - - - - - - - - - - - + return ( + + {enabledTabs.map((tab) => { + const tabConfig = TAB_CONFIGS[tab.id]; + if (!tabConfig) return null; + + return ( + + + + + ); + })} + + ); } + // 确定初始路由(第一个启用的标签) + const initialRouteName = enabledTabs.length > 0 ? enabledTabs[0].id : 'statistics'; + return ( getScreenOptions(route.name)} > - - - - - - + {enabledTabs.map((tab) => { + const tabConfig = TAB_CONFIGS[tab.id]; + if (!tabConfig) return null; + + return ( + + ); + })} ); } diff --git a/app/(tabs)/medications.tsx b/app/(tabs)/medications.tsx index 7420d16..88178fc 100644 --- a/app/(tabs)/medications.tsx +++ b/app/(tabs)/medications.tsx @@ -1,6 +1,7 @@ import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation'; import { DateSelector } from '@/components/DateSelector'; import { MedicationCard } from '@/components/medication/MedicationCard'; +import { TakenMedicationsStack } from '@/components/medication/TakenMedicationsStack'; import { ThemedText } from '@/components/ThemedText'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { MedicalDisclaimerSheet } from '@/components/ui/MedicalDisclaimerSheet'; @@ -189,6 +190,16 @@ export default function MedicationsScreen() { return medicationsWithImages.filter((item: any) => item.status === activeFilter); }, [activeFilter, medicationsWithImages]); + const activeMedications = useMemo(() => { + if (activeFilter !== 'all') return filteredMedications; + return filteredMedications.filter((item: any) => item.status !== 'taken' && item.status !== 'skipped'); + }, [activeFilter, filteredMedications]); + + const completedMedications = useMemo(() => { + if (activeFilter !== 'all') return []; + return filteredMedications.filter((item: any) => item.status === 'taken' || item.status === 'skipped'); + }, [activeFilter, filteredMedications]); + const counts = useMemo(() => { const taken = medicationsWithImages.filter((item: any) => item.status === 'taken').length; // "未服用"计数包含 missed(已错过)和 upcoming(待服用) @@ -354,7 +365,8 @@ export default function MedicationsScreen() { ) : ( - {filteredMedications.map((item: any) => ( + {/* 渲染未服用的药物 */} + {activeMedications.map((item: any) => ( ))} + + {/* 渲染已完成(服用/跳过)的药物堆叠 */} + {completedMedications.length > 0 && ( + handleOpenMedicationDetails(item.medicationId)} + onCelebrate={handleMedicationTakenCelebration} + /> + )} )} diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index a43f50f..e107b81 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -1,6 +1,7 @@ import ActivityHeatMap from '@/components/ActivityHeatMap'; import { BadgeShowcaseModal } from '@/components/badges/BadgeShowcaseModal'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; +import { palette } from '@/constants/Colors'; import { ROUTES } from '@/constants/Routes'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; @@ -586,6 +587,16 @@ export default function PersonalScreen() { }, ], }, + { + title: t('personal.sections.customization'), + items: [ + { + icon: 'albums-outline' as React.ComponentProps['name'], + title: t('personal.menu.tabBarConfig'), + onPress: () => router.push(ROUTES.TAB_BAR_CONFIG), + }, + ], + }, // 开发者section(需要连续点击三次用户名激活) ...(showDeveloperSection ? [{ title: t('personal.sections.developer'), @@ -698,16 +709,12 @@ export default function PersonalScreen() { {/* 背景渐变 */} - {/* 装饰性圆圈 */} - - - {/* 左边logo */} diff --git a/app/_layout.tsx b/app/_layout.tsx index 436cc66..3f78be0 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -35,6 +35,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard'; import { STORAGE_KEYS, setUnauthorizedHandler } from '@/services/api'; import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2'; import { fetchChallenges } from '@/store/challengesSlice'; +import { loadTabBarConfigs } from '@/store/tabBarConfigSlice'; import AsyncStorage from '@/utils/kvStore'; import { logger } from '@/utils/logger'; import { Provider } from 'react-redux'; @@ -120,6 +121,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } }, [isLoggedIn]); + // 初始化底部栏配置 + useEffect(() => { + dispatch(loadTabBarConfigs()); + }, [dispatch]); + // ==================== 基础服务初始化(不需要权限,总是执行)==================== React.useEffect(() => { const initializeBasicServices = async () => { @@ -515,6 +521,7 @@ export default function RootLayout() { options={{ headerShown: false }} /> + diff --git a/app/medications/[medicationId].tsx b/app/medications/[medicationId].tsx index f537695..050d278 100644 --- a/app/medications/[medicationId].tsx +++ b/app/medications/[medicationId].tsx @@ -34,6 +34,7 @@ import { Image } from 'expo-image'; import * as ImagePicker from 'expo-image-picker'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; +import LottieView from 'lottie-react-native'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, @@ -1299,8 +1300,13 @@ export default function MedicationDetailScreen() { {aiAnalysisLoading && ( - - + + {t('medications.detail.aiAnalysis.analyzing')} @@ -2276,13 +2282,19 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '700', }, - aiLoadingRow: { - flexDirection: 'row', + aiLoadingContainer: { alignItems: 'center', - gap: 8, + justifyContent: 'center', + paddingVertical: 24, + gap: 12, + }, + aiLoadingAnimation: { + width: 120, + height: 120, }, aiLoadingText: { - fontSize: 13, + fontSize: 14, + fontWeight: '500', }, aiHeroRow: { flexDirection: 'row', diff --git a/app/settings/tab-bar-config.tsx b/app/settings/tab-bar-config.tsx new file mode 100644 index 0000000..c1fd530 --- /dev/null +++ b/app/settings/tab-bar-config.tsx @@ -0,0 +1,266 @@ +import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; +import { + resetToDefault, + selectTabBarConfigs, + toggleTabEnabled, + type TabConfig, +} from '@/store/tabBarConfigSlice'; +import { Ionicons } from '@expo/vector-icons'; +import { isLiquidGlassAvailable } from 'expo-glass-effect'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useRouter } from 'expo-router'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Alert, + ScrollView, + StyleSheet, + Switch, + Text, + TouchableOpacity, + View +} from 'react-native'; + +import { HeaderBar } from '@/components/ui/HeaderBar'; +import { IconSymbol } from '@/components/ui/IconSymbol'; +import { palette } from '@/constants/Colors'; + +export default function TabBarConfigScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const dispatch = useAppDispatch(); + const safeAreaTop = useSafeAreaTop(60); + const configs = useAppSelector(selectTabBarConfigs); + const isGlassAvailable = isLiquidGlassAvailable(); + + // 处理开关切换 + const handleToggle = useCallback( + (tabId: string) => { + dispatch(toggleTabEnabled(tabId)); + }, + [dispatch] + ); + + // 恢复默认设置 + const handleReset = useCallback(() => { + Alert.alert( + t('personal.tabBarConfig.resetConfirm.title'), + t('personal.tabBarConfig.resetConfirm.message'), + [ + { + text: t('personal.tabBarConfig.resetConfirm.cancel'), + style: 'cancel', + }, + { + text: t('personal.tabBarConfig.resetConfirm.confirm'), + style: 'destructive', + onPress: () => { + dispatch(resetToDefault()); + Alert.alert('', t('personal.tabBarConfig.resetSuccess')); + }, + }, + ] + ); + }, [dispatch, t]); + + // 渲染单个 Tab 行 + const renderTabRow = useCallback( + (item: TabConfig, index: number, total: number) => { + return ( + + + {/* Tab 图标和名称 */} + + + + + + {t(item.titleKey)} + {!item.canBeDisabled && ( + + {t('personal.tabBarConfig.cannotDisable')} + + )} + + + + {/* 开关 */} + handleToggle(item.id)} + disabled={!item.canBeDisabled} + trackColor={{ false: '#E5E5E5', true: '#9370DB' }} + thumbColor="#FFFFFF" + style={styles.switch} + /> + + + {/* 分割线 - 最后一项不显示 */} + {index < total - 1 && ( + + + + )} + + ); + }, + [handleToggle, t] + ); + + return ( + + + + {/* 顶部导航栏 */} + router.back()} + right={ + + + {t('personal.tabBarConfig.resetButton')} + + + } + /> + + {/* 主内容区 */} + + {/* 说明区域 */} + + {t('personal.tabBarConfig.subtitle')} + + + + + {t('personal.tabBarConfig.description')} + + + + + + {/* Tab 列表 - 聚合在一个卡片中 */} + + {configs.map((item, index) => renderTabRow(item, index, configs.length))} + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + gradientBackground: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + height: '60%', // 渐变覆盖上半部分即可 + }, + content: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: 16, + paddingBottom: 40, + }, + headerSection: { + marginBottom: 16, + }, + subtitle: { + fontSize: 14, + color: '#6C757D', + marginBottom: 12, + }, + descriptionCard: { + backgroundColor: 'rgba(255, 255, 255, 0.6)', + borderRadius: 12, + padding: 12, + gap: 8, + borderWidth: 1, + borderColor: 'rgba(147, 112, 219, 0.1)', + }, + hintRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + descriptionText: { + flex: 1, + fontSize: 13, + color: '#2C3E50', + lineHeight: 18, + }, + sectionContainer: { + backgroundColor: '#FFFFFF', + borderRadius: 20, + marginBottom: 20, + overflow: 'hidden', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.03, + shadowRadius: 8, + elevation: 2, + }, + tabItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + paddingVertical: 16, + }, + separatorContainer: { + paddingLeft: 68, // 40(icon) + 12(gap) + 16(padding) + paddingRight: 16, + }, + separator: { + height: 1, + backgroundColor: '#F0F0F0', + }, + tabInfo: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + iconContainer: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + tabTextContainer: { + flex: 1, + }, + tabTitle: { + fontSize: 16, + fontWeight: '500', + color: '#2C3E50', + marginBottom: 2, + }, + tabSubtitle: { + fontSize: 12, + color: '#9370DB', + }, + switch: { + transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }], + }, + headerRightButton: { + fontSize: 15, + fontWeight: '600', + color: '#9370DB', // 使用主色调 + }, +}); \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png index 03b906b..cf3a6a8 100644 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/assets/lottie/loading-blue.json b/assets/lottie/loading-blue.json new file mode 100644 index 0000000..deadbc9 --- /dev/null +++ b/assets/lottie/loading-blue.json @@ -0,0 +1 @@ +{"v":"5.6.10","fr":30,"ip":30,"op":210,"w":800,"h":600,"nm":"合成 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":340,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":30,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":320,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":40,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0]},{"t":50,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":20,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"形状图层 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":280,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[0]},{"t":60,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":30,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"形状图层 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":260,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0]},{"t":70,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":40,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"形状图层 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[0]},{"t":80,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":50,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"形状图层 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":220,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[0]},{"t":90,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":60,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"形状图层 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":200,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":70,"s":[0]},{"t":100,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":70,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"形状图层 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":80,"s":[0]},{"t":110,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":80,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"形状图层 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":160,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[0]},{"t":120,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":90,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"形状图层 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":140,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":100,"s":[0]},{"t":130,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":100,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"形状图层 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":110,"s":[0]},{"t":140,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":110,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"形状图层 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":100,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"t":150,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":420,"st":120,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"形状图层 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":80,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":130,"s":[0]},{"t":160,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":430,"st":130,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"形状图层 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":140,"s":[0]},{"t":170,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":440,"st":140,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"形状图层 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":40,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":150,"s":[0]},{"t":180,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":450,"st":150,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"形状图层 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":20,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":160,"s":[0]},{"t":190,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":460,"st":160,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":170,"s":[0]},{"t":200,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":78,"op":470,"st":170,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"形状图层 24","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":340,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[0]},{"t":210,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":180,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"形状图层 23","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":320,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":190,"s":[0]},{"t":220,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":190,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"形状图层 22","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":200,"s":[0]},{"t":230,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":200,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"形状图层 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":280,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":210,"s":[0]},{"t":240,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":210,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"形状图层 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":260,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":220,"s":[0]},{"t":250,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":220,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"形状图层 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[1.258,2.078,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[200,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":5,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":230,"s":[0]},{"t":260,"s":[120]}],"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.09019607843137255,0.3843137254901961,0.8745098039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[1.258,2.078],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":301,"st":230,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"预合成 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,300,0],"ix":2},"a":{"a":0,"k":[400,300,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"简单阻塞工具","np":4,"mn":"ADBE Simple Choker","ix":1,"en":1,"ef":[{"ty":7,"nm":"视图","mn":"ADBE Simple Choker-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":0,"nm":"阻塞遮罩","mn":"ADBE Simple Choker-0002","ix":2,"v":{"a":0,"k":14,"ix":2}}]},{"ty":5,"nm":"梯度渐变","np":10,"mn":"ADBE Ramp","ix":2,"en":1,"ef":[{"ty":3,"nm":"渐变起点","mn":"ADBE Ramp-0001","ix":1,"v":{"a":0,"k":[400,0],"ix":1}},{"ty":2,"nm":"起始颜色","mn":"ADBE Ramp-0002","ix":2,"v":{"a":0,"k":[0.263066768646,1,0.867243647575,1],"ix":2}},{"ty":3,"nm":"渐变终点","mn":"ADBE Ramp-0003","ix":3,"v":{"a":0,"k":[400,600],"ix":3}},{"ty":2,"nm":"结束颜色","mn":"ADBE Ramp-0004","ix":4,"v":{"a":0,"k":[0.354580283165,1,0.820018112659,1],"ix":4}},{"ty":7,"nm":"渐变形状","mn":"ADBE Ramp-0005","ix":5,"v":{"a":0,"k":1,"ix":5}},{"ty":0,"nm":"渐变散射","mn":"ADBE Ramp-0006","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"与原始图像混合","mn":"ADBE Ramp-0007","ix":7,"v":{"a":0,"k":0,"ix":7}},{"ty":6,"nm":"","mn":"ADBE Ramp-0008","ix":8,"v":0}]}],"w":800,"h":600,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"预合成 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":56,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,320,0],"ix":2},"a":{"a":0,"k":[400,300,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"简单阻塞工具","np":4,"mn":"ADBE Simple Choker","ix":1,"en":1,"ef":[{"ty":7,"nm":"视图","mn":"ADBE Simple Choker-0001","ix":1,"v":{"a":0,"k":1,"ix":1}},{"ty":0,"nm":"阻塞遮罩","mn":"ADBE Simple Choker-0002","ix":2,"v":{"a":0,"k":14,"ix":2}}]},{"ty":5,"nm":"梯度渐变","np":10,"mn":"ADBE Ramp","ix":2,"en":1,"ef":[{"ty":3,"nm":"渐变起点","mn":"ADBE Ramp-0001","ix":1,"v":{"a":0,"k":[400,0],"ix":1}},{"ty":2,"nm":"起始颜色","mn":"ADBE Ramp-0002","ix":2,"v":{"a":0,"k":[0.263066768646,1,0.867243647575,1],"ix":2}},{"ty":3,"nm":"渐变终点","mn":"ADBE Ramp-0003","ix":3,"v":{"a":0,"k":[400,600],"ix":3}},{"ty":2,"nm":"结束颜色","mn":"ADBE Ramp-0004","ix":4,"v":{"a":0,"k":[0.354580283165,1,0.820018112659,1],"ix":4}},{"ty":7,"nm":"渐变形状","mn":"ADBE Ramp-0005","ix":5,"v":{"a":0,"k":1,"ix":5}},{"ty":0,"nm":"渐变散射","mn":"ADBE Ramp-0006","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"与原始图像混合","mn":"ADBE Ramp-0007","ix":7,"v":{"a":0,"k":0,"ix":7}},{"ty":6,"nm":"","mn":"ADBE Ramp-0008","ix":8,"v":0}]},{"ty":29,"nm":"高斯模糊","np":5,"mn":"ADBE Gaussian Blur 2","ix":3,"en":1,"ef":[{"ty":0,"nm":"模糊度","mn":"ADBE Gaussian Blur 2-0001","ix":1,"v":{"a":0,"k":41.3,"ix":1}},{"ty":7,"nm":"模糊方向","mn":"ADBE Gaussian Blur 2-0002","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"nm":"重复边缘像素","mn":"ADBE Gaussian Blur 2-0003","ix":3,"v":{"a":0,"k":0,"ix":3}}]}],"w":800,"h":600,"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/machine.png b/assets/machine.png new file mode 100644 index 0000000..0882900 Binary files /dev/null and b/assets/machine.png differ diff --git a/components/medication/MedicationCard.tsx b/components/medication/MedicationCard.tsx index 6b71f3a..aae7b55 100644 --- a/components/medication/MedicationCard.tsx +++ b/components/medication/MedicationCard.tsx @@ -327,11 +327,11 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails const styles = StyleSheet.create({ card: { - borderRadius: 18, + borderRadius: 24, position: 'relative', }, cardSurface: { - borderRadius: 18, + borderRadius: 24, overflow: 'hidden', }, cardBody: { @@ -354,7 +354,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', overflow: 'hidden', - borderRadius: 18, + borderRadius: 24, }, thumbnailImage: { width: '70%', diff --git a/components/medication/TakenMedicationsStack.tsx b/components/medication/TakenMedicationsStack.tsx new file mode 100644 index 0000000..4a5d926 --- /dev/null +++ b/components/medication/TakenMedicationsStack.tsx @@ -0,0 +1,272 @@ +import { useI18n } from '@/hooks/useI18n'; +import type { MedicationDisplayItem } from '@/types/medication'; +import React, { useEffect } from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import Animated, { + Extrapolation, + interpolate, + type SharedValue, + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; +import { MedicationCard } from './MedicationCard'; + +type Props = { + medications: MedicationDisplayItem[]; + colors: (typeof import('@/constants/Colors').Colors)[keyof typeof import('@/constants/Colors').Colors]; + selectedDate: any; + onOpenDetails: (medication: MedicationDisplayItem) => void; + onCelebrate?: () => void; +}; + +const STACK_OFFSET = 12; +const STACK_SCALE_STEP = 0.04; +const MAX_STACK_VISIBLE = 3; + +export function TakenMedicationsStack({ + medications, + colors, + selectedDate, + onOpenDetails, + onCelebrate, +}: Props) { + const { t } = useI18n(); + const [isExpanded, setIsExpanded] = React.useState(false); + const progress = useSharedValue(0); + + useEffect(() => { + progress.value = withSpring(isExpanded ? 1 : 0, { + damping: 20, + stiffness: 200, // Faster spring + mass: 0.8, + }); + }, [isExpanded, progress]); + + const handleToggle = () => { + setIsExpanded(!isExpanded); + }; + + // Header arrow rotation style + const arrowStyle = useAnimatedStyle(() => { + return { + transform: [ + { + rotate: `${interpolate(progress.value, [0, 1], [0, 180])}deg`, + }, + ], + }; + }); + + if (medications.length === 0) { + return null; + } + + return ( + + {/* Stack/List Container */} + + {medications.map((item, index) => ( + + ))} + + + ); +} + +const CardItem = ({ + item, + index, + total, + progress, + isExpanded, + colors, + selectedDate, + onOpenDetails, + onCelebrate, + onToggle, +}: { + item: MedicationDisplayItem; + index: number; + total: number; + progress: SharedValue; + isExpanded: boolean; + colors: (typeof import('@/constants/Colors').Colors)[keyof typeof import('@/constants/Colors').Colors]; + selectedDate: any; + onOpenDetails: (medication: MedicationDisplayItem) => void; + onCelebrate?: () => void; + onToggle: () => void; +}) => { + // Only render top 3 cards when collapsed to save performance/visuals + // But we need to render all when expanding. + // We'll hide index >= MAX_STACK_VISIBLE when collapsed via opacity/zIndex. + + const style = useAnimatedStyle(() => { + // Stack state (progress = 0) + const stackTranslateY = index * STACK_OFFSET; + const stackScale = 1 - index * STACK_SCALE_STEP; + const stackOpacity = index < MAX_STACK_VISIBLE ? 1 - index * 0.15 : 0; + const stackZIndex = total - index; + + // List state (progress = 1) + // In list state, we rely on layout (relative positioning). + // However, to animate smoothly from absolute (stack) to relative (list), + // we need a strategy. + // Strategy: Always Absolute? No, height is dynamic. + // Strategy: Use negative margins for stack? + + // Let's try: + // Collapsed: marginTop = -(height - offset). + // Expanded: marginTop = 16 (gap). + // But we don't know height. + + // Alternative: + // Use 'top' offset relative to the first card? + // This is hard without measuring. + + // Let's go with the "Transform" approach assuming standard card height for the stack effect, + // but switching to relative layout when expanded. + // Wait, switching 'position' prop is not animatable by useAnimatedStyle directly (requires Layout Animation). + + // Let's keep it simple: + // When collapsed (progress 0): + // Items > 0 are absolutely positioned relative to the container (which wraps them all). + // Item 0 is relative. + // When expanded (progress 1): + // All items are relative. + + // To smooth this, we can use interpolate for translateY. + + return { + zIndex: stackZIndex, + opacity: interpolate(progress.value, [0, 1], [stackOpacity, 1]), + transform: [ + { + scale: interpolate(progress.value, [0, 1], [stackScale, 1]), + }, + { + translateY: interpolate( + progress.value, + [0, 1], + [stackTranslateY, 0] // In stack, they go down. In list, translation is 0 (relative flow handles pos). + ), + }, + ], + }; + }); + + // Logic for positioning: + // We'll use a container View for each card. + // When collapsed, the container height for index > 0 should be 0? + // That would pull them up. + + const containerStyle = useAnimatedStyle(() => { + // We can animate the height of the wrapper view. + // But we don't know the content height. + // Assuming ~140px for card. + const approxHeight = 140; + + if (index === 0) return {}; // First card always takes space + + // For others: + // Collapsed: height is 0 (so they stack on top of first one, roughly) + // Expanded: height is 'auto' (we can't animate to auto easily in RN without LayoutAnimation) + + return { + marginTop: interpolate(progress.value, [0, 1], [-approxHeight + STACK_OFFSET, 16], Extrapolation.CLAMP), + }; + }); + + // Using Layout Animation for the actual position change support + // requires the parent to handle it. + + // Simpler Visual Hack: + // When collapsed, we just set marginTop to a negative value that overlaps them. + // Since MedicationCard is roughly constant height, we can tune this. + // MedicationCard height is roughly 130-150. + // Let's guess -130 + 12. + + const cardContainerStyle = useAnimatedStyle(() => { + // We assume a fixed height for the negative margin calculation logic. + // A better way is needed if heights vary wildly. + // But for now, let's use a safe estimated overlap. + const cardHeight = 140; + const collapsedMarginTop = index === 0 ? 0 : -(cardHeight - STACK_OFFSET); + const expandedMarginTop = index === 0 ? 0 : 16; + + return { + marginTop: interpolate(progress.value, [0, 1], [collapsedMarginTop, expandedMarginTop]), + zIndex: total - index, + }; + }); + + return ( + + {/* When collapsed, clicking any card should expand. When expanded, open details. */} + {/* We can intercept touches if !isExpanded */} + + {/* Overlay to intercept clicks when collapsed */} + {!isExpanded && ( + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginTop: 8, + gap: 12, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 8, + paddingHorizontal: 4, + }, + headerContent: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + iconContainer: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 16, + fontWeight: '600', + }, + stackContainer: { + position: 'relative', + // minHeight ensures space for the stack when collapsed + }, +}); \ No newline at end of file diff --git a/constants/Routes.ts b/constants/Routes.ts index eba78bb..85fa029 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -83,6 +83,9 @@ export const ROUTES = { // 药品相关路由 MEDICATION_EDIT_FREQUENCY: '/medications/edit-frequency', MEDICATION_MANAGE: '/medications/manage-medications', + + // 底部栏配置路由 + TAB_BAR_CONFIG: '/settings/tab-bar-config', } as const; // 路由参数常量 diff --git a/i18n/index.ts b/i18n/index.ts index be0dda2..cfbc577 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -47,6 +47,7 @@ const personalScreenResources = { language: '语言', healthData: '健康数据授权', medicalSources: '医学建议来源', + customization: '个性化', }, menu: { notificationSettings: '通知设置', @@ -59,6 +60,7 @@ const personalScreenResources = { deleteAccount: '注销帐号', healthDataPermissions: '健康数据授权说明', whoSource: '世界卫生组织 (WHO)', + tabBarConfig: '底部栏配置', }, language: { title: '语言', @@ -77,6 +79,20 @@ const personalScreenResources = { }, }, }, + tabBarConfig: { + title: '底部栏配置', + subtitle: '自定义你的底部导航栏', + description: '使用开关控制标签的显示和隐藏', + resetButton: '恢复默认', + cannotDisable: '此标签不可关闭', + resetConfirm: { + title: '恢复默认设置?', + message: '将重置所有底部栏配置和显示状态', + cancel: '取消', + confirm: '确认恢复', + }, + resetSuccess: '已恢复默认设置', + }, }; const badgesScreenResources = { @@ -458,6 +474,9 @@ const medicationsResources = { title: '今日暂无用药安排', subtitle: '还未添加任何用药计划,快来补充吧。', }, + stack: { + completed: '已完成 ({{count}})', + }, dateFormats: { today: '今天,{{date}}', other: '{{date}}', @@ -894,6 +913,7 @@ const resources = { language: 'Language', healthData: 'Health data permissions', medicalSources: 'Medical Advice Sources', + customization: 'Customization', }, menu: { notificationSettings: 'Notification settings', @@ -906,6 +926,7 @@ const resources = { deleteAccount: 'Delete account', healthDataPermissions: 'Health data disclosure', whoSource: 'World Health Organization (WHO)', + tabBarConfig: 'Tab Bar Settings', }, language: { title: 'Language', @@ -1224,6 +1245,9 @@ const resources = { title: 'No medications scheduled for today', subtitle: 'No medication plans added yet. Let\'s add some.', }, + stack: { + completed: 'Completed ({{count}})', + }, dateFormats: { today: 'Today, {{date}}', other: '{{date}}', @@ -1607,6 +1631,20 @@ const resources = { }, }, }, + tabBarConfig: { + title: 'Tab Bar Settings', + subtitle: 'Customize your bottom navigation', + description: 'Use toggle to show or hide tabs', + resetButton: 'Reset to Default', + cannotDisable: 'This tab cannot be disabled', + resetConfirm: { + title: 'Reset to default?', + message: 'This will reset all tab bar settings and visibility', + cancel: 'Cancel', + confirm: 'Reset', + }, + resetSuccess: 'Settings reset to default', + }, }, }, }; diff --git a/ios/OutLive/Images.xcassets/AppIcon.appiconset/logo.png b/ios/OutLive/Images.xcassets/AppIcon.appiconset/logo.png index 03b906b..cf3a6a8 100644 Binary files a/ios/OutLive/Images.xcassets/AppIcon.appiconset/logo.png and b/ios/OutLive/Images.xcassets/AppIcon.appiconset/logo.png differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/Contents.json index 4e12055..f702e96 100644 --- a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/Contents.json +++ b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "logo.png", + "filename" : "onBoarding.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "logo 1.png", + "filename" : "onBoarding 1.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "logo 2.png", + "filename" : "onBoarding 2.png", "idiom" : "universal", "scale" : "3x" } diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 1.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 1.png deleted file mode 100644 index 03b906b..0000000 Binary files a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 1.png and /dev/null differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 2.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 2.png deleted file mode 100644 index 03b906b..0000000 Binary files a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo 2.png and /dev/null differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo.png deleted file mode 100644 index 03b906b..0000000 Binary files a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/logo.png and /dev/null differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 1.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 1.png new file mode 100644 index 0000000..0882900 Binary files /dev/null and b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 1.png differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 2.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 2.png new file mode 100644 index 0000000..0882900 Binary files /dev/null and b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding 2.png differ diff --git a/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding.png b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding.png new file mode 100644 index 0000000..0882900 Binary files /dev/null and b/ios/OutLive/Images.xcassets/SplashScreenLogo.imageset/onBoarding.png differ diff --git a/ios/OutLive/SplashScreen.storyboard b/ios/OutLive/SplashScreen.storyboard index 5afb2d2..654bf67 100644 --- a/ios/OutLive/SplashScreen.storyboard +++ b/ios/OutLive/SplashScreen.storyboard @@ -1,15 +1,15 @@ - + - + - + @@ -17,16 +17,16 @@ - - + + - - - - + + + + @@ -35,12 +35,9 @@ - - - - + - + - \ No newline at end of file + diff --git a/store/index.ts b/store/index.ts index fa1fc56..b952150 100644 --- a/store/index.ts +++ b/store/index.ts @@ -20,6 +20,7 @@ import membershipReducer from './membershipSlice'; import moodReducer from './moodSlice'; import nutritionReducer from './nutritionSlice'; import scheduleExerciseReducer from './scheduleExerciseSlice'; +import tabBarConfigReducer from './tabBarConfigSlice'; import trainingPlanReducer from './trainingPlanSlice'; import userReducer from './userSlice'; import waterReducer from './waterSlice'; @@ -113,6 +114,7 @@ export const store = configureStore({ fasting: fastingReducer, medications: medicationsReducer, badges: badgesReducer, + tabBarConfig: tabBarConfigReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware), diff --git a/store/tabBarConfigSlice.ts b/store/tabBarConfigSlice.ts new file mode 100644 index 0000000..4a3f166 --- /dev/null +++ b/store/tabBarConfigSlice.ts @@ -0,0 +1,203 @@ +import AsyncStorage from '@/utils/kvStore'; +import { logger } from '@/utils/logger'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './index'; + +// Tab 配置接口 +export interface TabConfig { + id: string; // tab 标识符 + icon: string; // SF Symbol 图标名 + titleKey: string; // i18n 翻译 key + enabled: boolean; // 是否启用 + canBeDisabled: boolean; // 是否可以被禁用 + order: number; // 显示顺序 +} + +// State 接口 +interface TabBarConfigState { + configs: TabConfig[]; + isInitialized: boolean; +} + +// 默认配置 +export const DEFAULT_TAB_CONFIGS: TabConfig[] = [ + { + id: 'statistics', + icon: 'chart.pie.fill', + titleKey: 'statistics.tabs.health', + enabled: true, + canBeDisabled: false, + order: 1, + }, + { + id: 'medications', + icon: 'pills.fill', + titleKey: 'statistics.tabs.medications', + enabled: true, + canBeDisabled: false, + order: 2, + }, + { + id: 'fasting', + icon: 'timer', + titleKey: 'statistics.tabs.fasting', + enabled: true, + canBeDisabled: true, // 只有断食可以被关闭 + order: 3, + }, + { + id: 'challenges', + icon: 'trophy.fill', + titleKey: 'statistics.tabs.challenges', + enabled: true, + canBeDisabled: false, + order: 4, + }, + { + id: 'personal', + icon: 'person.fill', + titleKey: 'statistics.tabs.personal', + enabled: true, + canBeDisabled: false, + order: 5, + }, +]; + +// AsyncStorage key +const STORAGE_KEY = 'tab_bar_config'; + +// 初始状态 +const initialState: TabBarConfigState = { + configs: DEFAULT_TAB_CONFIGS, + isInitialized: false, +}; + +const tabBarConfigSlice = createSlice({ + name: 'tabBarConfig', + initialState, + reducers: { + // 设置配置(用于从 AsyncStorage 恢复) + setConfigs: (state, action: PayloadAction) => { + state.configs = action.payload; + state.isInitialized = true; + }, + + // 切换 tab 启用状态 + toggleTabEnabled: (state, action: PayloadAction) => { + const tabId = action.payload; + const config = state.configs.find(c => c.id === tabId); + + if (config && config.canBeDisabled) { + config.enabled = !config.enabled; + // 自动持久化到 AsyncStorage + saveConfigsToStorage(state.configs); + } + }, + + // 更新 tab 顺序(拖拽后) + reorderTabs: (state, action: PayloadAction) => { + // 更新顺序,同时保持其他属性不变 + const newConfigs = action.payload.map((config, index) => ({ + ...config, + order: index + 1, + })); + state.configs = newConfigs; + // 自动持久化到 AsyncStorage + saveConfigsToStorage(newConfigs); + }, + + // 恢复默认配置 + resetToDefault: (state) => { + state.configs = DEFAULT_TAB_CONFIGS; + // 持久化到 AsyncStorage + saveConfigsToStorage(DEFAULT_TAB_CONFIGS); + }, + + // 标记已初始化 + markInitialized: (state) => { + state.isInitialized = true; + }, + }, +}); + +// 持久化配置到 AsyncStorage +const saveConfigsToStorage = async (configs: TabConfig[]) => { + try { + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(configs)); + logger.info('底部栏配置已保存'); + } catch (error) { + logger.error('保存底部栏配置失败:', error); + } +}; + +// 从 AsyncStorage 加载配置 +export const loadTabBarConfigs = () => async (dispatch: any) => { + try { + const stored = await AsyncStorage.getItem(STORAGE_KEY); + + if (stored) { + const configs = JSON.parse(stored) as TabConfig[]; + + // 验证配置有效性 + if (Array.isArray(configs) && configs.length > 0) { + // 合并默认配置,确保新增的 tab 也能显示 + const mergedConfigs = mergeWithDefaults(configs); + dispatch(setConfigs(mergedConfigs)); + logger.info('底部栏配置已加载'); + return; + } + } + + // 如果没有存储或无效,使用默认配置 + dispatch(setConfigs(DEFAULT_TAB_CONFIGS)); + dispatch(markInitialized()); + } catch (error) { + logger.error('加载底部栏配置失败:', error); + // 出错时使用默认配置 + dispatch(setConfigs(DEFAULT_TAB_CONFIGS)); + dispatch(markInitialized()); + } +}; + +// 合并存储的配置和默认配置 +const mergeWithDefaults = (storedConfigs: TabConfig[]): TabConfig[] => { + const merged = [...storedConfigs]; + + // 检查是否有新增的默认 tab + DEFAULT_TAB_CONFIGS.forEach(defaultConfig => { + const exists = merged.find(c => c.id === defaultConfig.id); + if (!exists) { + // 新增的 tab,添加到末尾 + merged.push({ + ...defaultConfig, + order: merged.length + 1, + }); + } + }); + + // 按 order 排序 + return merged.sort((a, b) => a.order - b.order); +}; + +// Actions +export const { + setConfigs, + toggleTabEnabled, + reorderTabs, + resetToDefault, + markInitialized, +} = tabBarConfigSlice.actions; + +// Selectors +export const selectTabBarConfigs = (state: RootState) => state.tabBarConfig.configs; +export const selectEnabledTabs = (state: RootState) => + state.tabBarConfig.configs + .filter(config => config.enabled) + .sort((a, b) => a.order - b.order); +export const selectIsInitialized = (state: RootState) => state.tabBarConfig.isInitialized; + +// 按 id 获取配置 +export const selectTabConfigById = (tabId: string) => (state: RootState) => + state.tabBarConfig.configs.find(config => config.id === tabId); + +export default tabBarConfigSlice.reducer; \ No newline at end of file