import { ArticleCard } from '@/components/ArticleCard'; import { PlanCard } from '@/components/PlanCard'; import { SearchBox } from '@/components/SearchBox'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { listRecommendedArticles } from '@/services/articles'; import { fetchRecommendations, RecommendationType } from '@/services/recommendations'; // Removed WorkoutCard import since we no longer use the horizontal carousel import { useAuthGuard } from '@/hooks/useAuthGuard'; import { getChineseGreeting } from '@/utils/date'; import dayjs from 'dayjs'; import { useRouter } from 'expo-router'; import React from 'react'; import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; // 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片 export default function HomeScreen() { const router = useRouter(); const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const insets = useSafeAreaInsets(); const { width: windowWidth, height: windowHeight } = useWindowDimensions(); // Draggable coach badge state const pan = React.useRef(new Animated.ValueXY()).current; const [coachSize, setCoachSize] = React.useState({ width: 0, height: 0 }); const hasInitPos = React.useRef(false); const startRef = React.useRef({ x: 0, y: 0 }); const dragState = React.useRef({ moved: false }); const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); const panResponder = React.useMemo(() => PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: (_evt, gesture) => Math.abs(gesture.dx) + Math.abs(gesture.dy) > 2, onPanResponderGrant: () => { dragState.current.moved = false; // @ts-ignore access current value const currentX = (pan.x as any)._value ?? 0; // @ts-ignore access current value const currentY = (pan.y as any)._value ?? 0; startRef.current = { x: currentX, y: currentY }; }, onPanResponderMove: (_evt, gesture) => { if (!dragState.current.moved && (Math.abs(gesture.dx) + Math.abs(gesture.dy) > 4)) { dragState.current.moved = true; } const nextX = startRef.current.x + gesture.dx; const nextY = startRef.current.y + gesture.dy; pan.setValue({ x: nextX, y: nextY }); }, onPanResponderRelease: (_evt, gesture) => { const minX = 8; const minY = insets.top + 2; const maxX = Math.max(minX, windowWidth - coachSize.width - 8); const maxY = Math.max(minY, windowHeight - coachSize.height - (insets.bottom + 8)); const rawX = startRef.current.x + gesture.dx; const rawY = startRef.current.y + gesture.dy; const clampedX = clamp(rawX, minX, maxX); const clampedY = clamp(rawY, minY, maxY); // Snap horizontally to nearest side (left/right only) const distLeft = Math.abs(clampedX - minX); const distRight = Math.abs(maxX - clampedX); const snapX = distLeft <= distRight ? minX : maxX; Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, bounciness: 6 }).start(() => { if (!dragState.current.moved) { // Treat as tap // @ts-ignore - expo-router string ok router.push('/ai-coach-chat?name=Iris' as any); } }); }, }), [coachSize.height, coachSize.width, insets.bottom, insets.top, pan, windowHeight, windowWidth, router]); // 推荐项类型(本地 UI 使用) type RecommendItem = | { type: 'plan'; key: string; image: string; title: string; subtitle: string; level?: '初学者' | '中级' | '高级'; progress: number; onPress?: () => void; } | { type: 'article'; key: string; id: string; title: string; coverImage: string; publishedAt: string; readCount: number; }; // 打底数据(接口不可用时) const getFallbackItems = React.useCallback((): RecommendItem[] => { return [ { type: 'plan', key: 'assess', image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg', title: '体态评估', subtitle: '评估你的体态,制定训练计划', level: '初学者', progress: 0, onPress: () => router.push('/ai-posture-assessment'), }, ...listRecommendedArticles().map((a) => ({ type: 'article' as const, key: `article-${a.id}`, id: a.id, title: a.title, coverImage: a.coverImage, publishedAt: a.publishedAt, readCount: a.readCount, })), ]; }, [router]); const [items, setItems] = React.useState(() => getFallbackItems()); // 拉取推荐接口(已登录时) React.useEffect(() => { let canceled = false; async function load() { if (!isLoggedIn) { console.log('fetchRecommendations not logged in'); setItems(getFallbackItems()); return; } try { const cards = await fetchRecommendations(); if (canceled) return; const mapped: RecommendItem[] = []; for (const c of cards || []) { if (c.type === RecommendationType.Article) { const publishedAt = (c.extra && (c.extra.publishedDate || c.extra.published_at)) || new Date().toISOString(); const readCount = (c.extra && (c.extra.readCount ?? c.extra.read_count)) || 0; mapped.push({ type: 'article', key: c.id, id: c.articleId || c.id, title: c.title || '', coverImage: c.coverUrl, publishedAt, readCount, }); } else if (c.type === RecommendationType.Checkin) { mapped.push({ type: 'plan', key: c.id || 'checkin', image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg', title: c.title || '今日打卡', subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持', progress: 0, onPress: () => pushIfAuthedElseLogin('/checkin?date=' + dayjs().format('YYYY-MM-DD')), }); } } // 若接口返回空,也回退到打底 setItems(mapped.length > 0 ? mapped : getFallbackItems()); } catch (e) { console.error('fetchRecommendations error', e); setItems(getFallbackItems()); } } load(); return () => { canceled = true; }; }, [isLoggedIn, pushIfAuthedElseLogin, getFallbackItems]); return ( {/* Floating Coach Badge */} { const { width, height } = e.nativeEvent.layout; if (width !== coachSize.width || height !== coachSize.height) { setCoachSize({ width, height }); } if (!hasInitPos.current && width > 0 && windowWidth > 0) { const initX = windowWidth - width - 14; const initY = insets.top + 2; // 默认更靠上,避免遮挡搜索框 pan.setValue({ x: initX, y: initY }); hasInitPos.current = true; } }} style={[ styles.coachBadge, { transform: [{ translateX: pan.x }, { translateY: pan.y }], backgroundColor: colorTokens.heroSurfaceTint, borderColor: 'rgba(187,242,70,0.35)', shadowColor: '#000', shadowOpacity: 0.08, shadowRadius: 10, shadowOffset: { width: 0, height: 4 }, elevation: 3, position: 'absolute', left: 0, top: 0, }, ]} > Iris 在线 {/* Header Section */} {getChineseGreeting()} 新学员,欢迎你 {/* Search Box */} {/* Hot Features Section */} 热点功能 router.push('/ai-posture-assessment')} > AI体态评估 3分钟获取体态报告 router.push('/ai-coach-chat?name=Sarah' as any)} > 在线教练 认证教练 · 1对1即时解答 pushIfAuthedElseLogin('/checkin?date=' + dayjs().format('YYYY-MM-DD'))} > 每日打卡 自选动作 · 记录完成 pushIfAuthedElseLogin('/training-plan')} > 训练计划制定 按周安排 · 个性化目标 {/* Today Plan Section */} 为你推荐 {items.map((item) => { if (item.type === 'article') { return ( ); } const card = ( ); return item.onPress ? ( {card} ) : ( {card} ); })} {/* Add some spacing at the bottom */} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: '#F7F8FA', }, container: { flex: 1, backgroundColor: '#F7F8FA', }, header: { paddingHorizontal: 24, paddingTop: 16, paddingBottom: 8, }, coachOverlayWrap: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 10, }, greeting: { fontSize: 16, color: '#8A8A8E', fontWeight: '400', marginBottom: 6, }, userName: { fontSize: 30, fontWeight: 'bold', color: '#1A1A1A', lineHeight: 36, }, coachBadge: { flexDirection: 'row', alignItems: 'center', // RN 不完全支持 gap,这里用 margin 实现 paddingHorizontal: 10, paddingVertical: 8, borderRadius: 20, borderWidth: 1, backgroundColor: '#FFFFFF00', }, coachAvatar: { width: 26, height: 26, borderRadius: 13, }, coachMeta: { marginLeft: 8, }, coachName: { fontSize: 13, fontWeight: '700', color: '#192126', }, coachStatusRow: { flexDirection: 'row', alignItems: 'center', marginTop: 2, }, statusDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#22C55E', marginRight: 4, }, coachStatusText: { fontSize: 11, color: '#6B7280', }, sectionContainer: { marginTop: 24, }, sectionTitle: { fontSize: 24, fontWeight: 'bold', color: '#1A1A1A', paddingHorizontal: 24, marginBottom: 18, }, featureGrid: { paddingHorizontal: 24, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap', }, featureCard: { width: '48%', borderRadius: 12, padding: 12, backgroundColor: '#FFFFFF', marginBottom: 12, // 轻量阴影,减少臃肿感 shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 2, }, featureCardPrimary: { backgroundColor: '#EEF2FF', // 柔和的靛蓝背景 }, featureCardSecondary: { backgroundColor: '#F0FDFA', // 柔和的青绿背景 }, featureCardTertiary: { backgroundColor: '#FFF7ED', // 柔和的橙色背景 }, featureCardQuaternary: { backgroundColor: '#F5F3FF', // 柔和的紫色背景 }, featureIcon: { fontSize: 28, marginBottom: 8, }, featureTitle: { fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 4, }, featureSubtitle: { fontSize: 11, color: '#6B7280', lineHeight: 15, }, planList: { paddingHorizontal: 24, }, // 移除旧的滑动样式 bottomSpacing: { height: 120, }, });