This commit is contained in:
imeepos
2025-11-12 13:19:06 +08:00
parent a7d922b535
commit 3ebcc24e9b
14 changed files with 483 additions and 265 deletions

View File

@@ -1,5 +1,5 @@
import { memo } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { memo, useCallback, useEffect, useRef } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
type Category = {
id: string;
@@ -13,18 +13,77 @@ type CategoryTabsProps = {
};
export function CategoryTabs({ categories, activeId, onChange }: CategoryTabsProps) {
const scrollViewRef = useRef<ScrollView>(null);
const tabPositionsRef = useRef<Map<string, { x: number; width: number }>>(new Map());
const containerWidthRef = useRef<number>(0);
const { width: screenWidth } = useWindowDimensions();
const scrollToActiveTab = useCallback(() => {
if (!activeId) {
console.log('[CategoryTabs] No activeId');
return;
}
const position = tabPositionsRef.current.get(activeId);
if (!position || !scrollViewRef.current) {
console.log('[CategoryTabs] Missing position or ref:', { position, hasRef: !!scrollViewRef.current });
return;
}
const centerOffset = screenWidth / 2;
const tabCenter = position.x + position.width / 2;
const scrollX = tabCenter - centerOffset;
const maxScrollX = Math.max(0, containerWidthRef.current - screenWidth);
const targetX = Math.max(0, Math.min(scrollX, maxScrollX));
console.log('[CategoryTabs] Scrolling to:', {
activeId,
position,
screenWidth,
tabCenter,
targetX,
containerWidth: containerWidthRef.current,
});
scrollViewRef.current.scrollTo({
x: targetX,
animated: true,
});
}, [activeId, screenWidth]);
useEffect(() => {
const timer = setTimeout(scrollToActiveTab, 150);
return () => clearTimeout(timer);
}, [scrollToActiveTab]);
const handleContentSizeChange = useCallback((width: number) => {
containerWidthRef.current = width;
console.log('[CategoryTabs] Content size:', width);
}, []);
return (
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
style={styles.scrollView}
onContentSizeChange={handleContentSizeChange}
>
{categories.map(category => (
{categories.map((category, index) => (
<CategoryTabItem
key={category.id}
category={category}
isActive={category.id === activeId}
onPress={onChange}
onLayout={(layout) => {
tabPositionsRef.current.set(category.id, {
x: layout.x,
width: layout.width,
});
console.log(`[CategoryTabs] Tab layout [${category.label}]:`, layout);
}}
/>
))}
</ScrollView>
@@ -35,11 +94,19 @@ type CategoryTabItemProps = {
category: Category;
isActive: boolean;
onPress: (id: string) => void;
onLayout: (layout: { x: number; width: number }) => void;
};
const CategoryTabItem = memo(({ category, isActive, onPress }: CategoryTabItemProps) => {
const CategoryTabItem = memo(({ category, isActive, onPress, onLayout }: CategoryTabItemProps) => {
return (
<Pressable onPress={() => onPress(category.id)} style={styles.tabButton}>
<Pressable
onPress={() => onPress(category.id)}
style={styles.tabButton}
onLayout={(event) => {
const { x, width } = event.nativeEvent.layout;
onLayout({ x, width });
}}
>
<Text style={[styles.label, isActive && styles.labelActive]}>{category.label}</Text>
<View style={[styles.indicator, isActive && styles.indicatorActive]} />
</Pressable>
@@ -48,6 +115,10 @@ const CategoryTabItem = memo(({ category, isActive, onPress }: CategoryTabItemPr
CategoryTabItem.displayName = 'CategoryTabItem';
const styles = StyleSheet.create({
scrollView: {
flexGrow: 0,
flexShrink: 0,
},
container: {
paddingVertical: 10,
paddingHorizontal: 6,

View File

@@ -20,6 +20,8 @@ export type CommunityItem = {
image: string;
chip: string;
actionLabel: string;
width?: number;
height?: number;
};
type CommunityGridProps = {
@@ -83,10 +85,16 @@ type CommunityCardProps = {
};
const CommunityCard = memo(({ item, style, onPressCard, onPressAction }: CommunityCardProps) => {
const aspectRatio = item.width && item.height ? item.width / item.height : 9 / 16;
return (
<Pressable onPress={() => onPressCard?.(item)} style={[styles.card, style]}>
<View style={styles.imageWrapper}>
<ExpoImage source={{ uri: item.image }} style={styles.cardImage} contentFit="cover" />
<ExpoImage
source={{ uri: item.image }}
style={[styles.cardImage, { aspectRatio }]}
contentFit="cover"
/>
<LinearGradient
colors={['rgba(12, 14, 20, 0)', 'rgba(12, 14, 20, 0.85)']}
style={styles.imageGradient}
@@ -169,7 +177,6 @@ const styles = StyleSheet.create({
},
cardImage: {
width: '100%',
aspectRatio: 9 / 16,
height: undefined,
},
imageGradient: {

View File

@@ -1,3 +1,4 @@
import { memo } from 'react';
import { StyleSheet, View } from 'react-native';
import Svg, { Path } from 'react-native-svg';
@@ -5,7 +6,7 @@ type HeaderProps = {
title?: string;
};
export function Header({ title = 'BESTAI' }: HeaderProps) {
export const Header = memo(function Header({ title = 'BESTAI' }: HeaderProps) {
return (
<View style={styles.container}>
<Svg width={89} height={13} viewBox="0 0 89 13" style={styles.logo}>
@@ -13,7 +14,7 @@ export function Header({ title = 'BESTAI' }: HeaderProps) {
</Svg>
</View>
);
}
});
const styles = StyleSheet.create({
container: {

View File

@@ -1,20 +1,43 @@
import { PropsWithChildren } from 'react';
import { memo, PropsWithChildren } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type PageLayoutProps = PropsWithChildren<{
backgroundColor?: string;
topInset?: 'auto' | 'none' | number;
horizontalPadding?: number | false;
}>;
export function PageLayout({ children, backgroundColor = '#050505' }: PageLayoutProps) {
export function PageLayout({
children,
backgroundColor = '#050505',
topInset = 'auto',
horizontalPadding = 24,
}: PageLayoutProps) {
const insets = useSafeAreaInsets();
const spacingTop = topInset === 'auto' ? insets.top : topInset === 'none' ? 0 : topInset;
const spacingBottom = Math.max(insets.bottom, 16);
const spacingHorizontal = horizontalPadding === false ? 0 : horizontalPadding;
return <View style={[styles.container, { paddingBottom: spacingBottom, backgroundColor }]}>{children}</View>;
return (
<View
style={[
styles.container,
{
paddingTop: spacingTop,
paddingBottom: spacingBottom,
paddingHorizontal: spacingHorizontal,
backgroundColor
}
]}
>
{children}
</View>
);
}
export function StatusBarSpacer() {
export const StatusBarSpacer = memo(function StatusBarSpacer() {
const insets = useSafeAreaInsets();
if (!insets.top) {
@@ -22,11 +45,10 @@ export function StatusBarSpacer() {
}
return <View style={{ height: insets.top }} />;
}
});
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 24,
},
});