/** * 学科资料列表页:展示某个学科下的所有资料 */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { View, StyleSheet, ScrollView, TouchableOpacity, StatusBar, RefreshControl, FlatList, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, fontSizes, borderRadius, shadows, useAppColors, useResolvedColorScheme, type AppColors, } from '../../theme'; import * as hrefs from '../../navigation/hrefs'; import { Text, ResponsiveContainer, Loading } from '../../components/common'; import { useResponsive, useResponsiveSpacing, useBreakpointGTE } from '../../hooks/useResponsive'; import type { MaterialSubject, MaterialFile, MaterialFileType } from '../../types/material'; import { materialRepository, } from '../../data/repositories/MaterialRepository'; import { MATERIAL_FILE_TYPE_ICONS, MATERIAL_FILE_TYPE_COLORS, formatFileSize, } from '../../types/material'; export const SubjectMaterialsScreen: React.FC = () => { const colors = useAppColors(); const resolvedScheme = useResolvedColorScheme(); const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content'; const styles = useMemo(() => createSubjectMaterialsStyles(colors), [colors]); const router = useRouter(); const params = useLocalSearchParams<{ subjectId: string }>(); const insets = useSafeAreaInsets(); const { isMobile } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const [subject, setSubject] = useState(null); const [materials, setMaterials] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [selectedFileType, setSelectedFileType] = useState(null); const scrollBottomInset = isMobile ? 88 + insets.bottom + spacing.md : spacing['3xl']; const loadData = useCallback(async () => { if (!params.subjectId) { setError('缺少学科ID'); setIsLoading(false); return; } try { setError(null); const [subjectData, materialsData] = await Promise.all([ materialRepository.getSubjectById(params.subjectId), materialRepository.getMaterials({ subjectId: params.subjectId }), ]); setSubject(subjectData); setMaterials(materialsData.materials); } catch (err) { setError('加载资料列表失败'); console.error('Failed to load materials:', err); } finally { setIsLoading(false); setIsRefreshing(false); } }, [params.subjectId]); useEffect(() => { loadData(); }, [loadData]); const handleRefresh = useCallback(() => { setIsRefreshing(true); loadData(); }, [loadData]); const onOpenMaterial = useCallback( (materialId: string) => { router.push(hrefs.hrefMaterialDetail(materialId)); }, [router] ); const filteredMaterials = useMemo(() => { if (!selectedFileType) return materials; return materials.filter((m) => m.file_type === selectedFileType); }, [materials, selectedFileType]); const fileTypes: { type: MaterialFileType | null; label: string }[] = [ { type: null, label: '全部' }, { type: 'pdf', label: 'PDF' }, { type: 'word', label: 'Word' }, { type: 'ppt', label: 'PPT' }, ]; const cardStyle = useMemo( () => ({ backgroundColor: colors.background.paper, borderRadius: 12, overflow: 'hidden' as const, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 1, }), [colors] ); const renderMaterialItem = ({ item }: { item: MaterialFile }) => { const fileColor = MATERIAL_FILE_TYPE_COLORS[item.file_type]; const iconName = MATERIAL_FILE_TYPE_ICONS[item.file_type]; return ( onOpenMaterial(item.id)} activeOpacity={0.85} > {item.title} {formatFileSize(item.file_size)} {' · '} {item.download_count} 次下载 {item.description && ( {item.description} )} ); }; const renderFilterTabs = () => ( {fileTypes.map(({ type, label }) => ( setSelectedFileType(type)} activeOpacity={0.7} > {label} ))} ); const renderEmptyComponent = () => ( {selectedFileType ? '该类型暂无资料' : '暂无资料'} ); const renderContent = () => { if (isLoading) { return ( ); } if (error) { return ( {error} 重试 ); } return ( item.id} renderItem={renderMaterialItem} ListHeaderComponent={renderFilterTabs} ListEmptyComponent={renderEmptyComponent} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} refreshControl={ } /> ); }; return ( {/* Header */} router.back()} style={styles.backButton}> {subject?.name || '资料列表'} {/* Content */} {isWideScreen ? ( {renderContent()} ) : ( {renderContent()} )} ); }; export default SubjectMaterialsScreen; function createSubjectMaterialsStyles(colors: AppColors) { const headerBg = colors.background.default; return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, backgroundColor: headerBg, ...shadows.sm, }, headerWide: { paddingHorizontal: spacing.lg, paddingVertical: spacing.lg, }, headerLeft: { width: 44, alignItems: 'flex-start', }, backButton: { padding: spacing.xs, }, headerCenter: { flexDirection: 'row', alignItems: 'center', }, headerTitle: { fontSize: 19, fontWeight: '700', color: colors.text.primary, }, headerTitleWide: { fontSize: 22, }, headerRight: { width: 44, alignItems: 'flex-end', }, searchButton: { padding: spacing.xs, }, contentWrapper: { flex: 1, }, listContent: { paddingTop: spacing.md, }, filterContainer: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.md, }, filterTab: { paddingVertical: spacing.sm, paddingHorizontal: spacing.md, borderRadius: borderRadius.full, backgroundColor: colors.background.paper, }, filterTabActive: { // backgroundColor set inline }, filterTabText: { fontWeight: '600', }, materialCard: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, paddingHorizontal: spacing.md, marginBottom: spacing.sm, }, fileTypeIcon: { width: 44, height: 44, borderRadius: borderRadius.md, alignItems: 'center', justifyContent: 'center', }, materialContent: { flex: 1, marginLeft: spacing.md, marginRight: spacing.sm, }, materialTitle: { fontWeight: '600', fontSize: fontSizes.md, color: colors.text.primary, }, materialMeta: { flexDirection: 'row', alignItems: 'center', marginTop: 4, }, centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: spacing['3xl'], }, errorText: { marginTop: spacing.md, textAlign: 'center', }, emptyText: { marginTop: spacing.md, textAlign: 'center', }, retryButton: { marginTop: spacing.md, paddingVertical: spacing.sm, paddingHorizontal: spacing.lg, }, }); }