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 { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { listRecommendedArticles } from '@/services/articles'; import { fetchRecommendations, RecommendationType } from '@/services/recommendations'; import { loadPlans } from '@/store/trainingPlanSlice'; // Removed WorkoutCard import since we no longer use the horizontal carousel import { useAuthGuard } from '@/hooks/useAuthGuard'; import { TrainingPlan } from '@/services/trainingPlanApi'; import { getChineseGreeting } from '@/utils/date'; 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 dispatch = useAppDispatch(); const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const insets = useSafeAreaInsets(); const { width: windowWidth, height: windowHeight } = useWindowDimensions(); // 训练计划状态 const { plans } = useAppSelector((s) => s.trainingPlan); const [activePlan, setActivePlan] = React.useState(null); // 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) { // 切换到教练 tab,并传递name参数 router.push('/coach?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?: '初学者' | '中级' | '高级'; 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: 'today-workout', image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg', title: '今日训练', subtitle: '完成一次普拉提训练,记录你的坚持', level: '初学者', onPress: () => pushIfAuthedElseLogin('/workout/today'), }, { type: 'plan', key: 'assess', image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg', title: '体态评估', subtitle: '评估你的体态,制定训练计划', level: '初学者', 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, pushIfAuthedElseLogin]); const [items, setItems] = React.useState(() => getFallbackItems()); // 加载训练计划数据 React.useEffect(() => { if (isLoggedIn) { dispatch(loadPlans()); } }, [isLoggedIn, dispatch]); // 获取激活的训练计划 React.useEffect(() => { if (isLoggedIn && plans.length > 0) { const currentPlan = plans.find(p => p.isActive); setActivePlan(currentPlan || null); } else { setActivePlan(null); } }, [isLoggedIn, plans]); // 拉取推荐接口(已登录时) React.useEffect(() => { let canceled = false; async function load() { 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 || '完成一次普拉提训练,记录你的坚持', onPress: () => pushIfAuthedElseLogin('/workout/today'), }); } } // 若接口返回空,也回退到打底 setItems(mapped.length > 0 ? mapped : getFallbackItems()); } catch (e) { console.error('fetchRecommendations error', e); setItems(getFallbackItems()); } } load(); return () => { canceled = true; }; }, [isLoggedIn, pushIfAuthedElseLogin, getFallbackItems]); // 处理点击训练计划卡片,跳转到锻炼tab const handlePlanCardPress = () => { if (activePlan) { // 跳转到训练计划页面的锻炼tab,并传递planId参数 router.push(`/training-plan?planId=${activePlan.id}&tab=schedule` as any); } }; 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 */} 热点功能 pushIfAuthedElseLogin('/workout/today')} > 训练 pushIfAuthedElseLogin('/ai-posture-assessment')} > 体态 pushIfAuthedElseLogin('/training-plan')} > 计划 {/* My Plan Section - 显示激活的训练计划 */} {/* {activePlan && ( )} */} {/* 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', gap: 12, }, featureCard: { flex: 1, flexDirection: 'row', alignItems: 'center', borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, backgroundColor: '#FFFFFF', // 精致的阴影效果 shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 3, // 渐变边框效果 borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.8)', // 添加微妙的内阴影效果 position: 'relative', minHeight: 48, }, featureCardPrimary: { // 由于RN不支持CSS渐变,使用渐变色背景 backgroundColor: '#667eea', }, featureCardSecondary: { backgroundColor: '#4facfe', }, featureCardTertiary: { backgroundColor: '#43e97b', }, featureCardQuaternary: { backgroundColor: '#fa709a', }, featureCardQuinary: { backgroundColor: '#f59e0b', }, featureIconWrapper: { width: 32, height: 32, borderRadius: 16, backgroundColor: 'rgba(255, 255, 255, 0.25)', alignItems: 'center', justifyContent: 'center', marginRight: 10, // 图标容器的阴影 shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 4, shadowOffset: { width: 0, height: 1 }, elevation: 2, }, featureIconImage: { width: 20, height: 20, borderRadius: 10, resizeMode: 'cover', }, featureIconPlaceholder: { width: 20, height: 20, borderRadius: 10, backgroundColor: 'rgba(255, 255, 255, 0.3)', alignItems: 'center', justifyContent: 'center', }, featureIconText: { fontSize: 12, }, featureTitle: { fontSize: 14, fontWeight: '700', color: '#FFFFFF', textAlign: 'left', letterSpacing: 0.2, flex: 1, }, featureSubtitle: { fontSize: 12, color: 'rgba(255, 255, 255, 0.85)', lineHeight: 16, textAlign: 'center', fontWeight: '500', }, planList: { paddingHorizontal: 24, }, // 移除旧的滑动样式 bottomSpacing: { height: 120, }, });