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