✨ feat: 完整应用重构 - 优化界面架构与用户体验
主要变更: - 重构应用界面:优化首页、登录、认证流程 - 新增用户档案模块:包含完整的档案管理组件 - 优化路由结构:重新组织页面布局和导航 - 改进API集成:新增活动、内容分类等API模块 - 删除冗余组件:清理不必要的文件和依赖 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
233
app/(tabs)/home.tsx
Normal file
233
app/(tabs)/home.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
|
||||
import {
|
||||
CategoryTabs,
|
||||
CommunityGrid,
|
||||
FeatureCarousel,
|
||||
Header,
|
||||
PageLayout,
|
||||
SectionHeader,
|
||||
StatusBarSpacer,
|
||||
type CommunityItem,
|
||||
type FeatureItem
|
||||
} from '@/components/bestai';
|
||||
import type { FeatureItem as FeatureItemType } from '@/components/bestai/feature-carousel';
|
||||
import { useAuth } from '@/hooks/use-auth';
|
||||
import { useAuthGuard } from '@/hooks/use-auth-guard';
|
||||
import { getActivities, type Activity } from '@/lib/api/activities';
|
||||
import { categoriesWithChildren } from '@/lib/api/categories-with-children';
|
||||
import type { CategoryTemplate, CategoryWithChildren } from '@/lib/types/template';
|
||||
|
||||
function coalesceText(...values: Array<string | null | undefined>): string {
|
||||
for (const value of values) {
|
||||
const trimmed = (value ?? '').trim();
|
||||
if (trimmed.length > 0) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function translateTemplateToCommunity(template: CategoryTemplate): CommunityItem | null {
|
||||
const image = coalesceText(template.coverImageUrl, template.previewUrl);
|
||||
const title = coalesceText(template.titleEn, template.title);
|
||||
if (!image || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryTag = template.tags?.[0];
|
||||
const chip = coalesceText(primaryTag?.nameEn, primaryTag?.name, template.aspectRatio) || 'Featured';
|
||||
|
||||
return {
|
||||
id: template.id,
|
||||
title,
|
||||
image,
|
||||
chip,
|
||||
actionLabel: 'Generate',
|
||||
};
|
||||
}
|
||||
|
||||
function translateActivity(activity: Activity): FeatureItem | null {
|
||||
const image = (activity.coverUrl || '').trim();
|
||||
const title = (activity.titleEn || '').trim() || (activity.title || '').trim();
|
||||
if (!image || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subtitle = (activity.descEn || '').trim() || (activity.desc || '').trim();
|
||||
|
||||
return {
|
||||
id: activity.id,
|
||||
title,
|
||||
subtitle,
|
||||
image,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ExploreScreen() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { requireAuth } = useAuthGuard();
|
||||
const [activeCategory, setActiveCategory] = useState<string>();
|
||||
const [activityFeatures, setActivityFeatures] = useState<FeatureItem[]>([]);
|
||||
const [categoryCollection, setCategoryCollection] = useState<CategoryWithChildren[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const hydrateFeatureCarousel = async () => {
|
||||
try {
|
||||
const activities = await getActivities({ isActive: true });
|
||||
console.log({ activities })
|
||||
|
||||
const curatedFeatures = activities
|
||||
.map(translateActivity)
|
||||
.filter((feature): feature is FeatureItem => feature !== null);
|
||||
if (curatedFeatures.length > 0) {
|
||||
setActivityFeatures(curatedFeatures);
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.warn('FeatureCarousel activities feed unavailable:', detail);
|
||||
}
|
||||
};
|
||||
|
||||
hydrateFeatureCarousel();
|
||||
|
||||
return () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
|
||||
const hydrateCategories = async () => {
|
||||
try {
|
||||
const response = await categoriesWithChildren();
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = response.success && Array.isArray(response.data) ? response.data : [];
|
||||
const curated = payload.filter((category) => category.id && (category.isActive ?? true));
|
||||
|
||||
setCategoryCollection(curated.length > 0 ? curated : payload);
|
||||
} catch (error) {
|
||||
if (isActive) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.warn('Categories feed unavailable:', detail);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
hydrateCategories();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (categoryCollection.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasActive = categoryCollection.some((category) => category.id === activeCategory);
|
||||
if (!hasActive) {
|
||||
setActiveCategory(categoryCollection[0].id);
|
||||
}
|
||||
}, [categoryCollection, activeCategory]);
|
||||
|
||||
const categoryOptions = useMemo(() => {
|
||||
if (categoryCollection.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = categoryCollection
|
||||
.map((category) => {
|
||||
const label = coalesceText(category.nameEn, category.name);
|
||||
return label
|
||||
? {
|
||||
id: category.id,
|
||||
label,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter((category): category is { id: string; label: string } => category !== null);
|
||||
|
||||
return options.length > 0 ? options : [];
|
||||
}, [categoryCollection]);
|
||||
|
||||
|
||||
const activeCategoryRecord = useMemo(() => {
|
||||
return categoryCollection.find((category) => category.id === activeCategory) ?? null;
|
||||
}, [categoryCollection, activeCategory]);
|
||||
|
||||
const templateCommunityItems = useMemo(() => {
|
||||
if (!activeCategoryRecord) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (activeCategoryRecord.templates ?? [])
|
||||
.map(translateTemplateToCommunity)
|
||||
.filter((item): item is CommunityItem => item !== null);
|
||||
}, [activeCategoryRecord]);
|
||||
|
||||
const featureItems = useMemo(() => {
|
||||
return activityFeatures.map((item, index) => ({
|
||||
...item,
|
||||
id: `${activeCategory}-${item.id || index}`,
|
||||
}));
|
||||
}, [activityFeatures]);
|
||||
|
||||
const communityItems = useMemo(() => {
|
||||
const source: CommunityItem[] =
|
||||
templateCommunityItems.length > 0 ? templateCommunityItems : [];
|
||||
|
||||
return source.map((item, index) => ({
|
||||
...item,
|
||||
id: `${activeCategory}-${item.id || index}`,
|
||||
}));
|
||||
}, [activeCategory, templateCommunityItems]);
|
||||
|
||||
const handleGeneratePress = (item: CommunityItem) => {
|
||||
requireAuth(() => {
|
||||
console.log('用户已登录,执行生成操作', item.id);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeaturePress = (item: FeatureItemType) => {
|
||||
requireAuth(() => {
|
||||
console.log('用户已登录,使用功能:', item.title);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout backgroundColor="#050505">
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<StatusBarSpacer />
|
||||
<Header />
|
||||
<CategoryTabs categories={categoryOptions} activeId={activeCategory || ``} onChange={setActiveCategory} />
|
||||
{featureItems.length > 0 && <FeatureCarousel items={featureItems} onPress={handleFeaturePress} />}
|
||||
<SectionHeader title={activeCategoryRecord?.name || ``} />
|
||||
<CommunityGrid items={communityItems} onPressAction={handleGeneratePress} />
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScrollView>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 48,
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: 32,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user