✨ feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面 - 更新底部导航:将explore改为content,index改为home - 新增多个页面:积分、兑换、历史记录、结果展示、设置等 - 优化UI组件:新增通用按钮、分类标签、加载状态等组件 - 清理冗余文件:删除旧的认证文档和积分详情页面 - 更新主题配置:调整配色方案和视觉风格 - 修复导入路径:优化组件导入结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
162
components/CategoryTabs.tsx
Normal file
162
components/CategoryTabs.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
useDerivedValue,
|
||||
} from 'react-native-reanimated';
|
||||
import { Colors, Spacing, FontSize, Animation } from '@/constants/theme';
|
||||
|
||||
interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CategoryTabsProps {
|
||||
categories?: Category[];
|
||||
activeId?: string;
|
||||
onChange?: (category: Category) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const DEFAULT_CATEGORIES: Category[] = [
|
||||
{ id: 'all', name: '全部' },
|
||||
{ id: 'finance', name: '财经' },
|
||||
{ id: 'entertainment', name: '娱乐' },
|
||||
{ id: 'education', name: '教育' },
|
||||
{ id: 'technology', name: '科技' },
|
||||
{ id: 'sports', name: '体育' },
|
||||
{ id: 'lifestyle', name: '生活' },
|
||||
{ id: 'travel', name: '旅行' },
|
||||
];
|
||||
|
||||
const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||
categories = DEFAULT_CATEGORIES,
|
||||
activeId = 'all',
|
||||
onChange,
|
||||
style,
|
||||
}) => {
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
|
||||
const activeIndex = useMemo(() => {
|
||||
return categories.findIndex(cat => cat.id === activeId);
|
||||
}, [categories, activeId]);
|
||||
|
||||
const indicatorPosition = useDerivedValue(() => {
|
||||
if (categories.length === 0) return 0;
|
||||
|
||||
const activeIndexSafe = activeIndex >= 0 ? activeIndex : 0;
|
||||
const padding = 16;
|
||||
const gap = 8;
|
||||
let offset = padding;
|
||||
|
||||
for (let i = 0; i < activeIndexSafe; i++) {
|
||||
const item = categories[i];
|
||||
const itemWidth = Math.min(
|
||||
Math.max(44, item.name.length * 16 + 32),
|
||||
screenWidth / 3
|
||||
);
|
||||
offset += itemWidth + gap;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}, [activeIndex, categories, screenWidth]);
|
||||
|
||||
const activeCategory = categories[activeIndex] || categories[0];
|
||||
|
||||
const indicatorStyle = useAnimatedStyle(() => {
|
||||
if (!activeCategory) {
|
||||
return { width: 0, left: 0 };
|
||||
}
|
||||
|
||||
const width = Math.min(
|
||||
Math.max(44, activeCategory.name.length * 16 + 32),
|
||||
screenWidth / 3
|
||||
);
|
||||
|
||||
return {
|
||||
width: withTiming(width, { duration: Animation.duration.normal }),
|
||||
left: withTiming(indicatorPosition.value, { duration: Animation.duration.normal }),
|
||||
};
|
||||
});
|
||||
|
||||
const renderCategory = (category: Category, index: number) => {
|
||||
const isActive = category.id === activeId;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={category.id}
|
||||
style={styles.tabItem}
|
||||
onPress={() => onChange?.(category)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.tabText, isActive && styles.activeTabText]}>
|
||||
{category.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
decelerationRate="fast"
|
||||
snapToInterval={1}
|
||||
>
|
||||
{categories.map(renderCategory)}
|
||||
</ScrollView>
|
||||
|
||||
<Animated.View style={[styles.indicator, indicatorStyle]} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: Colors.background.secondary,
|
||||
width: '100%',
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: Spacing.md,
|
||||
paddingVertical: Spacing.sm,
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
tabItem: {
|
||||
minWidth: 44,
|
||||
paddingHorizontal: Spacing.md,
|
||||
paddingVertical: Spacing.sm,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
tabText: {
|
||||
fontSize: FontSize.sm,
|
||||
color: Colors.text.secondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
activeTabText: {
|
||||
color: Colors.brand.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
indicator: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
height: 2,
|
||||
backgroundColor: Colors.brand.primary,
|
||||
borderRadius: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(CategoryTabs);
|
||||
Reference in New Issue
Block a user