diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f57c273 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `app/` holds Expo Router screens; tab flows live in `app/(tabs)/`, while modal or detail pages sit alongside feature folders. +- Shared UI and domain logic belong in `components/`, `services/`, and `utils/`; Redux state is organized per feature under `store/`. +- Native iOS code (HealthKit bridge, widgets, quick actions) resides in `ios/`; design and process docs are tracked in `docs/`. +- Assets, fonts, and icons live in `assets/`; keep new media optimized and referenced via `@/assets` aliases. + +## Build, Test, and Development Commands +- `npm run ios` / `npm run ios-device` – builds and runs the prebuilt iOS app in Simulator or on a connected device. +- `npm run reset-project` – clears caches and regenerates native artifacts; use after dependency or native module changes. + +## Coding Style & Naming Conventions +- TypeScript with React hooks is standard; use functional components and keep state in Redux slices if shared. +- Follow ESLint (`eslint-config-expo`) and default Prettier formatting (2 spaces, trailing commas, single quotes). +- Name components in `PascalCase`, hooks/utilities in `camelCase`, and screen files with kebab-case (e.g., `ai-posture-assessment.tsx`). +- Co-locate feature assets, styles, and tests to simplify maintenance. + +## Testing Guidelines +- Automated tests are minimal; add Jest + React Native Testing Library specs under `__tests__/` or alongside modules when adding complex logic. +- For health and native bridges, include reproduction steps and Simulator logs in PR descriptions. +- Always run linting and verify critical flows on an iOS simulator (HealthKit requires a real device for full validation). + +## Commit & Pull Request Guidelines +- Prefer Conventional Commit prefixes (`feat`, `fix`, `chore`, etc.) with optional scope: `feat(water): 支持自定义提醒`. Keep summaries under 80 characters. +- Group related changes; avoid bundling unrelated features and formatting in one commit. +- PRs should describe the problem, solution, test evidence (commands run, screenshots, or screen recordings), and note any iOS-specific setup. +- Link to Linear/Jira issues where relevant and request review from feature owners or the iOS platform team. + +## iOS Integration Notes +- HealthKit, widgets, and quick actions depend on native modules: update `ios/` and re-run `npm run ios` after modifying Swift or entitlement files. +- Keep App Group IDs, bundle identifiers, and signing assets consistent with `app.json` and `ios/digitalpilates.xcodeproj`; coordinate credential changes with release engineering. diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 31732b3..f16c262 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -21,8 +21,8 @@ type TabConfig = { const TAB_CONFIGS: Record = { statistics: { icon: 'chart.pie.fill', title: '健康' }, - // explore: { icon: 'magnifyingglass.circle.fill', title: '发现' }, goals: { icon: 'flag.fill', title: '习惯' }, + challenges: { icon: 'trophy.fill', title: '挑战' }, personal: { icon: 'person.fill', title: '个人' }, }; @@ -35,9 +35,10 @@ export default function TabLayout() { // Helper function to determine if a tab is selected const isTabSelected = (routeName: string): boolean => { const routeMap: Record = { - explore: ROUTES.TAB_EXPLORE, - goals: ROUTES.TAB_GOALS, statistics: ROUTES.TAB_STATISTICS, + goals: ROUTES.TAB_GOALS, + challenges: ROUTES.TAB_CHALLENGES, + personal: ROUTES.TAB_PERSONAL, }; return routeMap[routeName] === pathname || pathname.includes(routeName); @@ -69,11 +70,11 @@ export default function TabLayout() { alignItems: 'center', justifyContent: 'center', flexDirection: 'row', - marginHorizontal: 6, + marginHorizontal: 2, marginVertical: 10, borderRadius: 25, backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent', - paddingHorizontal: isSelected ? 16 : 10, + paddingHorizontal: isSelected ? 8 : 4, paddingVertical: 8, }} > @@ -91,7 +92,7 @@ export default function TabLayout() { fontWeight: '600', marginLeft: 6, }} - numberOfLines={0 as any} + numberOfLines={1} > {tabConfig.title} @@ -148,12 +149,12 @@ export default function TabLayout() { shadowOpacity: glassEffectAvailable ? 0.1 : 0.2, shadowRadius: 10, elevation: 5, - paddingHorizontal: 10, + paddingHorizontal: 6, paddingTop: 0, paddingBottom: 0, - marginHorizontal: 20, - left: 20, - right: 20, + marginHorizontal: 16, + left: 16, + right: 16, alignSelf: 'center', borderWidth: glassEffectAvailable ? 1 : 0, borderColor: glassEffectAvailable ? (theme === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)') : 'transparent', @@ -177,7 +178,11 @@ export default function TabLayout() { - + + + + + @@ -193,9 +198,8 @@ export default function TabLayout() { > - - + ); diff --git a/app/(tabs)/challenges.tsx b/app/(tabs)/challenges.tsx new file mode 100644 index 0000000..1e9b095 --- /dev/null +++ b/app/(tabs)/challenges.tsx @@ -0,0 +1,297 @@ +import { IconSymbol } from '@/components/ui/IconSymbol'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useRouter } from 'expo-router'; +import React from 'react'; +import { Image, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +export const CHALLENGES = [ + { + id: 'joyful-dog-run', + title: '遛狗跑步,欢乐一路', + dateRange: '9月01日 - 9月30日', + participantsLabel: '6,364 跑者', + image: 'https://images.unsplash.com/photo-1525253086316-d0c936c814f8?auto=format&fit=crop&w=1200&q=80', + avatars: [ + 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1544723795-3fbce826f51f?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1502823403499-6ccfcf4fb453?auto=format&fit=crop&w=200&q=80', + ], + }, + { + id: 'penguin-swim', + title: '企鹅宝宝的游泳预备班', + dateRange: '9月01日 - 9月30日', + participantsLabel: '3,334 游泳者', + image: 'https://images.unsplash.com/photo-1531297484001-80022131f5a1?auto=format&fit=crop&w=1200&q=80', + avatars: [ + 'https://images.unsplash.com/photo-1525134479668-1bee5c7c6845?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1530268729831-4b0b9e170218?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1463453091185-61582044d556?auto=format&fit=crop&w=200&q=80', + ], + }, + { + id: 'hydration-hippo', + title: '学河马饮,做补水人', + dateRange: '9月01日 - 9月30日', + participantsLabel: '9,009 饮水者', + image: 'https://images.unsplash.com/photo-1481931098730-318b6f776db0?auto=format&fit=crop&w=1200&q=80', + avatars: [ + 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1544723660-4bfa6584218e?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1544723795-3fbfb7c6a9f1?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1544723795-432537f48b2b?auto=format&fit=crop&w=200&q=80', + ], + }, + { + id: 'autumn-cycling', + title: '炎夏渐散,踏板骑秋', + dateRange: '9月01日 - 9月30日', + participantsLabel: '4,617 骑行者', + image: 'https://images.unsplash.com/photo-1509395176047-4a66953fd231?auto=format&fit=crop&w=1200&q=80', + avatars: [ + 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80', + ], + }, + { + id: 'falcon-core', + title: '燃卡加练甄秋腰', + dateRange: '9月01日 - 9月30日', + participantsLabel: '11,995 健身爱好者', + image: 'https://images.unsplash.com/photo-1494871262121-6adf66e90adf?auto=format&fit=crop&w=1200&q=80', + avatars: [ + 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1502685104226-ee32379fefbe?auto=format&fit=crop&w=200&q=80', + 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?auto=format&fit=crop&w=200&q=80', + ], + }, +] as const; + +export type Challenge = (typeof CHALLENGES)[number]; + +const AVATAR_SIZE = 36; +const CARD_IMAGE_WIDTH = 132; +const CARD_IMAGE_HEIGHT = 96; + +export default function ChallengesScreen() { + const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const colorTokens = Colors[theme]; + const router = useRouter(); + + const gradientColors = + theme === 'dark' + ? ['#1f2230', '#10131e'] + : [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd]; + + return ( + + + + + + + + 挑战 + 参与精选活动,保持每日动力 + + + + + + + + + + {CHALLENGES.map((challenge) => ( + + router.push({ pathname: '/challenges/[id]', params: { id: challenge.id } }) + } + /> + ))} + + + + + ); +} + +type ChallengeCardProps = { + challenge: Challenge; + surfaceColor: string; + textColor: string; + mutedColor: string; + onPress: () => void; +}; + +function ChallengeCard({ challenge, surfaceColor, textColor, mutedColor, onPress }: ChallengeCardProps) { + return ( + + + + + + {challenge.title} + + {challenge.dateRange} + {challenge.participantsLabel} + + + + ); +} + +type AvatarStackProps = { + avatars: string[]; + borderColor: string; +}; + +function AvatarStack({ avatars, borderColor }: AvatarStackProps) { + return ( + + {avatars.map((avatar, index) => ( + + ))} + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + safeArea: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: 20, + paddingBottom: 32, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 8, + marginBottom: 26, + }, + title: { + fontSize: 32, + fontWeight: '700', + letterSpacing: 1, + }, + subtitle: { + marginTop: 6, + fontSize: 14, + fontWeight: '500', + opacity: 0.8, + }, + giftShadow: { + shadowColor: 'rgba(94, 62, 199, 0.45)', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.35, + shadowRadius: 12, + elevation: 8, + borderRadius: 26, + }, + giftButton: { + width: 52, + height: 52, + borderRadius: 26, + alignItems: 'center', + justifyContent: 'center', + }, + cardsContainer: { + gap: 18, + }, + card: { + flexDirection: 'row', + borderRadius: 28, + padding: 18, + alignItems: 'center', + shadowOffset: { width: 0, height: 16 }, + shadowOpacity: 0.18, + shadowRadius: 24, + elevation: 6, + }, + cardImage: { + width: CARD_IMAGE_WIDTH, + height: CARD_IMAGE_HEIGHT, + borderRadius: 22, + }, + cardContent: { + flex: 1, + marginLeft: 16, + }, + cardTitle: { + fontSize: 18, + fontWeight: '700', + marginBottom: 4, + }, + cardDate: { + fontSize: 13, + fontWeight: '500', + marginBottom: 4, + }, + cardParticipants: { + fontSize: 13, + fontWeight: '500', + }, + avatarRow: { + flexDirection: 'row', + marginTop: 16, + alignItems: 'center', + }, + avatar: { + width: AVATAR_SIZE, + height: AVATAR_SIZE, + borderRadius: AVATAR_SIZE / 2, + borderWidth: 2, + }, + avatarOffset: { + marginLeft: -12, + }, +}); diff --git a/app/challenges/[id].tsx b/app/challenges/[id].tsx new file mode 100644 index 0000000..b7a8e70 --- /dev/null +++ b/app/challenges/[id].tsx @@ -0,0 +1,683 @@ +import { CHALLENGES, type Challenge } from '@/app/(tabs)/challenges'; +import { HeaderBar } from '@/components/ui/HeaderBar'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import React, { useMemo, useState } from 'react'; +import { + Dimensions, + Image, + Platform, + ScrollView, + Share, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; + +const { width } = Dimensions.get('window'); +const HERO_HEIGHT = width * 0.86; +const BADGE_SIZE = 120; + +type ChallengeDetail = { + badgeImage: string; + periodLabel: string; + durationLabel: string; + requirementLabel: string; + summary?: string; + participantsCount: number; + rankingDescription?: string; + rankings: Record; + highlightTitle: string; + highlightSubtitle: string; + ctaLabel: string; +}; + +type RankingItem = { + id: string; + name: string; + avatar: string; + metric: string; + badge?: string; +}; + +const DETAIL_PRESETS: Record = { + 'hydration-hippo': { + badgeImage: + 'https://images.unsplash.com/photo-1616628182503-5ef2941510da?auto=format&fit=crop&w=240&q=80', + periodLabel: '9月01日 - 9月30日 · 剩余 4 天', + durationLabel: '30 天', + requirementLabel: '喝水 1500ml 15 天以上', + summary: '与河马一起练就最佳补水习惯,让身体如湖水般澄澈充盈。', + participantsCount: 9009, + rankingDescription: '榜单实时更新,记录每位补水达人每日平均饮水量。', + rankings: { + all: [ + { + id: 'all-1', + name: '湖光暮色', + avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=140&q=80', + metric: '平均 3,200 ml', + badge: '金冠冠军', + }, + { + id: 'all-2', + name: '温柔潮汐', + avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=140&q=80', + metric: '平均 2,980 ml', + }, + { + id: 'all-3', + name: '晨雾河岸', + avatar: 'https://images.unsplash.com/photo-1544723795-432537f48b2b?auto=format&fit=crop&w=140&q=80', + metric: '平均 2,860 ml', + }, + ], + male: [ + { + id: 'male-1', + name: '北岸微风', + avatar: 'https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?auto=format&fit=crop&w=140&q=80', + metric: '平均 3,120 ml', + }, + { + id: 'male-2', + name: '静水晚霞', + avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=140&q=80', + metric: '平均 2,940 ml', + }, + ], + female: [ + { + id: 'female-1', + name: '露珠初晓', + avatar: 'https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=140&q=80', + metric: '平均 3,060 ml', + }, + { + id: 'female-2', + name: '桔梗水语', + avatar: 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?auto=format&fit=crop&w=140&q=80', + metric: '平均 2,880 ml', + }, + ], + }, + highlightTitle: '分享一次,免费参与', + highlightSubtitle: '解锁高级会员,无限加入挑战', + ctaLabel: '马上分享激励好友', + }, +}; + +const DEFAULT_DETAIL: ChallengeDetail = { + badgeImage: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=240&q=80', + periodLabel: '本周进行中', + durationLabel: '30 天', + requirementLabel: '保持专注完成每日任务', + participantsCount: 3200, + highlightTitle: '立即参加,点燃动力', + highlightSubtitle: '邀请好友一起坚持,更容易收获成果', + ctaLabel: '立即加入挑战', + rankings: { + all: [], + }, +}; + +const SEGMENTS = [ + { key: 'all', label: '全部' }, + { key: 'male', label: '男生' }, + { key: 'female', label: '女生' }, +] as const; + +type SegmentKey = (typeof SEGMENTS)[number]['key']; + +export default function ChallengeDetailScreen() { + const { id } = useLocalSearchParams<{ id?: string }>(); + const router = useRouter(); + const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const colorTokens = Colors[theme]; + const insets = useSafeAreaInsets(); + + const challenge = useMemo(() => { + if (!id) return undefined; + return CHALLENGES.find((item) => item.id === id); + }, [id]); + + const detail = useMemo(() => { + if (!id) return DEFAULT_DETAIL; + return DETAIL_PRESETS[id] ?? { + ...DEFAULT_DETAIL, + periodLabel: challenge?.dateRange ?? DEFAULT_DETAIL.periodLabel, + highlightTitle: `加入 ${challenge?.title ?? '挑战'}`, + }; + }, [challenge?.dateRange, challenge?.title, id]); + + const [segment, setSegment] = useState('all'); + + const rankingData = detail.rankings[segment] ?? detail.rankings.all ?? []; + + const handleShare = async () => { + if (!challenge) { + return; + } + + try { + await Share.share({ + title: challenge.title, + message: `我正在参与「${challenge.title}」,一起坚持吧!`, + url: challenge.image, + }); + } catch (error) { + console.warn('分享失败', error); + } + }; + + const handleJoin = () => { + // 当前没有具体业务流程,先回退到挑战列表 + router.back(); + }; + + if (!challenge) { + return ( + + router.back()} withSafeTop transparent={false} /> + + 未找到该挑战,稍后再试试吧。 + + + ); + } + + return ( + + + + + + + } + /> + + + + + + + + + + + + + + + + {detail.periodLabel} + {challenge.title} + {detail.summary ? {detail.summary} : null} + + + + + + + + + {challenge.dateRange} + {detail.durationLabel} + + + + + + + + + {detail.requirementLabel} + 按日打卡自动累计 + + + + + + + + + {detail.participantsCount.toLocaleString('zh-CN')} 人正在参与 + + {challenge.avatars.slice(0, 6).map((avatar, index) => ( + 0 && styles.avatarOffset]} + /> + ))} + + 更多 + + + + + + + + 排行榜 + + 查看全部 + + + + {detail.rankingDescription ? ( + {detail.rankingDescription} + ) : null} + + + {SEGMENTS.map(({ key, label }) => { + const isActive = segment === key; + const disabled = !(detail.rankings[key] && detail.rankings[key].length); + return ( + { + if (disabled) return; + setSegment(key); + }} + > + + {label} + + + ); + })} + + + + {rankingData.length ? ( + rankingData.map((item, index) => ( + 0 && styles.rankingRowDivider]}> + + {index + 1} + + + + {item.name} + {item.metric} + + {item.badge ? {item.badge} : null} + + )) + ) : ( + + 榜单即将开启,快来抢占席位。 + + )} + + + + + {detail.highlightTitle} + {detail.highlightSubtitle} + + {detail.ctaLabel} + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: '#f3f4fb', + }, + headerOverlay: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + zIndex: 20, + }, + heroContainer: { + height: HERO_HEIGHT, + width: '100%', + overflow: 'hidden', + borderBottomLeftRadius: 36, + borderBottomRightRadius: 36, + }, + heroImage: { + width: '100%', + height: '100%', + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingBottom: Platform.select({ ios: 40, default: 28 }), + }, + badgeWrapper: { + alignItems: 'center', + marginTop: -BADGE_SIZE / 2, + }, + badgeShadow: { + width: BADGE_SIZE, + height: BADGE_SIZE, + borderRadius: BADGE_SIZE / 2, + backgroundColor: '#fff', + padding: 12, + shadowColor: 'rgba(17, 24, 39, 0.2)', + shadowOpacity: 0.25, + shadowRadius: 18, + shadowOffset: { width: 0, height: 10 }, + elevation: 12, + }, + badgeImage: { + flex: 1, + borderRadius: BADGE_SIZE / 2, + }, + headerTextBlock: { + paddingHorizontal: 24, + marginTop: 24, + alignItems: 'center', + }, + periodLabel: { + fontSize: 14, + color: '#596095', + letterSpacing: 0.2, + }, + title: { + marginTop: 10, + fontSize: 24, + fontWeight: '800', + color: '#1c1f3a', + textAlign: 'center', + }, + summary: { + marginTop: 12, + fontSize: 14, + lineHeight: 20, + color: '#7080b4', + textAlign: 'center', + }, + detailCard: { + marginTop: 28, + marginHorizontal: 20, + padding: 20, + borderRadius: 28, + backgroundColor: '#ffffff', + shadowColor: 'rgba(30, 41, 59, 0.18)', + shadowOpacity: 0.2, + shadowRadius: 20, + shadowOffset: { width: 0, height: 12 }, + elevation: 8, + gap: 20, + }, + detailRow: { + flexDirection: 'row', + alignItems: 'center', + }, + detailIconWrapper: { + width: 42, + height: 42, + borderRadius: 21, + backgroundColor: '#EFF1FF', + alignItems: 'center', + justifyContent: 'center', + }, + detailTextWrapper: { + marginLeft: 14, + }, + detailLabel: { + fontSize: 15, + fontWeight: '600', + color: '#1c1f3a', + }, + detailMeta: { + marginTop: 4, + fontSize: 12, + color: '#6f7ba7', + }, + avatarRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 12, + }, + avatar: { + width: 36, + height: 36, + borderRadius: 18, + borderWidth: 2, + borderColor: '#fff', + }, + avatarOffset: { + marginLeft: -12, + }, + moreAvatarButton: { + marginLeft: 12, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 14, + backgroundColor: '#EEF0FF', + }, + moreAvatarText: { + fontSize: 12, + color: '#4F5BD5', + fontWeight: '600', + }, + sectionHeader: { + marginTop: 36, + marginHorizontal: 24, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: '#1c1f3a', + }, + sectionAction: { + fontSize: 13, + fontWeight: '600', + color: '#5F6BF0', + }, + sectionSubtitle: { + marginTop: 8, + marginHorizontal: 24, + fontSize: 13, + color: '#6f7ba7', + lineHeight: 18, + }, + segmentedControl: { + marginTop: 20, + marginHorizontal: 24, + borderRadius: 20, + backgroundColor: '#EAECFB', + padding: 4, + flexDirection: 'row', + }, + segmentButton: { + flex: 1, + paddingVertical: 8, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + segmentButtonActive: { + backgroundColor: '#fff', + shadowColor: 'rgba(79, 91, 213, 0.25)', + shadowOpacity: 0.3, + shadowRadius: 10, + shadowOffset: { width: 0, height: 6 }, + elevation: 4, + }, + segmentDisabled: { + opacity: 0.5, + }, + segmentLabel: { + fontSize: 13, + fontWeight: '600', + color: '#6372C6', + }, + segmentLabelActive: { + color: '#4F5BD5', + }, + segmentLabelDisabled: { + color: '#9AA3CF', + }, + rankingCard: { + marginTop: 20, + marginHorizontal: 24, + borderRadius: 24, + backgroundColor: '#ffffff', + paddingVertical: 10, + shadowColor: 'rgba(30, 41, 59, 0.12)', + shadowOpacity: 0.16, + shadowRadius: 18, + shadowOffset: { width: 0, height: 10 }, + elevation: 6, + }, + rankingRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 18, + }, + rankingRowDivider: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#E5E7FF', + }, + rankingOrderCircle: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#EEF0FF', + marginRight: 12, + }, + rankingOrder: { + fontSize: 15, + fontWeight: '700', + color: '#4F5BD5', + }, + rankingAvatar: { + width: 44, + height: 44, + borderRadius: 22, + marginRight: 14, + }, + rankingInfo: { + flex: 1, + }, + rankingName: { + fontSize: 15, + fontWeight: '700', + color: '#1c1f3a', + }, + rankingMetric: { + marginTop: 4, + fontSize: 13, + color: '#6f7ba7', + }, + rankingBadge: { + fontSize: 12, + color: '#A67CFF', + fontWeight: '700', + }, + emptyRanking: { + paddingVertical: 40, + alignItems: 'center', + }, + emptyRankingText: { + fontSize: 14, + color: '#6f7ba7', + }, + highlightCard: { + marginTop: 32, + marginHorizontal: 24, + borderRadius: 28, + paddingVertical: 28, + paddingHorizontal: 24, + overflow: 'hidden', + }, + highlightTitle: { + fontSize: 18, + fontWeight: '800', + color: '#ffffff', + }, + highlightSubtitle: { + marginTop: 10, + fontSize: 14, + color: 'rgba(255,255,255,0.85)', + lineHeight: 20, + }, + highlightButton: { + marginTop: 22, + backgroundColor: 'rgba(255,255,255,0.18)', + paddingVertical: 12, + borderRadius: 22, + alignItems: 'center', + borderWidth: 1, + borderColor: 'rgba(247,248,255,0.5)', + }, + highlightButtonLabel: { + fontSize: 15, + fontWeight: '700', + color: '#ffffff', + }, + circularButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255,255,255,0.24)', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.45)', + }, + shareIcon: { + fontSize: 18, + color: '#ffffff', + fontWeight: '700', + }, + missingContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + }, + missingText: { + fontSize: 16, + textAlign: 'center', + }, +}); diff --git a/app/(tabs)/coach.tsx b/app/coach.tsx similarity index 99% rename from app/(tabs)/coach.tsx rename to app/coach.tsx index af36d90..b4dcedb 100644 --- a/app/(tabs)/coach.tsx +++ b/app/coach.tsx @@ -30,8 +30,8 @@ import { api, getAuthToken, postTextStream } from '@/services/api'; import { selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage'; import { Image } from 'expo-image'; -import { HistoryModal } from '../../components/model/HistoryModal'; -import { ActionSheet } from '../../components/ui/ActionSheet'; +import { HistoryModal } from '../components/model/HistoryModal'; +import { ActionSheet } from '../components/ui/ActionSheet'; // 导入新的 coach 组件 import { diff --git a/app/(tabs)/explore.tsx b/app/explore.tsx similarity index 100% rename from app/(tabs)/explore.tsx rename to app/explore.tsx diff --git a/constants/Routes.ts b/constants/Routes.ts index c4b608b..f6a12d1 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -5,6 +5,7 @@ export const ROUTES = { TAB_COACH: '/coach', TAB_GOALS: '/goals', TAB_STATISTICS: '/statistics', + TAB_CHALLENGES: '/challenges', TAB_PERSONAL: '/personal', // 训练相关路由 @@ -93,4 +94,4 @@ export const QUERY_PARAMS = { // 教练页面参数 COACH_NAME: 'name', -} as const; \ No newline at end of file +} as const;