From 9529ea39c4e8817ebed8ad529157bff4973d0996 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 25 Mar 2026 21:17:17 +0800 Subject: [PATCH] feat(Materials): add new materials navigation and href functions - Introduced href functions for accessing materials, including hrefMaterials, hrefMaterialSubject, and hrefMaterialDetail. - Updated AppsScreen to include a new entry for materials, enhancing the app's educational resources section. - Exported material types in index.ts to support the new materials functionality. --- app/(app)/(tabs)/apps/materials/_layout.tsx | 11 + app/(app)/(tabs)/apps/materials/detail.tsx | 5 + app/(app)/(tabs)/apps/materials/index.tsx | 5 + app/(app)/(tabs)/apps/materials/subject.tsx | 5 + src/data/mock/materialMockData.ts | 418 +++++++++++++++ src/data/repositories/MaterialRepository.ts | 289 ++++++++++ src/navigation/hrefs.ts | 14 + src/screens/apps/AppsScreen.tsx | 7 + src/screens/material/MaterialDetailScreen.tsx | 500 ++++++++++++++++++ src/screens/material/MaterialsScreen.tsx | 331 ++++++++++++ .../material/SubjectMaterialsScreen.tsx | 404 ++++++++++++++ src/screens/material/index.ts | 6 + src/types/index.ts | 1 + src/types/material.ts | 103 ++++ 14 files changed, 2099 insertions(+) create mode 100644 app/(app)/(tabs)/apps/materials/_layout.tsx create mode 100644 app/(app)/(tabs)/apps/materials/detail.tsx create mode 100644 app/(app)/(tabs)/apps/materials/index.tsx create mode 100644 app/(app)/(tabs)/apps/materials/subject.tsx create mode 100644 src/data/mock/materialMockData.ts create mode 100644 src/data/repositories/MaterialRepository.ts create mode 100644 src/screens/material/MaterialDetailScreen.tsx create mode 100644 src/screens/material/MaterialsScreen.tsx create mode 100644 src/screens/material/SubjectMaterialsScreen.tsx create mode 100644 src/screens/material/index.ts create mode 100644 src/types/material.ts diff --git a/app/(app)/(tabs)/apps/materials/_layout.tsx b/app/(app)/(tabs)/apps/materials/_layout.tsx new file mode 100644 index 0000000..bf8d863 --- /dev/null +++ b/app/(app)/(tabs)/apps/materials/_layout.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; + +export default function MaterialsStackLayout() { + return ( + + + + + + ); +} \ No newline at end of file diff --git a/app/(app)/(tabs)/apps/materials/detail.tsx b/app/(app)/(tabs)/apps/materials/detail.tsx new file mode 100644 index 0000000..287e66b --- /dev/null +++ b/app/(app)/(tabs)/apps/materials/detail.tsx @@ -0,0 +1,5 @@ +import { MaterialDetailScreen } from '../../../../../src/screens/material'; + +export default function MaterialDetailRoute() { + return ; +} \ No newline at end of file diff --git a/app/(app)/(tabs)/apps/materials/index.tsx b/app/(app)/(tabs)/apps/materials/index.tsx new file mode 100644 index 0000000..b947e5a --- /dev/null +++ b/app/(app)/(tabs)/apps/materials/index.tsx @@ -0,0 +1,5 @@ +import { MaterialsScreen } from '../../../../../src/screens/material'; + +export default function MaterialsRoute() { + return ; +} \ No newline at end of file diff --git a/app/(app)/(tabs)/apps/materials/subject.tsx b/app/(app)/(tabs)/apps/materials/subject.tsx new file mode 100644 index 0000000..cd228f9 --- /dev/null +++ b/app/(app)/(tabs)/apps/materials/subject.tsx @@ -0,0 +1,5 @@ +import { SubjectMaterialsScreen } from '../../../../../src/screens/material'; + +export default function SubjectMaterialsRoute() { + return ; +} \ No newline at end of file diff --git a/src/data/mock/materialMockData.ts b/src/data/mock/materialMockData.ts new file mode 100644 index 0000000..dffed4d --- /dev/null +++ b/src/data/mock/materialMockData.ts @@ -0,0 +1,418 @@ +/** + * 学习资料 Mock 数据 + */ +import type { MaterialSubject, MaterialFile, MaterialFileType, SUBJECT_COLORS } from '../../types/material'; + +// 学科分类 Mock 数据 +export const MOCK_SUBJECTS: MaterialSubject[] = [ + { + id: 'subject-math', + name: '数学', + icon: 'calculator-variant', + color: '#FF6B6B', + description: '高等数学、线性代数、概率论等', + sort_order: 1, + material_count: 15, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-20T10:00:00Z', + }, + { + id: 'subject-english', + name: '英语', + icon: 'translate', + color: '#4ECDC4', + description: '四六级、考研英语、托福雅思等', + sort_order: 2, + material_count: 12, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-18T14:30:00Z', + }, + { + id: 'subject-physics', + name: '物理', + icon: 'atom', + color: '#45B7D1', + description: '大学物理、电磁学、力学等', + sort_order: 3, + material_count: 8, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-15T09:00:00Z', + }, + { + id: 'subject-chemistry', + name: '化学', + icon: 'flask', + color: '#96CEB4', + description: '有机化学、无机化学、分析化学等', + sort_order: 4, + material_count: 6, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-10T16:00:00Z', + }, + { + id: 'subject-cs', + name: '计算机', + icon: 'laptop', + color: '#FFEAA7', + description: '数据结构、算法、操作系统等', + sort_order: 5, + material_count: 20, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-22T11:30:00Z', + }, + { + id: 'subject-economics', + name: '经济管理', + icon: 'chart-line', + color: '#DDA0DD', + description: '微观经济学、宏观经济学、管理学等', + sort_order: 6, + material_count: 10, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-19T08:45:00Z', + }, + { + id: 'subject-law', + name: '法学', + icon: 'scale-balance', + color: '#F39C12', + description: '民法、刑法、宪法等', + sort_order: 7, + material_count: 7, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-12T13:20:00Z', + }, + { + id: 'subject-chinese', + name: '语文文学', + icon: 'book-open-page-variant', + color: '#3498DB', + description: '古代文学、现代文学、写作等', + sort_order: 8, + material_count: 5, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-03-08T15:00:00Z', + }, +]; + +// 文件资料 Mock 数据 +export const MOCK_MATERIALS: MaterialFile[] = [ + // 数学 + { + id: 'math-001', + subject_id: 'subject-math', + title: '高等数学上册重点笔记', + description: '包含极限、导数、积分等重点章节的详细笔记,配有典型例题解析', + file_type: 'pdf', + file_size: 5242880, // 5MB + file_url: 'https://example.com/materials/math/advanced_math_1.pdf', + file_name: '高等数学上册重点笔记.pdf', + download_count: 1256, + author_name: '张同学', + tags: ['高等数学', '期末复习', '重点笔记'], + created_at: '2024-01-15T10:00:00Z', + updated_at: '2024-02-20T14:30:00Z', + }, + { + id: 'math-002', + subject_id: 'subject-math', + title: '线性代数公式大全', + description: '线性代数所有公式汇总,适合考前突击复习', + file_type: 'pdf', + file_size: 1572864, // 1.5MB + file_url: 'https://example.com/materials/math/linear_algebra_formulas.pdf', + file_name: '线性代数公式大全.pdf', + download_count: 2341, + author_name: '李同学', + tags: ['线性代数', '公式', '复习'], + created_at: '2024-01-20T09:00:00Z', + updated_at: '2024-01-20T09:00:00Z', + }, + { + id: 'math-003', + subject_id: 'subject-math', + title: '概率论与数理统计课件', + description: '概率论课程完整PPT课件,包含所有章节', + file_type: 'ppt', + file_size: 15728640, // 15MB + file_url: 'https://example.com/materials/math/probability_slides.pptx', + file_name: '概率论与数理统计课件.pptx', + download_count: 876, + author_name: '王老师', + tags: ['概率论', '课件', 'PPT'], + created_at: '2024-02-01T08:00:00Z', + updated_at: '2024-02-15T10:00:00Z', + }, + // 英语 + { + id: 'english-001', + subject_id: 'subject-english', + title: '四级核心词汇手册', + description: '精选四级核心词汇2000个,附带例句和记忆技巧', + file_type: 'pdf', + file_size: 3145728, // 3MB + file_url: 'https://example.com/materials/english/cet4_vocabulary.pdf', + file_name: '四级核心词汇手册.pdf', + download_count: 3567, + author_name: '刘同学', + tags: ['四级', '词汇', '备考'], + created_at: '2024-01-10T11:00:00Z', + updated_at: '2024-02-28T16:00:00Z', + }, + { + id: 'english-002', + subject_id: 'subject-english', + title: '六级阅读理解技巧总结', + description: '六级阅读理解题型分析与解题技巧,包含真题解析', + file_type: 'word', + file_size: 2097152, // 2MB + file_url: 'https://example.com/materials/english/cet6_reading.docx', + file_name: '六级阅读理解技巧总结.docx', + download_count: 1892, + author_name: '陈同学', + tags: ['六级', '阅读理解', '技巧'], + created_at: '2024-01-25T14:00:00Z', + updated_at: '2024-01-25T14:00:00Z', + }, + { + id: 'english-003', + subject_id: 'subject-english', + title: '考研英语作文模板', + description: '考研英语大作文、小作文模板合集,覆盖常考话题', + file_type: 'pdf', + file_size: 1048576, // 1MB + file_url: 'https://example.com/materials/english/kaoyan_writing.pdf', + file_name: '考研英语作文模板.pdf', + download_count: 4521, + author_name: '周同学', + tags: ['考研', '英语', '作文'], + created_at: '2024-02-05T10:30:00Z', + updated_at: '2024-03-10T09:00:00Z', + }, + // 物理 + { + id: 'physics-001', + subject_id: 'subject-physics', + title: '大学物理期末复习提纲', + description: '大学物理上下册复习要点,包含公式总结和典型例题', + file_type: 'pdf', + file_size: 4194304, // 4MB + file_url: 'https://example.com/materials/physics/university_physics_review.pdf', + file_name: '大学物理期末复习提纲.pdf', + download_count: 987, + author_name: '吴同学', + tags: ['大学物理', '复习', '期末'], + created_at: '2024-01-18T15:00:00Z', + updated_at: '2024-02-22T11:00:00Z', + }, + { + id: 'physics-002', + subject_id: 'subject-physics', + title: '电磁学实验报告模板', + description: '电磁学实验报告标准格式和填写示例', + file_type: 'word', + file_size: 524288, // 512KB + file_url: 'https://example.com/materials/physics/em_lab_template.docx', + file_name: '电磁学实验报告模板.docx', + download_count: 654, + author_name: '实验室', + tags: ['电磁学', '实验', '报告'], + created_at: '2024-02-10T09:00:00Z', + updated_at: '2024-02-10T09:00:00Z', + }, + // 化学 + { + id: 'chemistry-001', + subject_id: 'subject-chemistry', + title: '有机化学反应机理汇总', + description: '有机化学常见反应机理图文详解', + file_type: 'pdf', + file_size: 8388608, // 8MB + file_url: 'https://example.com/materials/chemistry/organic_mechanisms.pdf', + file_name: '有机化学反应机理汇总.pdf', + download_count: 1123, + author_name: '赵同学', + tags: ['有机化学', '反应机理', '重点'], + created_at: '2024-01-22T10:00:00Z', + updated_at: '2024-02-18T14:00:00Z', + }, + // 计算机 + { + id: 'cs-001', + subject_id: 'subject-cs', + title: '数据结构与算法笔记', + description: '数据结构核心内容整理,包含时间复杂度分析和代码实现', + file_type: 'pdf', + file_size: 7340032, // 7MB + file_url: 'https://example.com/materials/cs/data_structures.pdf', + file_name: '数据结构与算法笔记.pdf', + download_count: 5678, + author_name: '孙同学', + tags: ['数据结构', '算法', '笔记'], + created_at: '2024-01-05T08:00:00Z', + updated_at: '2024-03-15T10:30:00Z', + }, + { + id: 'cs-002', + subject_id: 'subject-cs', + title: '操作系统概念详解', + description: '操作系统核心概念讲解,进程、内存、文件系统等', + file_type: 'pdf', + file_size: 6291456, // 6MB + file_url: 'https://example.com/materials/cs/operating_systems.pdf', + file_name: '操作系统概念详解.pdf', + download_count: 3421, + author_name: '钱同学', + tags: ['操作系统', '计算机', '笔记'], + created_at: '2024-01-12T09:30:00Z', + updated_at: '2024-02-25T16:00:00Z', + }, + { + id: 'cs-003', + subject_id: 'subject-cs', + title: '计算机网络课件', + description: '计算机网络课程完整PPT,涵盖TCP/IP协议栈', + file_type: 'ppt', + file_size: 20971520, // 20MB + file_url: 'https://example.com/materials/cs/network_slides.pptx', + file_name: '计算机网络课件.pptx', + download_count: 2134, + author_name: '计算机学院', + tags: ['计算机网络', '课件', 'PPT'], + created_at: '2024-02-08T10:00:00Z', + updated_at: '2024-02-28T09:00:00Z', + }, + { + id: 'cs-004', + subject_id: 'subject-cs', + title: 'LeetCode刷题笔记', + description: 'LeetCode热门题目解题思路和代码实现', + file_type: 'pdf', + file_size: 10485760, // 10MB + file_url: 'https://example.com/materials/cs/leetcode_notes.pdf', + file_name: 'LeetCode刷题笔记.pdf', + download_count: 8765, + author_name: '郑同学', + tags: ['算法', '面试', 'LeetCode'], + created_at: '2024-01-28T14:00:00Z', + updated_at: '2024-03-20T11:00:00Z', + }, + // 经济管理 + { + id: 'economics-001', + subject_id: 'subject-economics', + title: '微观经济学重点总结', + description: '微观经济学核心理论和图形分析', + file_type: 'pdf', + file_size: 3145728, // 3MB + file_url: 'https://example.com/materials/economics/microeconomics.pdf', + file_name: '微观经济学重点总结.pdf', + download_count: 1456, + author_name: '冯同学', + tags: ['微观经济学', '重点', '复习'], + created_at: '2024-01-30T11:00:00Z', + updated_at: '2024-02-20T15:00:00Z', + }, + { + id: 'economics-002', + subject_id: 'subject-economics', + title: '管理学案例分析模板', + description: '管理学课程案例分析报告格式和范例', + file_type: 'word', + file_size: 1572864, // 1.5MB + file_url: 'https://example.com/materials/economics/management_case.docx', + file_name: '管理学案例分析模板.docx', + download_count: 876, + author_name: '商学院', + tags: ['管理学', '案例分析', '模板'], + created_at: '2024-02-12T10:00:00Z', + updated_at: '2024-02-12T10:00:00Z', + }, + // 法学 + { + id: 'law-001', + subject_id: 'subject-law', + title: '民法总则笔记', + description: '民法总则核心知识点梳理', + file_type: 'pdf', + file_size: 2621440, // 2.5MB + file_url: 'https://example.com/materials/law/civil_law.pdf', + file_name: '民法总则笔记.pdf', + download_count: 765, + author_name: '何同学', + tags: ['民法', '笔记', '法学'], + created_at: '2024-02-03T09:00:00Z', + updated_at: '2024-02-15T14:00:00Z', + }, + // 语文文学 + { + id: 'chinese-001', + subject_id: 'subject-chinese', + title: '古代文学作品选读', + description: '中国古代文学经典篇目赏析', + file_type: 'pdf', + file_size: 5242880, // 5MB + file_url: 'https://example.com/materials/chinese/ancient_literature.pdf', + file_name: '古代文学作品选读.pdf', + download_count: 543, + author_name: '中文系', + tags: ['古代文学', '作品选', '赏析'], + created_at: '2024-02-18T14:00:00Z', + updated_at: '2024-02-18T14:00:00Z', + }, +]; + +// 获取学科列表 +export function getMockSubjects(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve([...MOCK_SUBJECTS].sort((a, b) => a.sort_order - b.sort_order)); + }, 300); + }); +} + +// 根据学科ID获取资料列表 +export function getMockMaterialsBySubject(subjectId: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const materials = MOCK_MATERIALS.filter((m) => m.subject_id === subjectId); + resolve(materials); + }, 300); + }); +} + +// 搜索资料 +export function searchMockMaterials(keyword: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const lowerKeyword = keyword.toLowerCase(); + const materials = MOCK_MATERIALS.filter( + (m) => + m.title.toLowerCase().includes(lowerKeyword) || + m.description?.toLowerCase().includes(lowerKeyword) || + m.tags?.some((t) => t.toLowerCase().includes(lowerKeyword)) + ); + resolve(materials); + }, 300); + }); +} + +// 根据ID获取资料详情 +export function getMockMaterialById(id: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const material = MOCK_MATERIALS.find((m) => m.id === id); + resolve(material || null); + }, 200); + }); +} + +// 根据ID获取学科详情 +export function getMockSubjectById(id: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const subject = MOCK_SUBJECTS.find((s) => s.id === id); + resolve(subject || null); + }, 200); + }); +} diff --git a/src/data/repositories/MaterialRepository.ts b/src/data/repositories/MaterialRepository.ts new file mode 100644 index 0000000..a94c179 --- /dev/null +++ b/src/data/repositories/MaterialRepository.ts @@ -0,0 +1,289 @@ +/** + * 学习资料 Repository + * 提供学习资料的数据访问接口 + */ +import type { + MaterialSubject, + MaterialFile, + MaterialFileType, +} from '../../types/material'; +import { api } from '../../services/api'; + +// ==================== 接口定义 ==================== + +export interface GetMaterialsParams { + subjectId?: string; + fileType?: MaterialFileType; + keyword?: string; + sortBy?: 'createdAt' | 'downloadCount' | 'title'; + sortOrder?: 'asc' | 'desc'; + page?: number; + pageSize?: number; +} + +export interface MaterialsResult { + materials: MaterialFile[]; + total: number; + page: number; + pageSize: number; + hasMore: boolean; +} + +// ==================== API 响应类型 ==================== + +interface SubjectResponse { + id: string; + name: string; + icon: string; + color: string; + description: string; + sort_order: number; + is_active: boolean; + material_count: number; + created_at: string; + updated_at: string; +} + +interface MaterialFileResponse { + id: string; + subject_id: string; + title: string; + description: string; + file_type: string; + file_size: number; + file_url: string; + file_name: string; + download_count: number; + author_id: string; + tags: string[]; + status: string; + created_at: string; + updated_at: string; + subject?: SubjectResponse; +} + +// ==================== 转换函数 ==================== + +function toMaterialSubject(res: SubjectResponse): MaterialSubject { + return { + id: res.id, + name: res.name, + icon: res.icon, + color: res.color, + description: res.description, + sort_order: res.sort_order, + is_active: res.is_active, + material_count: res.material_count, + created_at: res.created_at, + updated_at: res.updated_at, + }; +} + +function toMaterialFile(res: MaterialFileResponse): MaterialFile { + return { + id: res.id, + subject_id: res.subject_id, + title: res.title, + description: res.description, + file_type: res.file_type as MaterialFileType, + file_size: res.file_size, + file_url: res.file_url, + file_name: res.file_name, + download_count: res.download_count, + author_id: res.author_id, + tags: res.tags || [], + status: res.status as 'active' | 'inactive', + created_at: res.created_at, + updated_at: res.updated_at, + subject: res.subject ? toMaterialSubject(res.subject) : undefined, + }; +} + +// ==================== Repository 实现 ==================== + +/** + * 学习资料数据仓库 + * 使用真实 API + */ +export class MaterialRepository { + private static instance: MaterialRepository; + + private constructor() {} + + static getInstance(): MaterialRepository { + if (!MaterialRepository.instance) { + MaterialRepository.instance = new MaterialRepository(); + } + return MaterialRepository.instance; + } + + /** + * 获取学科分类列表 + */ + async getSubjects(): Promise { + try { + const response = await api.get<{ list: SubjectResponse[] }>('/materials/subjects'); + return response.data.list.map(toMaterialSubject); + } catch (error) { + console.error('获取学科列表失败:', error); + return []; + } + } + + /** + * 根据ID获取学科详情 + */ + async getSubjectById(id: string): Promise { + try { + const response = await api.get(`/materials/subjects/${id}`); + return toMaterialSubject(response.data); + } catch (error) { + console.error('获取学科详情失败:', error); + return null; + } + } + + /** + * 获取资料列表 + */ + async getMaterials(params: GetMaterialsParams): Promise { + const { + subjectId, + fileType, + keyword, + sortBy = 'createdAt', + sortOrder = 'desc', + page = 1, + pageSize = 20, + } = params; + + try { + const queryParams: Record = { + page, + page_size: pageSize, + }; + + if (subjectId) { + queryParams.subject_id = subjectId; + } + if (fileType) { + queryParams.file_type = fileType; + } + if (keyword) { + queryParams.keyword = keyword; + } + + const response = await api.get<{ + list: MaterialFileResponse[]; + total: number; + page: number; + page_size: number; + has_more: boolean; + }>('/materials', queryParams); + + return { + materials: response.data.list.map(toMaterialFile), + total: response.data.total, + page: response.data.page, + pageSize: response.data.page_size, + hasMore: response.data.has_more, + }; + } catch (error) { + console.error('获取资料列表失败:', error); + return { + materials: [], + total: 0, + page: 1, + pageSize, + hasMore: false, + }; + } + } + + /** + * 根据ID获取资料详情 + */ + async getMaterialById(id: string): Promise { + try { + const response = await api.get(`/materials/${id}`); + return toMaterialFile(response.data); + } catch (error) { + console.error('获取资料详情失败:', error); + return null; + } + } + + /** + * 搜索资料 + */ + async searchMaterials(keyword: string, params?: { fileType?: MaterialFileType }): Promise { + return this.getMaterials({ + keyword, + fileType: params?.fileType, + }); + } + + /** + * 获取热门资料 + */ + async getHotMaterials(limit: number = 10): Promise { + try { + const response = await api.get<{ + list: MaterialFileResponse[]; + total: number; + page: number; + page_size: number; + has_more: boolean; + }>('/materials', { + page: 1, + page_size: limit, + sort_by: 'download_count', + sort_order: 'desc', + }); + return response.data.list.map(toMaterialFile); + } catch (error) { + console.error('获取热门资料失败:', error); + return []; + } + } + + /** + * 获取最新资料 + */ + async getLatestMaterials(limit: number = 10): Promise { + try { + const response = await api.get<{ + list: MaterialFileResponse[]; + total: number; + page: number; + page_size: number; + has_more: boolean; + }>('/materials', { + page: 1, + page_size: limit, + sort_by: 'created_at', + sort_order: 'desc', + }); + return response.data.list.map(toMaterialFile); + } catch (error) { + console.error('获取最新资料失败:', error); + return []; + } + } + + /** + * 获取资料的下载链接(增加下载次数) + */ + async downloadMaterial(id: string): Promise<{ file_url: string; file_name: string } | null> { + try { + const response = await api.get<{ file_url: string; file_name: string }>(`/materials/${id}/download`); + return response.data; + } catch (error) { + console.error('获取下载链接失败:', error); + return null; + } + } +} + +// 导出单例 +export const materialRepository = MaterialRepository.getInstance(); \ No newline at end of file diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts index 93e2c82..a169d5c 100644 --- a/src/navigation/hrefs.ts +++ b/src/navigation/hrefs.ts @@ -155,3 +155,17 @@ export function hrefAuthRegister(): string { export function hrefAuthForgot(): string { return '/forgot-password'; } + +// ==================== Materials (学习资料) ==================== + +export function hrefMaterials(): string { + return '/apps/materials'; +} + +export function hrefMaterialSubject(subjectId: string): string { + return `/apps/materials/subject?subjectId=${encodeURIComponent(subjectId)}`; +} + +export function hrefMaterialDetail(materialId: string): string { + return `/apps/materials/detail?materialId=${encodeURIComponent(materialId)}`; +} diff --git a/src/screens/apps/AppsScreen.tsx b/src/screens/apps/AppsScreen.tsx index 4c4da95..acd9be3 100644 --- a/src/screens/apps/AppsScreen.tsx +++ b/src/screens/apps/AppsScreen.tsx @@ -37,6 +37,13 @@ const APP_ENTRIES: AppItem[] = [ icon: 'calendar-week', href: hrefs.hrefSchedule(), }, + { + id: 'materials', + title: '学习资料', + subtitle: '按学科分类的学习资源网盘', + icon: 'folder-multiple', + href: hrefs.hrefMaterials(), + }, ]; export const AppsScreen: React.FC = () => { diff --git a/src/screens/material/MaterialDetailScreen.tsx b/src/screens/material/MaterialDetailScreen.tsx new file mode 100644 index 0000000..32d3237 --- /dev/null +++ b/src/screens/material/MaterialDetailScreen.tsx @@ -0,0 +1,500 @@ +/** + * 资料详情页:展示资料详细信息 + */ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + View, + StyleSheet, + ScrollView, + TouchableOpacity, + StatusBar, + Alert, +} 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 { Text, ResponsiveContainer, Loading } from '../../components/common'; +import { useResponsive, useResponsiveSpacing, useBreakpointGTE } from '../../hooks/useResponsive'; +import type { MaterialFile } from '../../types/material'; +import { materialRepository } from '../../data/repositories/MaterialRepository'; +import { + MATERIAL_FILE_TYPE_ICONS, + MATERIAL_FILE_TYPE_COLORS, + MATERIAL_FILE_TYPE_NAMES, + formatFileSize, +} from '../../types/material'; + +export const MaterialDetailScreen: React.FC = () => { + const colors = useAppColors(); + const resolvedScheme = useResolvedColorScheme(); + const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content'; + const styles = useMemo(() => createMaterialDetailStyles(colors), [colors]); + const router = useRouter(); + const params = useLocalSearchParams<{ materialId: 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 [material, setMaterial] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const scrollBottomInset = isMobile ? 88 + insets.bottom + spacing.md : spacing['3xl']; + + const loadMaterial = useCallback(async () => { + if (!params.materialId) { + setError('缺少资料ID'); + setIsLoading(false); + return; + } + + try { + setError(null); + const data = await materialRepository.getMaterialById(params.materialId); + setMaterial(data); + } catch (err) { + setError('加载资料详情失败'); + console.error('Failed to load material:', err); + } finally { + setIsLoading(false); + } + }, [params.materialId]); + + useEffect(() => { + loadMaterial(); + }, [loadMaterial]); + + const handleDownload = useCallback(() => { + if (!material) return; + + Alert.alert( + '下载资料', + `确定要下载「${material.title}」吗?`, + [ + { text: '取消', style: 'cancel' }, + { + text: '下载', + onPress: () => { + // Mock下载逻辑 + Alert.alert('提示', '下载功能开发中,请稍后再试'); + }, + }, + ] + ); + }, [material]); + + const handlePreview = useCallback(() => { + if (!material?.file_url) return; + + Alert.alert( + '在线预览', + '在线预览功能开发中,请稍后再试', + [{ text: '确定', style: 'default' }] + ); + }, [material]); + + const formatDate = useCallback((dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }, []); + + const cardStyle = useMemo( + () => ({ + backgroundColor: colors.background.paper, + borderRadius: 16, + overflow: 'hidden' as const, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, + }), + [colors] + ); + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } + + if (error || !material) { + return ( + + + + {error || '资料不存在'} + + router.back()} style={styles.retryButton}> + + 返回 + + + + ); + } + + const fileColor = MATERIAL_FILE_TYPE_COLORS[material.file_type]; + const iconName = MATERIAL_FILE_TYPE_ICONS[material.file_type]; + const fileTypeName = MATERIAL_FILE_TYPE_NAMES[material.file_type]; + + return ( + <> + {/* 文件信息卡片 */} + + + + + {fileTypeName} + + + + + {material.title} + + {material.description && ( + + {material.description} + + )} + + + + + {formatFileSize(material.file_size)} + + + + + + {material.download_count} 次下载 + + + + + + + {/* 详细信息 */} + + + 详细信息 + + + + 文件名 + + + {material.file_name} + + + + + 上传时间 + + + {formatDate(material.created_at)} + + + {material.author_name && ( + + + 上传者 + + + {material.author_name} + + + )} + {material.tags && material.tags.length > 0 && ( + + + 标签 + + + {material.tags.map((tag, index) => ( + + + {tag} + + + ))} + + + )} + + + {/* 操作按钮 */} + + + + + 在线预览 + + + + + + 下载资料 + + + + + ); + }; + + return ( + + + + {/* Header */} + + + router.back()} style={styles.backButton}> + + + + + + 资料详情 + + + + + + + + + + {/* Content */} + {isWideScreen ? ( + + + {renderContent()} + + + ) : ( + + {renderContent()} + + )} + + ); +}; + +export default MaterialDetailScreen; + +function createMaterialDetailStyles(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', + }, + shareButton: { + padding: spacing.xs, + }, + scroll: { + flex: 1, + backgroundColor: headerBg, + }, + scrollContent: { + paddingTop: spacing.md, + flexGrow: 1, + backgroundColor: headerBg, + }, + centerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: spacing['3xl'], + }, + errorText: { + marginTop: spacing.md, + textAlign: 'center', + }, + retryButton: { + marginTop: spacing.md, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.lg, + }, + fileCard: { + marginBottom: spacing.md, + }, + fileTypeBanner: { + height: 100, + alignItems: 'center', + justifyContent: 'center', + }, + fileTypeName: { + color: '#FFFFFF', + fontWeight: '700', + fontSize: fontSizes.lg, + marginTop: spacing.sm, + }, + fileInfo: { + padding: spacing.lg, + }, + fileName: { + color: colors.text.primary, + fontWeight: '700', + fontSize: fontSizes.lg, + }, + fileDescription: { + marginTop: spacing.sm, + lineHeight: fontSizes.md * 1.5, + }, + fileMetaRow: { + flexDirection: 'row', + marginTop: spacing.md, + gap: spacing.md, + }, + metaItem: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + detailCard: { + padding: spacing.lg, + marginBottom: spacing.md, + }, + sectionTitle: { + fontWeight: '700', + fontSize: fontSizes.md, + color: colors.text.primary, + marginBottom: spacing.md, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + detailLabel: { + width: 80, + }, + detailValue: { + flex: 1, + textAlign: 'right', + }, + tagContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: spacing.sm, + }, + tags: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: spacing.xs, + flex: 1, + }, + tag: { + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + backgroundColor: colors.background.paper, + borderRadius: borderRadius.sm, + borderWidth: 1, + borderColor: colors.primary.main, + }, + actionButtons: { + flexDirection: 'row', + gap: spacing.md, + marginTop: spacing.lg, + }, + previewButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + gap: spacing.xs, + }, + downloadButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + gap: spacing.xs, + }, + actionButtonText: { + fontWeight: '600', + }, + }); +} \ No newline at end of file diff --git a/src/screens/material/MaterialsScreen.tsx b/src/screens/material/MaterialsScreen.tsx new file mode 100644 index 0000000..74c6a27 --- /dev/null +++ b/src/screens/material/MaterialsScreen.tsx @@ -0,0 +1,331 @@ +/** + * 学习资料主页:学科分类网格 + */ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + View, + StyleSheet, + ScrollView, + TouchableOpacity, + StatusBar, + RefreshControl, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRouter } 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 } from '../../types/material'; +import { materialRepository } from '../../data/repositories/MaterialRepository'; + +export const MaterialsScreen: React.FC = () => { + const colors = useAppColors(); + const resolvedScheme = useResolvedColorScheme(); + const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content'; + const styles = useMemo(() => createMaterialsStyles(colors), [colors]); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { isMobile } = useResponsive(); + const isWideScreen = useBreakpointGTE('lg'); + const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); + + const [subjects, setSubjects] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + const scrollBottomInset = isMobile ? 88 + insets.bottom + spacing.md : spacing['3xl']; + + const loadSubjects = useCallback(async () => { + try { + setError(null); + const data = await materialRepository.getSubjects(); + setSubjects(data); + } catch (err) { + setError('加载学科列表失败'); + console.error('Failed to load subjects:', err); + } finally { + setIsLoading(false); + setIsRefreshing(false); + } + }, []); + + useEffect(() => { + loadSubjects(); + }, [loadSubjects]); + + const handleRefresh = useCallback(() => { + setIsRefreshing(true); + loadSubjects(); + }, [loadSubjects]); + + const onOpenSubject = useCallback( + (subjectId: string) => { + router.push(hrefs.hrefMaterialSubject(subjectId)); + }, + [router] + ); + + const cardStyle = useMemo( + () => ({ + backgroundColor: colors.background.paper, + borderRadius: 16, + overflow: 'hidden' as const, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, + }), + [colors] + ); + + const renderSubjectCard = (subject: MaterialSubject) => ( + onOpenSubject(subject.id)} + activeOpacity={0.85} + > + + + + + + {subject.name} + + + {subject.description || `${subject.material_count} 份资料`} + + + + + {subject.material_count} + + + + + ); + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + {error} + + + + 重试 + + + + ); + } + + return ( + <> + + 按学科分类浏览学习资料 + + {subjects.map(renderSubjectCard)} + + ); + }; + + return ( + + + + {/* Header */} + + + router.back()} style={styles.backButton}> + + + + + + 学习资料 + + + + + + + + + + {/* Content */} + {isWideScreen ? ( + + + } + > + {renderContent()} + + + ) : ( + + } + > + {renderContent()} + + )} + + ); +}; + +export default MaterialsScreen; + +function createMaterialsStyles(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, + }, + scroll: { + flex: 1, + backgroundColor: headerBg, + }, + scrollContent: { + paddingTop: spacing.md, + flexGrow: 1, + backgroundColor: headerBg, + }, + pageSubtitle: { + marginBottom: spacing.lg, + lineHeight: fontSizes.sm * 1.45, + }, + subjectsGrid: { + gap: spacing.md, + }, + subjectCard: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.lg, + paddingHorizontal: spacing.lg, + }, + subjectIconCircle: { + width: 56, + height: 56, + borderRadius: borderRadius.full, + alignItems: 'center', + justifyContent: 'center', + }, + subjectContent: { + flex: 1, + marginLeft: spacing.md, + marginRight: spacing.sm, + }, + subjectName: { + fontWeight: '700', + fontSize: fontSizes.md + 1, + color: colors.text.primary, + }, + subjectDesc: { + marginTop: 4, + }, + subjectMeta: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + }, + centerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: spacing['3xl'], + }, + errorText: { + marginTop: spacing.md, + textAlign: 'center', + }, + retryButton: { + marginTop: spacing.md, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.lg, + }, + }); +} diff --git a/src/screens/material/SubjectMaterialsScreen.tsx b/src/screens/material/SubjectMaterialsScreen.tsx new file mode 100644 index 0000000..e0b59fd --- /dev/null +++ b/src/screens/material/SubjectMaterialsScreen.tsx @@ -0,0 +1,404 @@ +/** + * 学科资料列表页:展示某个学科下的所有资料 + */ +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, + }, + }); +} diff --git a/src/screens/material/index.ts b/src/screens/material/index.ts new file mode 100644 index 0000000..ad4c16d --- /dev/null +++ b/src/screens/material/index.ts @@ -0,0 +1,6 @@ +/** + * 学习资料屏幕导出 + */ +export { MaterialsScreen } from './MaterialsScreen'; +export { SubjectMaterialsScreen } from './SubjectMaterialsScreen'; +export { MaterialDetailScreen } from './MaterialDetailScreen'; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 3eb3c6f..3e4fd0c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ // 导出DTO类型作为主要类型 export * from './dto'; export * from './schedule'; +export * from './material'; // 兼容旧类型(用于内部组件) import type { diff --git a/src/types/material.ts b/src/types/material.ts new file mode 100644 index 0000000..6e79da2 --- /dev/null +++ b/src/types/material.ts @@ -0,0 +1,103 @@ +/** + * 学习资料类型定义 + */ + +// 文件类型枚举 +export type MaterialFileType = 'pdf' | 'word' | 'ppt'; + +// 学科分类 +export interface MaterialSubject { + id: string; + name: string; + icon: string; // MaterialCommunityIcons name + color: string; // 主题色 + description?: string; + sort_order: number; + is_active: boolean; + material_count: number; + created_at: string; + updated_at: string; +} + +// 文件资料 +export interface MaterialFile { + id: string; + subject_id: string; + title: string; + description?: string; + file_type: MaterialFileType; + file_size: number; // 字节数 + file_url: string; + file_name: string; + download_count: number; + author_id?: string; + author_name?: string; + tags?: string[]; + status: 'active' | 'inactive'; + created_at: string; + updated_at: string; + subject?: MaterialSubject; +} + +// 学科资料列表响应 +export interface SubjectMaterialsResponse { + subject: MaterialSubject; + materials: MaterialFile[]; + total: number; + page: number; + page_size: number; +} + +// 搜索结果响应 +export interface SearchMaterialsResponse { + materials: MaterialFile[]; + total: number; + page: number; + page_size: number; +} + +// 文件类型图标映射 +export const MATERIAL_FILE_TYPE_ICONS: Record = { + pdf: 'file-pdf-box', + word: 'file-word-box', + ppt: 'file-powerpoint-box', +}; + +// 文件类型颜色映射 +export const MATERIAL_FILE_TYPE_COLORS: Record = { + pdf: '#E53935', + word: '#1E88E5', + ppt: '#FF9800', +}; + +// 文件类型名称映射 +export const MATERIAL_FILE_TYPE_NAMES: Record = { + pdf: 'PDF', + word: 'Word', + ppt: 'PPT', +}; + +// 文件大小格式化 +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +// 学科颜色预设 +export const SUBJECT_COLORS = [ + '#FF6B6B', // 红色 + '#4ECDC4', // 青色 + '#45B7D1', // 蓝色 + '#96CEB4', // 绿色 + '#FFEAA7', // 黄色 + '#DDA0DD', // 紫色 + '#F39C12', // 橙色 + '#3498DB', // 深蓝 + '#27AE60', // 深绿 + '#9B59B6', // 深紫 + '#1ABC9C', // 青绿 + '#E74C3C', // 深红 +]; \ No newline at end of file