From eaab9a762a5f8ffcec7a0d8a8d0563d3328288e7 Mon Sep 17 00:00:00 2001 From: ChuXun <70203584+ChuXunYu@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:28:31 +0800 Subject: [PATCH] 1 --- README.md | 373 +++ app.js | 186 ++ app.json | 57 + app.wxss | 286 +++ components/empty/empty.js | 31 + components/empty/empty.json | 4 + components/empty/empty.wxml | 11 + components/empty/empty.wxss | 42 + components/loading/loading.js | 25 + components/loading/loading.json | 4 + components/loading/loading.wxml | 27 + components/loading/loading.wxss | 137 + custom-tab-bar/index.js | 99 + custom-tab-bar/index.json | 3 + custom-tab-bar/index.wxml | 23 + custom-tab-bar/index.wxss | 118 + images/README.md | 71 + pages/ai-assistant/ai-assistant.js | 288 +++ pages/ai-assistant/ai-assistant.json | 7 + pages/ai-assistant/ai-assistant.wxml | 88 + pages/ai-assistant/ai-assistant.wxss | 356 +++ pages/countdown/countdown.js | 159 ++ pages/countdown/countdown.json | 3 + pages/countdown/countdown.wxml | 78 + pages/countdown/countdown.wxss | 163 ++ pages/course-detail/course-detail.js | 82 + pages/course-detail/course-detail.json | 3 + pages/course-detail/course-detail.wxml | 121 + pages/course-detail/course-detail.wxss | 220 ++ pages/courses/courses.js | 204 ++ pages/courses/courses.json | 3 + pages/courses/courses.wxml | 115 + pages/courses/courses.wxss | 482 ++++ pages/dashboard/dashboard.js | 823 ++++++ pages/dashboard/dashboard.json | 7 + pages/dashboard/dashboard.wxml | 123 + pages/dashboard/dashboard.wxss | 279 ++ pages/forum-detail/forum-detail.js | 272 ++ pages/forum-detail/forum-detail.json | 3 + pages/forum-detail/forum-detail.wxml | 111 + pages/forum-detail/forum-detail.wxss | 278 ++ pages/forum/forum.js | 191 ++ pages/forum/forum.json | 3 + pages/forum/forum.wxml | 99 + pages/forum/forum.wxss | 338 +++ pages/gpa/gpa.js | 150 ++ pages/gpa/gpa.json | 3 + pages/gpa/gpa.wxml | 72 + pages/gpa/gpa.wxss | 328 +++ pages/index/index.js | 283 ++ pages/index/index.json | 3 + pages/index/index.wxml | 119 + pages/index/index.wxss | 643 +++++ pages/my/my.js | 726 ++++++ pages/my/my.json | 3 + pages/my/my.wxml | 90 + pages/my/my.wxss | 421 +++ pages/post/post.js | 120 + pages/post/post.json | 3 + pages/post/post.wxml | 82 + pages/post/post.wxss | 187 ++ pages/schedule/schedule.js | 421 +++ pages/schedule/schedule.json | 3 + pages/schedule/schedule.wxml | 123 + pages/schedule/schedule.wxss | 526 ++++ pages/tools/tools.js | 86 + pages/tools/tools.json | 3 + pages/tools/tools.wxml | 27 + pages/tools/tools.wxss | 96 + project-structure.txt | 114 + project.config.json | 59 + project.private.config.json | 24 + sitemap.json | 7 + styles/design-tokens.wxss | 340 +++ styles/premium-animations.wxss | 765 ++++++ styles/premium-components.wxss | 755 ++++++ utils/aiService.js | 185 ++ utils/analytics.js | 350 +++ utils/auth.js | 316 +++ utils/data.js | 158 ++ utils/dataManager.js | 265 ++ utils/gpaPredictor.js | 276 ++ utils/learningTracker.js | 278 ++ utils/logger.js | 310 +++ utils/performance.js | 390 +++ utils/request.js | 312 +++ utils/storage.js | 165 ++ utils/store.js | 128 + utils/userManager.js | 239 ++ utils/util.js | 302 +++ 直接初始化持久化数据.js | 186 ++ 答辩资料/00-答辩资料总览.md | 556 ++++ 答辩资料/01-项目介绍PPT大纲-V2.md | 546 ++++ 答辩资料/02-答辩演讲稿-V2.md | 305 +++ 答辩资料/03-项目功能说明书(非技术版)-V2.md | 466 ++++ 答辩资料/04-答辩Q&A手册-V2.md | 744 ++++++ 答辩资料/05-项目演示脚本-V2.md | 418 +++ 答辩资料/06-完整使用说明手册.md | 786 ++++++ 答辩资料/07-答辩PPT完整内容.md | 2419 ++++++++++++++++++ 答辩资料/答辩资料V2.0更新说明.md | 338 +++ 100 files changed, 23416 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 components/empty/empty.js create mode 100644 components/empty/empty.json create mode 100644 components/empty/empty.wxml create mode 100644 components/empty/empty.wxss create mode 100644 components/loading/loading.js create mode 100644 components/loading/loading.json create mode 100644 components/loading/loading.wxml create mode 100644 components/loading/loading.wxss create mode 100644 custom-tab-bar/index.js create mode 100644 custom-tab-bar/index.json create mode 100644 custom-tab-bar/index.wxml create mode 100644 custom-tab-bar/index.wxss create mode 100644 images/README.md create mode 100644 pages/ai-assistant/ai-assistant.js create mode 100644 pages/ai-assistant/ai-assistant.json create mode 100644 pages/ai-assistant/ai-assistant.wxml create mode 100644 pages/ai-assistant/ai-assistant.wxss create mode 100644 pages/countdown/countdown.js create mode 100644 pages/countdown/countdown.json create mode 100644 pages/countdown/countdown.wxml create mode 100644 pages/countdown/countdown.wxss create mode 100644 pages/course-detail/course-detail.js create mode 100644 pages/course-detail/course-detail.json create mode 100644 pages/course-detail/course-detail.wxml create mode 100644 pages/course-detail/course-detail.wxss create mode 100644 pages/courses/courses.js create mode 100644 pages/courses/courses.json create mode 100644 pages/courses/courses.wxml create mode 100644 pages/courses/courses.wxss create mode 100644 pages/dashboard/dashboard.js create mode 100644 pages/dashboard/dashboard.json create mode 100644 pages/dashboard/dashboard.wxml create mode 100644 pages/dashboard/dashboard.wxss create mode 100644 pages/forum-detail/forum-detail.js create mode 100644 pages/forum-detail/forum-detail.json create mode 100644 pages/forum-detail/forum-detail.wxml create mode 100644 pages/forum-detail/forum-detail.wxss create mode 100644 pages/forum/forum.js create mode 100644 pages/forum/forum.json create mode 100644 pages/forum/forum.wxml create mode 100644 pages/forum/forum.wxss create mode 100644 pages/gpa/gpa.js create mode 100644 pages/gpa/gpa.json create mode 100644 pages/gpa/gpa.wxml create mode 100644 pages/gpa/gpa.wxss create mode 100644 pages/index/index.js create mode 100644 pages/index/index.json create mode 100644 pages/index/index.wxml create mode 100644 pages/index/index.wxss create mode 100644 pages/my/my.js create mode 100644 pages/my/my.json create mode 100644 pages/my/my.wxml create mode 100644 pages/my/my.wxss create mode 100644 pages/post/post.js create mode 100644 pages/post/post.json create mode 100644 pages/post/post.wxml create mode 100644 pages/post/post.wxss create mode 100644 pages/schedule/schedule.js create mode 100644 pages/schedule/schedule.json create mode 100644 pages/schedule/schedule.wxml create mode 100644 pages/schedule/schedule.wxss create mode 100644 pages/tools/tools.js create mode 100644 pages/tools/tools.json create mode 100644 pages/tools/tools.wxml create mode 100644 pages/tools/tools.wxss create mode 100644 project-structure.txt create mode 100644 project.config.json create mode 100644 project.private.config.json create mode 100644 sitemap.json create mode 100644 styles/design-tokens.wxss create mode 100644 styles/premium-animations.wxss create mode 100644 styles/premium-components.wxss create mode 100644 utils/aiService.js create mode 100644 utils/analytics.js create mode 100644 utils/auth.js create mode 100644 utils/data.js create mode 100644 utils/dataManager.js create mode 100644 utils/gpaPredictor.js create mode 100644 utils/learningTracker.js create mode 100644 utils/logger.js create mode 100644 utils/performance.js create mode 100644 utils/request.js create mode 100644 utils/storage.js create mode 100644 utils/store.js create mode 100644 utils/userManager.js create mode 100644 utils/util.js create mode 100644 直接初始化持久化数据.js create mode 100644 答辩资料/00-答辩资料总览.md create mode 100644 答辩资料/01-项目介绍PPT大纲-V2.md create mode 100644 答辩资料/02-答辩演讲稿-V2.md create mode 100644 答辩资料/03-项目功能说明书(非技术版)-V2.md create mode 100644 答辩资料/04-答辩Q&A手册-V2.md create mode 100644 答辩资料/05-项目演示脚本-V2.md create mode 100644 答辩资料/06-完整使用说明手册.md create mode 100644 答辩资料/07-答辩PPT完整内容.md create mode 100644 答辩资料/答辩资料V2.0更新说明.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..caea464 --- /dev/null +++ b/README.md @@ -0,0 +1,373 @@ +# 🎓 知芽小筑 + +一款功能完善的微信小程序,为大学生提供课程管理、学习数据分析、AI助手等核心功能。 + +--- + +## 📱 核心功能模块 + +### 1. 🏠 首页(index) +- 快速入口导航 +- 今日课程预览 +- 重要倒计时提醒 +- 学习数据概览 + +### 2. 📚 课程管理(courses) +- 课程列表展示 +- 分类浏览(全部/进行中/已结束) +- 课程详情查看 +- 课程资料管理 + +### 3. 📅 课程表(schedule) +- 周视图课表 +- 当前时间定位 +- 课程详情快速查看 +- 空闲时间显示 + +### 4. 💬 论坛(forum) +- 帖子浏览 +- 发帖功能 +- 点赞评论 +- 收藏功能 +- 话题分类 + +### 5. 🎯 GPA计算器(gpa) +- 成绩录入 +- 学分管理 +- 加权平均绩点计算 +- 学期分组统计 + +### 6. ⏱️ 倒计时(countdown) +- 重要事件提醒 +- 考试倒计时 +- 自定义事件添加 +- 天数自动计算 + +### 7. 🛠️ 工具箱(tools) +- 实用工具集合 +- 快捷功能入口 + +### 8. 🤖 AI助手(启思AI) +- 智能对话 +- 学习问题解答 +- DeepSeek API集成 +- 对话历史记录 +- 实时流式响应 + +### 9. 📊 学习数据(dashboard) +**4大数据可视化功能**: + +#### 9.1 学习能力画像(雷达图) +- 6维度综合评估 +- 专注度、活跃度、学习时长等 +- Canvas 高质量绘制 + +#### 9.2 GPA趋势预测(折线图) +- 历史成绩趋势展示 +- 多项式回归预测 +- 智能预测下学期GPA +- 趋势分析(上升/下降) + +#### 9.3 时间分配(饼图) +- 各模块使用时长统计 +- 课程、论坛、工具、AI助手 +- 自动计算占比 +- 彩色可视化 + +#### 9.4 成绩对比(柱状图) +- 个人 vs 班级平均 +- 超过平均课程数统计 +- 班级排名展示 + +### 10. 👤 个人中心(my) +- 个人信息展示 +- 功能设置 +- 数据统计 +- 系统设置 + +--- + +## 🎨 技术特色 + +### UI/UX设计 +- **企业级视觉设计**:紫色渐变主题 +- **动画效果**:平滑过渡、加载动画 +- **响应式布局**:适配不同屏幕尺寸 +- **组件化开发**:loading、empty等通用组件 + +### 数据管理 +- **持久化存储**:wx.storage API +- **自动数据追踪**:learningTracker系统 +- **实时数据同步**:12个页面集成 +- **智能数据分析**:GPA预测、趋势分析 + +### 性能优化 +- **懒加载**:图表延迟渲染 +- **数据缓存**:减少重复计算 +- **异步处理**:流式响应 +- **错误处理**:友好的空数据提示 + +--- + +## 📦 技术栈 + +### 前端框架 +- **微信小程序原生框架** +- **Canvas API**:图表绘制 +- **WXSS**:样式设计 + +### 核心工具库 +- `utils/learningTracker.js` - 学习时间自动追踪 +- `utils/gpaPredictor.js` - GPA预测算法 +- `utils/util.js` - 通用工具函数 +- `utils/storage.js` - 数据存储管理 +- `utils/request.js` - 网络请求封装 +- `utils/logger.js` - 日志系统 +- `utils/analytics.js` - 数据分析 +- `utils/performance.js` - 性能监控 + +### 第三方服务 +- **DeepSeek API**:AI对话功能 +- **微信云服务**:数据存储(可选) + +--- + +## 🗂️ 数据存储结构 + +### 核心存储键 + +```javascript +// GPA课程成绩 +'gpaCourses': [ + { id, name, score, credit, semester } +] + +// 学习数据 +'learning_data': { + totalDays: 30, + totalHours: 85.5, + dailyRecords: [] +} + +// 模块使用时长(自动记录) +'module_usage': { + course: 28.5, // 小时 + forum: 22.3, + tools: 25.7, + ai: 9.0 +} + +// 学习画像(自动记录) +'learning_profile': { + focus: 85, // 专注度 + activity: 90, // 活跃度 + duration: 75, // 学习时长 + breadth: 88, // 知识广度 + interaction: 72,// 互动性 + persistence: 95 // 坚持度 +} + +// 课表数据 +'schedule': [ + { id, name, time, location, teacher, weeks, dayOfWeek } +] + +// 倒计时事件 +'countdowns': [ + { id, name, targetDate, description, color } +] + +// 论坛收藏 +'favoriteForumIds': [id1, id2, ...] + +// AI对话历史 +'ai_chat_history': [ + { role: 'user', content: '...', timestamp: ... }, + { role: 'assistant', content: '...', timestamp: ... } +] +``` + +--- + +## 🚀 核心功能实现 + +### 1. 学习时间自动追踪 + +**原理**:每个页面集成 `learningTracker` + +```javascript +// 在 onShow() 中 +learningTracker.onPageShow('pageName'); + +// 在 onHide() 中 +learningTracker.onHide(); +``` + +**已集成页面**(12个): +- ✅ index, courses, course-detail, schedule +- ✅ forum, forum-detail, post, gpa +- ✅ countdown, tools, my, dashboard + +### 2. GPA自动计算与预测 + +**流程**: +1. 用户在 GPA页面 录入课程成绩 +2. 存储到 `gpaCourses` +3. dashboard页面 自动读取 +4. 按学期分组计算平均GPA +5. 使用多项式回归预测下学期 + +**算法**:`utils/gpaPredictor.js` +- 数据清洗 +- 学期分组 +- 加权平均计算 +- 多项式回归 +- 预测与置信度评估 + +### 3. AI对话流式响应 + +**技术实现**: +```javascript +// 流式读取API响应 +const reader = res.data.getReader(); +while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // 解析SSE格式 + // 实时更新界面 + // 显示打字效果 +} +``` + +--- + +## 📊 数据可视化 + +### Canvas图表绘制 + +**支持的图表类型**: +1. **雷达图** - 学习能力6维评估 +2. **折线图** - GPA趋势与预测 +3. **饼图** - 时间分配占比 +4. **柱状图** - 成绩对比分析 + +**技术特点**: +- 高分辨率适配(pixelRatio) +- 响应式尺寸 +- 平滑动画 +- 彩色图例 +- 数据标注 + +--- + +## 🎯 特色亮点 + +### 1. 企业级设计系统 +- 统一的颜色主题(紫色渐变) +- 完整的动画库 +- 组件化样式 +- 响应式布局 + +### 2. 智能数据分析 +- 自动追踪学习时间 +- GPA趋势预测 +- 学习画像生成 +- 多维度数据可视化 + +### 3. AI助手集成 +- DeepSeek大模型 +- 流式对话体验 +- 对话历史管理 +- 智能学习建议 + +### 4. 完善的功能生态 +- 课程管理 +- 论坛交流 +- GPA计算 +- 倒计时提醒 +- 数据分析 +- AI助手 + +--- + +## 📈 代码统计 + +- **总代码量**:约 15,000+ 行 +- **页面数量**:12个主要页面 +- **组件数量**:2个通用组件 +- **工具函数**:9个核心工具文件 +- **样式文件**:6个主题样式 + +--- + +## 🔧 开发与维护 + +### 环境要求 +- 微信开发者工具 +- Node.js(可选,用于工具链) + +### 本地运行 +1. 打开微信开发者工具 +2. 导入项目文件夹 +3. 配置 AppID +4. 点击编译运行 + +### 数据初始化 +- 首次启动自动初始化示例数据 +- 存储键:`demo_data_initialized` +- 可通过控制台手动初始化 + +--- + +## 📝 文档清单 + +- ✅ `学习数据功能说明.md` - 数据可视化详解 +- ✅ `学习活跃度删除记录.md` - 功能删除记录 +- ✅ `如何直接添加持久化存储数据.md` - 数据管理指南 +- ✅ `课表功能优化说明.md` - 课表功能文档 +- ✅ `课表使用指南.md` - 用户使用指南 +- ✅ `个人中心功能测试指南.md` - 测试文档 +- ✅ `论坛收藏功能说明.md` - 论坛功能文档 + +--- + +## 🎓 项目亮点(适合答辩) + +### 技术创新 +1. **自动化数据追踪系统** - 零侵入式学习时间记录 +2. **GPA智能预测算法** - 多项式回归+趋势分析 +3. **AI对话流式响应** - 打字效果+实时交互 +4. **Canvas高质量图表** - 4种图表类型+响应式 + +### 功能完整性 +1. **12个功能页面** - 覆盖学习全流程 +2. **企业级设计** - 统一视觉规范 +3. **数据持久化** - 完整的存储方案 +4. **性能优化** - 懒加载+缓存策略 + +### 用户体验 +1. **直观的数据可视化** - 一目了然的学习状态 +2. **智能的AI助手** - 随时解答学习问题 +3. **便捷的工具集合** - GPA、课表、倒计时 +4. **社交论坛功能** - 学习交流互动 + +--- + +**项目完整、功能丰富、技术先进!** 🎉 + +--- + +## 📄 License + +MIT License + +--- + +## 👨‍💻 开发者 + +GitHub: [ChuXunYu/program](https://github.com/ChuXunYu/program) + +--- + +**最后更新**:2025年10月14日 diff --git a/app.js b/app.js new file mode 100644 index 0000000..27ce7aa --- /dev/null +++ b/app.js @@ -0,0 +1,186 @@ +// app.js +App({ + onLaunch() { + console.log('========================================'); + console.log('🚀 小程序启动'); + console.log('========================================'); + + // 初始化日志 + const logs = wx.getStorageSync('logs') || [] + logs.unshift(Date.now()) + wx.setStorageSync('logs', logs) + + // 静默登录获取code + wx.login({ + success: res => { + console.log('登录成功', res.code) + }, + fail: err => { + console.error('登录失败', err) + } + }) + + // 🔥 强制初始化示例数据(用于演示和答辩) + // 如果不需要示例数据,注释掉下面这行 + this.initDefaultData() + + console.log('========================================'); + }, + + onError(error) { + console.error('小程序错误:', error) + wx.showToast({ + title: '程序异常,请重试', + icon: 'none' + }) + }, + + // 初始化默认数据 + initDefaultData() { + // 检查是否已初始化过示例数据 + const dataInitialized = wx.getStorageSync('demo_data_initialized'); + + if (!dataInitialized) { + console.log('[App] 初始化示例数据...'); + this.initDemoData(); + wx.setStorageSync('demo_data_initialized', true); + } + + // 确保基础数据结构存在 + if (!wx.getStorageSync('userInfo')) { + wx.setStorageSync('userInfo', { + nickname: '同学', + avatar: '/images/avatar-default.png', + isLogin: false + }) + } + if (!wx.getStorageSync('favoriteCourses')) { + wx.setStorageSync('favoriteCourses', []) + } + if (!wx.getStorageSync('favoritePosts')) { + wx.setStorageSync('favoritePosts', []) + } + }, + + // 初始化演示数据 + initDemoData() { + // 1. 初始化GPA课程数据(4个学期,共16门课) + const gpaCourses = [ + // 2023-1学期 + { id: 1, name: '高等数学A(上)', score: 92, credit: 5, semester: '2023-1' }, + { id: 2, name: '大学英语(一)', score: 88, credit: 3, semester: '2023-1' }, + { id: 3, name: '程序设计基础', score: 95, credit: 4, semester: '2023-1' }, + { id: 4, name: '思想道德与法治', score: 85, credit: 3, semester: '2023-1' }, + + // 2023-2学期 + { id: 5, name: '高等数学A(下)', score: 90, credit: 5, semester: '2023-2' }, + { id: 6, name: '大学英语(二)', score: 87, credit: 3, semester: '2023-2' }, + { id: 7, name: '数据结构', score: 93, credit: 4, semester: '2023-2' }, + { id: 8, name: '线性代数', score: 89, credit: 3, semester: '2023-2' }, + + // 2024-1学期 + { id: 9, name: '概率论与数理统计', score: 91, credit: 4, semester: '2024-1' }, + { id: 10, name: '计算机组成原理', score: 88, credit: 4, semester: '2024-1' }, + { id: 11, name: '操作系统', score: 94, credit: 4, semester: '2024-1' }, + { id: 12, name: '大学物理', score: 86, credit: 3, semester: '2024-1' }, + + // 2024-2学期 + { id: 13, name: '数据库系统', score: 95, credit: 4, semester: '2024-2' }, + { id: 14, name: '计算机网络', score: 92, credit: 4, semester: '2024-2' }, + { id: 15, name: '软件工程', score: 90, credit: 3, semester: '2024-2' }, + { id: 16, name: '人工智能导论', score: 96, credit: 3, semester: '2024-2' } + ]; + wx.setStorageSync('gpaCourses', gpaCourses); + console.log('[App] 初始化16门GPA课程数据'); + + // 2. 初始化学习时长数据(最近7天) + const today = new Date(); + const learningData = { + totalDays: 7, + totalHours: 24.5, + dailyRecords: [] + }; + + const dailyActivity = {}; + const moduleUsage = { + course: 8.5, // 课程中心使用8.5小时 + forum: 6.2, // 论坛使用6.2小时 + tools: 7.3, // 工具使用7.3小时 + ai: 2.5 // AI助手使用2.5小时 + }; + + // 生成最近7天的学习记录 + for (let i = 6; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const dateStr = this.formatDate(date); + + // 每天1-5小时随机学习时长 + const duration = 1 + Math.random() * 4; + learningData.dailyRecords.push({ + date: dateStr, + duration: parseFloat(duration.toFixed(1)) + }); + + // 每日活跃度(60-180分钟) + dailyActivity[dateStr] = Math.floor(60 + Math.random() * 120); + } + + wx.setStorageSync('learning_data', learningData); + wx.setStorageSync('daily_activity', dailyActivity); + wx.setStorageSync('module_usage', moduleUsage); + console.log('[App] 初始化学习时长数据(7天)'); + + // 3. 初始化学习画像 + const learningProfile = { + focus: 85, // 专注度 + activity: 90, // 活跃度 + duration: 75, // 学习时长 + breadth: 88, // 知识广度 + interaction: 72, // 互动性 + persistence: 95 // 坚持度 + }; + wx.setStorageSync('learning_profile', learningProfile); + console.log('[App] 初始化学习画像数据'); + + // 4. 初始化课表数据 + const scheduleData = { + 1: { // 周次 + '周一-1-2节': { name: '高等数学', location: '教学楼A101', teacher: '张教授', weeks: '1-16周' }, + '周一-3-4节': { name: '大学英语', location: '教学楼B203', teacher: '李老师', weeks: '1-16周' }, + '周二-1-2节': { name: '数据结构', location: '实验楼C301', teacher: '王教授', weeks: '1-16周' }, + '周二-5-6节': { name: '计算机网络', location: '实验楼C302', teacher: '赵老师', weeks: '1-16周' }, + '周三-3-4节': { name: '数据库系统', location: '教学楼A205', teacher: '刘教授', weeks: '1-16周' }, + '周四-1-2节': { name: '操作系统', location: '实验楼C303', teacher: '陈老师', weeks: '1-16周' }, + '周四-5-6节': { name: '软件工程', location: '教学楼B301', teacher: '杨教授', weeks: '1-16周' }, + '周五-3-4节': { name: '人工智能导论', location: '教学楼A301', teacher: '周教授', weeks: '1-16周' } + } + }; + wx.setStorageSync('schedule', scheduleData); + console.log('[App] 初始化课表数据'); + + // 5. 初始化倒计时数据 + const countdowns = [ + { id: 1, name: '期末考试', date: '2025-01-15', color: '#FF6B6B' }, + { id: 2, name: '英语四级', date: '2024-12-14', color: '#4ECDC4' }, + { id: 3, name: '数学竞赛', date: '2024-11-20', color: '#95E1D3' }, + { id: 4, name: '编程大赛', date: '2024-11-30', color: '#F38181' } + ]; + wx.setStorageSync('countdowns', countdowns); + console.log('[App] 初始化倒计时数据'); + + console.log('[App] ✅ 所有示例数据初始化完成'); + }, + + // 格式化日期为 YYYY-MM-DD + formatDate(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }, + + globalData: { + version: '1.0.0' + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..7ee2719 --- /dev/null +++ b/app.json @@ -0,0 +1,57 @@ +{ + "pages": [ + "pages/index/index", + "pages/courses/courses", + "pages/course-detail/course-detail", + "pages/forum/forum", + "pages/forum-detail/forum-detail", + "pages/post/post", + "pages/tools/tools", + "pages/gpa/gpa", + "pages/schedule/schedule", + "pages/countdown/countdown", + "pages/my/my", + "pages/ai-assistant/ai-assistant", + "pages/dashboard/dashboard" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#4A90E2", + "navigationBarTitleText": "知芽小筑", + "navigationBarTextStyle": "white", + "backgroundColor": "#F5F5F5", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 + }, + "tabBar": { + "custom": true, + "color": "#7A7E83", + "selectedColor": "#4A90E2", + "borderStyle": "white", + "backgroundColor": "#ffffff", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页" + }, + { + "pagePath": "pages/courses/courses", + "text": "课程" + }, + { + "pagePath": "pages/forum/forum", + "text": "论坛" + }, + { + "pagePath": "pages/tools/tools", + "text": "工具" + }, + { + "pagePath": "pages/my/my", + "text": "我的" + } + ] + }, + "style": "v2", + "sitemapLocation": "sitemap.json" +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..0080a4c --- /dev/null +++ b/app.wxss @@ -0,0 +1,286 @@ +/**app.wxss**/ +/* ==================== 导入企业级设计系统 ==================== */ +@import './styles/design-tokens.wxss'; +@import './styles/premium-components.wxss'; +@import './styles/premium-animations.wxss'; + +/* ==================== 全局基础样式 ==================== */ +page { + background-color: var(--bg-page); + font-family: var(--font-family-base); + color: var(--text-primary); + font-size: var(--font-size-base); + line-height: var(--line-height-normal); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + min-height: 100vh; + padding-bottom: calc(var(--safe-area-inset-bottom) + 120rpx); +} + +/* ==================== 工具类 ==================== */ + +/* 文本对齐 */ +.text-left { text-align: left; } +.text-center { text-align: center; } +.text-right { text-align: right; } + +/* 文本粗细 */ +.font-thin { font-weight: var(--font-weight-thin); } +.font-light { font-weight: var(--font-weight-light); } +.font-normal { font-weight: var(--font-weight-regular); } +.font-medium { font-weight: var(--font-weight-medium); } +.font-semibold { font-weight: var(--font-weight-semibold); } +.font-bold { font-weight: var(--font-weight-bold); } + +/* 文本颜色 */ +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-tertiary { color: var(--text-tertiary); } +.text-brand { color: var(--brand-primary); } +.text-success { color: var(--color-success); } +.text-warning { color: var(--color-warning); } +.text-error { color: var(--color-error); } + +/* 布局工具 */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-around { justify-content: space-around; } +.justify-start { justify-content: flex-start; } +.justify-end { justify-content: flex-end; } +.flex-1 { flex: 1; } +.flex-wrap { flex-wrap: wrap; } + +/* 间距工具 */ +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-6 { gap: var(--space-6); } +.gap-8 { gap: var(--space-8); } + +/* 内边距 */ +.p-0 { padding: 0; } +.p-2 { padding: var(--space-2); } +.p-4 { padding: var(--space-4); } +.p-6 { padding: var(--space-6); } +.p-8 { padding: var(--space-8); } + +/* 外边距 */ +.m-0 { margin: 0; } +.m-2 { margin: var(--space-2); } +.m-4 { margin: var(--space-4); } +.m-6 { margin: var(--space-6); } +.m-8 { margin: var(--space-8); } + +/* 圆角 */ +.rounded-sm { border-radius: var(--radius-sm); } +.rounded { border-radius: var(--radius-base); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-2xl { border-radius: var(--radius-2xl); } +.rounded-full { border-radius: var(--radius-full); } + +/* 阴影 */ +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow { box-shadow: var(--shadow-base); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-xl { box-shadow: var(--shadow-xl); } + +/* 宽高 */ +.w-full { width: 100%; } +.h-full { height: 100%; } + +/* 隐藏 */ +.hidden { display: none; } +.invisible { visibility: hidden; } + +/* 溢出 */ +.overflow-hidden { overflow: hidden; } +.overflow-scroll { overflow: scroll; } +.overflow-auto { overflow: auto; } + +/* 位置 */ +.relative { position: relative; } +.absolute { position: absolute; } +.fixed { position: fixed; } + +/* 点击效果 */ +.clickable { + cursor: pointer; + user-select: none; + -webkit-tap-highlight-color: transparent; + transition: var(--transition-fast); +} + +.clickable:active { + opacity: 0.7; + transform: scale(0.98); +} + +/* ==================== 页面通用样式 ==================== */ + +/* 页面容器 */ +.page-container { + min-height: 100vh; + background: var(--bg-page); +} + +/* 内容区域 */ +.page-content { + padding: var(--space-6); + padding-bottom: calc(var(--safe-area-inset-bottom) + 150rpx); +} + +/* 固定顶部导航 */ +.fixed-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: var(--z-index-sticky); + background: var(--glass-white); + backdrop-filter: blur(var(--blur-lg)); + border-bottom: 1rpx solid var(--border-light); +} + +/* 悬浮按钮 */ +.fab { + position: fixed; + bottom: calc(var(--safe-area-inset-bottom) + 150rpx); + right: var(--space-8); + width: 120rpx; + height: 120rpx; + display: flex; + align-items: center; + justify-content: center; + background: var(--gradient-purple-dream); + border-radius: var(--radius-full); + box-shadow: var(--shadow-primary), var(--shadow-xl); + color: var(--text-inverse); + font-size: 48rpx; + z-index: var(--z-index-fixed); + transition: var(--transition-base); +} + +.fab:active { + transform: scale(0.9); + box-shadow: var(--shadow-md); +} + +/* ==================== 滚动优化 ==================== */ + +.smooth-scroll { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* 隐藏滚动条 */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +.hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* ==================== 性能优化 ==================== */ + +/* GPU加速 */ +.gpu-accelerated { + transform: translateZ(0); + will-change: transform; +} + +/* 避免重绘 */ +.avoid-repaint { + will-change: opacity, transform; +} + +/* ==================== 图片优化 ==================== */ + +image { + display: block; + width: 100%; + height: auto; +} + +.image-cover { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-contain { + width: 100%; + height: 100%; + object-fit: contain; +} + +.image-rounded { + border-radius: var(--radius-xl); + overflow: hidden; +} + +/* ==================== 文本溢出处理 ==================== */ + +.ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.line-clamp-1 { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.line-clamp-2 { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.line-clamp-3 { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +/* ==================== 安全区域适配 ==================== */ + +.safe-area-top { + padding-top: var(--safe-area-inset-top); +} + +.safe-area-bottom { + padding-bottom: var(--safe-area-inset-bottom); +} + +.safe-area-right { + padding-right: var(--safe-area-inset-right); +} + +/* ==================== 保持向后兼容的别名 ==================== */ +.gradient-primary { background: var(--gradient-purple-dream); } +.gradient-secondary { background: var(--gradient-sunset-pink); } +.gradient-success { background: var(--gradient-ocean-blue); } +.gradient-warning { background: var(--gradient-sunset-pink); } + diff --git a/components/empty/empty.js b/components/empty/empty.js new file mode 100644 index 0000000..8665af5 --- /dev/null +++ b/components/empty/empty.js @@ -0,0 +1,31 @@ +// components/empty/empty.js +Component({ + properties: { + show: { + type: Boolean, + value: false + }, + icon: { + type: String, + value: '📭' + }, + text: { + type: String, + value: '暂无数据' + }, + desc: { + type: String, + value: '' + }, + buttonText: { + type: String, + value: '' + } + }, + + methods: { + onButtonClick() { + this.triggerEvent('buttonclick') + } + } +}) diff --git a/components/empty/empty.json b/components/empty/empty.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/empty/empty.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/empty/empty.wxml b/components/empty/empty.wxml new file mode 100644 index 0000000..366a996 --- /dev/null +++ b/components/empty/empty.wxml @@ -0,0 +1,11 @@ + + + + {{icon}} + + {{text}} + {{desc}} + + diff --git a/components/empty/empty.wxss b/components/empty/empty.wxss new file mode 100644 index 0000000..a21b8b3 --- /dev/null +++ b/components/empty/empty.wxss @@ -0,0 +1,42 @@ +/* components/empty/empty.wxss */ +.empty-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 60rpx; + min-height: 400rpx; +} + +.empty-image { + margin-bottom: 30rpx; +} + +.empty-icon { + font-size: 120rpx; + opacity: 0.3; +} + +.empty-text { + font-size: 32rpx; + color: #666666; + margin-bottom: 15rpx; + font-weight: 500; +} + +.empty-desc { + font-size: 26rpx; + color: #999999; + text-align: center; + line-height: 1.6; + margin-bottom: 30rpx; +} + +.empty-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + border: none; + padding: 20rpx 60rpx; + border-radius: 50rpx; + font-size: 28rpx; +} diff --git a/components/loading/loading.js b/components/loading/loading.js new file mode 100644 index 0000000..b509899 --- /dev/null +++ b/components/loading/loading.js @@ -0,0 +1,25 @@ +// components/loading/loading.js +Component({ + properties: { + show: { + type: Boolean, + value: false + }, + type: { + type: String, + value: 'spinner' // skeleton, spinner, progress + }, + text: { + type: String, + value: '加载中...' + }, + mask: { + type: Boolean, + value: true + }, + progress: { + type: Number, + value: 0 + } + } +}) diff --git a/components/loading/loading.json b/components/loading/loading.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/loading/loading.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/loading/loading.wxml b/components/loading/loading.wxml new file mode 100644 index 0000000..4c4a52d --- /dev/null +++ b/components/loading/loading.wxml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + {{text}} + + + + + + + + {{progress}}% + + + diff --git a/components/loading/loading.wxss b/components/loading/loading.wxss new file mode 100644 index 0000000..afe346f --- /dev/null +++ b/components/loading/loading.wxss @@ -0,0 +1,137 @@ +/* components/loading/loading.wxss */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +.loading-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); +} + +.loading-content { + position: relative; + z-index: 10000; +} + +/* 骨架屏 */ +.skeleton { + background-color: #ffffff; + border-radius: 12rpx; + padding: 30rpx; + width: 600rpx; +} + +.skeleton-avatar { + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; +} + +.skeleton-lines { + margin-top: 20rpx; +} + +.skeleton-line { + height: 30rpx; + margin-bottom: 15rpx; + border-radius: 4rpx; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; +} + +.skeleton-line:nth-child(1) { + width: 100%; +} + +.skeleton-line:nth-child(2) { + width: 80%; +} + +.skeleton-line:nth-child(3) { + width: 60%; +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* 加载动画 */ +.loading-spinner { + background-color: rgba(0, 0, 0, 0.7); + border-radius: 12rpx; + padding: 40rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid rgba(255, 255, 255, 0.3); + border-top-color: #ffffff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-text { + margin-top: 20rpx; + color: #ffffff; + font-size: 28rpx; +} + +/* 进度条 */ +.loading-progress { + background-color: #ffffff; + border-radius: 12rpx; + padding: 40rpx; + width: 500rpx; +} + +.progress-bar { + height: 8rpx; + background-color: #E0E0E0; + border-radius: 4rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(to right, #667eea, #764ba2); + transition: width 0.3s; +} + +.progress-text { + display: block; + text-align: center; + margin-top: 20rpx; + color: #666666; + font-size: 28rpx; +} diff --git a/custom-tab-bar/index.js b/custom-tab-bar/index.js new file mode 100644 index 0000000..2c59425 --- /dev/null +++ b/custom-tab-bar/index.js @@ -0,0 +1,99 @@ +Component({ + data: { + selected: 0, + color: "#7A7E83", + selectedColor: "#4A90E2", + list: [ + { + emoji: "🏠", + text: "首页", + pagePath: "/pages/index/index" + }, + { + emoji: "📚", + text: "课程", + pagePath: "/pages/courses/courses" + }, + { + emoji: "💬", + text: "论坛", + pagePath: "/pages/forum/forum" + }, + { + emoji: "🔧", + text: "工具", + pagePath: "/pages/tools/tools" + }, + { + emoji: "👤", + text: "我的", + pagePath: "/pages/my/my" + } + ] + }, + + attached() { + try { + // 获取当前页面路径 + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + const url = currentPage ? `/${currentPage.route}` : ''; + + // 匹配当前选中的tab + this.data.list.forEach((item, index) => { + if (item.pagePath === url) { + this.setData({ + selected: index + }); + } + }); + } catch (e) { + console.error('[TabBar] attached错误:', e); + } + }, + + methods: { + switchTab(e) { + try { + const data = e.currentTarget.dataset; + const url = data.path; + const index = data.index; + + if (!url) { + console.error('[TabBar] 页面路径为空'); + wx.showToast({ + title: '页面路径错误', + icon: 'none' + }); + return; + } + + // 先更新选中状态 + this.setData({ + selected: index + }); + + // 切换tab + wx.switchTab({ + url, + success: () => { + console.log('[TabBar] 切换成功:', url); + }, + fail: (err) => { + console.error('[TabBar] 切换失败:', err); + wx.showToast({ + title: '页面切换失败', + icon: 'none' + }); + } + }); + } catch (e) { + console.error('[TabBar] switchTab错误:', e); + wx.showToast({ + title: '程序异常,请重试', + icon: 'none' + }); + } + } + } +}); diff --git a/custom-tab-bar/index.json b/custom-tab-bar/index.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/custom-tab-bar/index.wxml b/custom-tab-bar/index.wxml new file mode 100644 index 0000000..ee9bdcc --- /dev/null +++ b/custom-tab-bar/index.wxml @@ -0,0 +1,23 @@ + + + + + {{item.emoji}} + + + + + {{item.text}} + + + + + + diff --git a/custom-tab-bar/index.wxss b/custom-tab-bar/index.wxss new file mode 100644 index 0000000..0c09758 --- /dev/null +++ b/custom-tab-bar/index.wxss @@ -0,0 +1,118 @@ +.tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 100rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + padding-bottom: env(safe-area-inset-bottom); + box-shadow: 0 -4rpx 20rpx rgba(102, 126, 234, 0.3); + z-index: 9999; +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.tab-item.active { + transform: translateY(-10rpx); +} + +/* Emoji图标 */ +.tab-emoji { + font-size: 44rpx; + line-height: 1; + margin-bottom: 4rpx; + transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + filter: grayscale(100%) brightness(1.2) opacity(0.5); +} + +.tab-item.active .tab-emoji { + filter: grayscale(0%) brightness(1.3) opacity(1) drop-shadow(0 0 12rpx rgba(255, 255, 255, 0.8)); + transform: scale(1.25); +} + +/* 弹跳动画 */ +.emoji-bounce { + animation: emoji-bounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +@keyframes emoji-bounce { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.35); + } +} + +/* 文字标签 */ +.tab-text { + font-size: 20rpx; + line-height: 1; + font-weight: 500; + transition: all 0.3s ease; + opacity: 0.7; + color: rgba(255, 255, 255, 0.7); +} + +.tab-item.active .tab-text { + opacity: 1; + font-weight: 700; + color: #FFFFFF; + text-shadow: 0 0 15rpx rgba(255, 255, 255, 0.8), + 0 2rpx 8rpx rgba(0, 0, 0, 0.3); + transform: scale(1.05); +} + +/* 选中指示器 */ +.tab-indicator { + position: absolute; + bottom: 0; + width: 50rpx; + height: 8rpx; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.6)); + border-radius: 4rpx 4rpx 0 0; + box-shadow: 0 0 20rpx rgba(255, 255, 255, 0.8), + 0 -2rpx 10rpx rgba(255, 255, 255, 0.5); + animation: indicator-slide 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes indicator-slide { + from { + width: 0; + opacity: 0; + transform: scaleX(0); + } + to { + width: 50rpx; + opacity: 1; + transform: scaleX(1); + } +} + +/* 点击波纹效果 */ +.tab-item::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.tab-item:active::before { + width: 100rpx; + height: 100rpx; +} diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..59a4082 --- /dev/null +++ b/images/README.md @@ -0,0 +1,71 @@ +# 图片资源说明 + +## images/avatar.png + +AI助手页面需要使用用户头像图片。请按以下步骤添加: + +### 方式1: 使用默认头像 +1. 在 `images/` 目录下添加一个默认头像图片,命名为 `avatar.png` +2. 建议尺寸: 200x200 像素 +3. 格式: PNG (支持透明背景) + +### 方式2: 使用用户微信头像 +修改 `pages/ai-assistant/ai-assistant.wxml` 第24行: + +```xml + + + + + +``` + +然后在 `pages/ai-assistant/ai-assistant.js` 的 `onLoad` 中获取用户信息: + +```javascript +onLoad(options) { + // 获取用户信息 + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo) { + this.setData({ userInfo }); + } + + // 加载场景列表 + const scenarios = aiService.getScenarios(); + this.setData({ scenarios }); + + // ... 其他代码 +} +``` + +### 临时方案 +如果暂时没有图片,可以注释掉图片显示: + +```xml + +👤 +``` + +并添加对应样式: + +```css +.user-avatar { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; +} +``` + +## 其他图片资源 + +目前项目中 `images/` 目录为空,如需添加其他图片资源: +- TabBar图标 +- 空状态插图 +- 引导页图片 +- 启动页背景 + +请将图片文件放入 `images/` 目录,并按功能分类命名。 diff --git a/pages/ai-assistant/ai-assistant.js b/pages/ai-assistant/ai-assistant.js new file mode 100644 index 0000000..535f7c1 --- /dev/null +++ b/pages/ai-assistant/ai-assistant.js @@ -0,0 +1,288 @@ +// pages/ai-assistant/ai-assistant.js +const aiService = require('../../utils/aiService.js') +const learningTracker = require('../../utils/learningTracker.js') +const util = require('../../utils/util'); + +Page({ + data: { + // 场景列表 + scenarios: [], + currentScenario: null, + + // 消息列表 + messages: [], + + // 输入框 + inputValue: '', + inputPlaceholder: '输入你的问题...', + + // 状态 + isThinking: false, + scrollIntoView: '', + + // 打字机效果 + typewriterTimer: null, + currentTypingMessage: '' + }, + + onLoad(options) { + // 加载场景列表 + const scenarios = aiService.getScenarios(); + this.setData({ scenarios }); + + // 从存储加载历史对话 + this.loadHistory(); + + // 如果从其他页面传入场景 + if (options.scenario) { + this.selectScenario({ currentTarget: { dataset: { id: options.scenario } } }); + } + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('ai') + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + + // 清理打字机定时器 + if (this.data.typewriterTimer) { + clearTimeout(this.data.typewriterTimer); + } + + // 保存对话历史 + this.saveHistory(); + }, + + /** + * 选择场景 + */ + selectScenario(e) { + const scenarioId = e.currentTarget.dataset.id; + const scenario = this.data.scenarios.find(s => s.id === scenarioId); + + if (!scenario) return; + + // 触觉反馈 + wx.vibrateShort({ type: 'light' }); + + // 设置当前场景 + this.setData({ + currentScenario: scenarioId, + inputPlaceholder: `${scenario.name} - 输入你的问题...` + }); + + // 如果有预设提示词,自动发送 + if (scenario.prompt && this.data.messages.length === 0) { + this.setData({ inputValue: scenario.prompt }); + // 延迟发送,让用户看到输入 + setTimeout(() => { + this.sendMessage(); + }, 500); + } + }, + + /** + * 输入框变化 + */ + onInput(e) { + this.setData({ + inputValue: e.detail.value + }); + }, + + /** + * 发送消息 + */ + async sendMessage() { + const content = this.data.inputValue.trim(); + + if (!content || this.data.isThinking) return; + + // 触觉反馈 + wx.vibrateShort({ type: 'medium' }); + + // 添加用户消息 + const userMessage = { + role: 'user', + content: content, + time: util.formatTime(new Date(), 'hh:mm') + }; + + const messages = [...this.data.messages, userMessage]; + + this.setData({ + messages, + inputValue: '', + isThinking: true + }); + + // 滚动到底部 + this.scrollToBottom(); + + try { + // 调用AI服务 + const reply = await aiService.chat( + messages.map(m => ({ role: m.role, content: m.content })), + this.data.currentScenario + ); + + // 添加AI回复(带打字机效果) + const assistantMessage = { + role: 'assistant', + content: '', // 初始为空,打字机逐步显示 + time: util.formatTime(new Date(), 'hh:mm') + }; + + this.setData({ + messages: [...this.data.messages, assistantMessage], + isThinking: false + }); + + // 启动打字机效果 + this.typewriterEffect(reply, this.data.messages.length - 1); + + } catch (error) { + // 错误处理 + const errorMessage = { + role: 'assistant', + content: aiService.getErrorMessage(error), + time: util.formatTime(new Date(), 'hh:mm') + }; + + this.setData({ + messages: [...this.data.messages, errorMessage], + isThinking: false + }); + + this.scrollToBottom(); + + wx.showToast({ + title: '发送失败', + icon: 'error' + }); + } + }, + + /** + * 打字机效果 + */ + typewriterEffect(text, messageIndex, currentIndex = 0) { + if (currentIndex >= text.length) { + // 打字完成,保存历史 + this.saveHistory(); + return; + } + + // 每次显示2-3个字符(中文)或5-8个字符(英文) + const charsToAdd = text[currentIndex].match(/[\u4e00-\u9fa5]/) ? + Math.min(2, text.length - currentIndex) : + Math.min(6, text.length - currentIndex); + + const newText = text.substring(0, currentIndex + charsToAdd); + + // 更新消息内容 + const messages = this.data.messages; + messages[messageIndex].content = newText; + + this.setData({ messages }); + this.scrollToBottom(); + + // 继续打字 + const delay = text[currentIndex].match(/[,。!?;:,.]/) ? 150 : 50; + this.data.typewriterTimer = setTimeout(() => { + this.typewriterEffect(text, messageIndex, currentIndex + charsToAdd); + }, delay); + }, + + /** + * 清空对话 + */ + clearMessages() { + wx.showModal({ + title: '确认清空', + content: '确定要清空所有对话记录吗?', + confirmColor: '#FF3B30', + success: (res) => { + if (res.confirm) { + this.setData({ + messages: [], + currentScenario: null, + inputPlaceholder: '输入你的问题...' + }); + + // 清除存储 + wx.removeStorageSync('ai_chat_history'); + + wx.showToast({ + title: '已清空', + icon: 'success' + }); + } + } + }); + }, + + /** + * 滚动到底部 + */ + scrollToBottom() { + const query = wx.createSelectorQuery(); + query.select('.message-list').boundingClientRect(); + query.selectViewport().scrollOffset(); + + setTimeout(() => { + this.setData({ + scrollIntoView: `msg-${this.data.messages.length - 1}` + }); + }, 100); + }, + + /** + * 保存对话历史 + */ + saveHistory() { + try { + wx.setStorageSync('ai_chat_history', { + messages: this.data.messages, + scenario: this.data.currentScenario, + timestamp: Date.now() + }); + } catch (error) { + console.error('保存历史失败:', error); + } + }, + + /** + * 加载对话历史 + */ + loadHistory() { + try { + const history = wx.getStorageSync('ai_chat_history'); + + if (history && history.messages) { + // 只加载24小时内的历史 + const hoursPassed = (Date.now() - history.timestamp) / (1000 * 60 * 60); + + if (hoursPassed < 24) { + this.setData({ + messages: history.messages, + currentScenario: history.scenario + }); + + setTimeout(() => this.scrollToBottom(), 300); + } + } + } catch (error) { + console.error('加载历史失败:', error); + } + } +}); diff --git a/pages/ai-assistant/ai-assistant.json b/pages/ai-assistant/ai-assistant.json new file mode 100644 index 0000000..3323171 --- /dev/null +++ b/pages/ai-assistant/ai-assistant.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "启思AI", + "navigationBarBackgroundColor": "#6C5CE7", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": false, + "backgroundColor": "#F5F6FA" +} diff --git a/pages/ai-assistant/ai-assistant.wxml b/pages/ai-assistant/ai-assistant.wxml new file mode 100644 index 0000000..b12daeb --- /dev/null +++ b/pages/ai-assistant/ai-assistant.wxml @@ -0,0 +1,88 @@ + + + + + + + {{item.icon}} + {{item.name}} + + + + + + + 🤖 + 启思AI + 启迪思维·智慧学习·与你同行 + + 💡 智能解答学习疑问 + 📚 个性化学习建议 + 🎯 高效学习计划 + + + + + + + + 👤 + 🤖 + + + {{item.content}} + {{item.time}} + + + + + + + 🤖 + + + + + + + + + + + + + + + + {{inputValue.length}}/500 + + + + + + + 🗑️ 清空对话 + + diff --git a/pages/ai-assistant/ai-assistant.wxss b/pages/ai-assistant/ai-assistant.wxss new file mode 100644 index 0000000..1f459ef --- /dev/null +++ b/pages/ai-assistant/ai-assistant.wxss @@ -0,0 +1,356 @@ +/* pages/ai-assistant/ai-assistant.wxss */ +@import "/styles/design-tokens.wxss"; +@import "/styles/premium-animations.wxss"; + +.ai-container { + width: 100%; + height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + flex-direction: column; + position: relative; +} + +/* 场景选择栏 */ +.scenario-bar { + padding: 20rpx 0; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20rpx); + border-bottom: 1rpx solid rgba(0, 0, 0, 0.05); +} + +.scenario-scroll { + white-space: nowrap; + padding: 0 20rpx; +} + +.scenario-item { + display: inline-block; + padding: 20rpx 30rpx; + margin-right: 20rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20rpx; + text-align: center; + transition: all 0.3s ease; + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} + +.scenario-item:active { + transform: scale(0.95); + box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.2); +} + +.scenario-icon { + font-size: 48rpx; + margin-bottom: 8rpx; +} + +.scenario-name { + font-size: 24rpx; + color: #FFFFFF; + font-weight: 500; +} + +/* 欢迎界面 */ +.welcome-section { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60rpx 40rpx; + animation: fadeInUp 0.6s ease; +} + +.welcome-icon { + font-size: 120rpx; + margin-bottom: 30rpx; + animation: float 3s ease-in-out infinite; +} + +.welcome-title { + font-size: 48rpx; + font-weight: bold; + color: #FFFFFF; + margin-bottom: 20rpx; + text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.welcome-subtitle { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 60rpx; + text-align: center; +} + +.welcome-tips { + width: 100%; +} + +.tip-item { + padding: 24rpx; + margin-bottom: 20rpx; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(20rpx); + border-radius: 16rpx; + color: #FFFFFF; + font-size: 28rpx; + border: 1rpx solid rgba(255, 255, 255, 0.3); +} + +/* 消息列表 */ +.message-list { + flex: 1; + padding: 30rpx; + overflow-y: auto; +} + +.message-item { + display: flex; + margin-bottom: 30rpx; + animation: messageSlideIn 0.3s ease; +} + +.message-item.user { + flex-direction: row-reverse; +} + +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + margin: 0 20rpx; +} + +.avatar image { + width: 100%; + height: 100%; +} + +.user-avatar { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + box-shadow: 0 4rpx 12rpx rgba(74, 144, 226, 0.3); +} + +.ai-avatar { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} + +.message-content { + max-width: 500rpx; + animation: fadeIn 0.3s ease; +} + +.message-text { + padding: 24rpx 28rpx; + border-radius: 20rpx; + font-size: 28rpx; + line-height: 1.6; + word-wrap: break-word; + white-space: pre-wrap; +} + +.message-item.user .message-text { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #FFFFFF; + border-bottom-right-radius: 4rpx; + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} + +.message-item.assistant .message-text { + background: rgba(255, 255, 255, 0.95); + color: #2D3436; + border-bottom-left-radius: 4rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.message-time { + font-size: 22rpx; + color: rgba(255, 255, 255, 0.7); + margin-top: 8rpx; + text-align: right; +} + +.message-item.assistant .message-time { + text-align: left; +} + +/* AI思考中 */ +.thinking-dots { + display: flex; + align-items: center; + padding: 24rpx 28rpx; + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + border-bottom-left-radius: 4rpx; +} + +.dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + background: #667eea; + margin: 0 6rpx; + animation: thinking 1.4s infinite; +} + +.dot:nth-child(2) { + animation-delay: 0.2s; +} + +.dot:nth-child(3) { + animation-delay: 0.4s; +} + +/* 输入区域 */ +.input-bar { + display: flex; + align-items: flex-end; + padding: 20rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20rpx); + border-top: 1rpx solid rgba(0, 0, 0, 0.05); + safe-area-inset-bottom: constant(safe-area-inset-bottom); + safe-area-inset-bottom: env(safe-area-inset-bottom); +} + +.input-wrapper { + flex: 1; + position: relative; + margin-right: 20rpx; +} + +.message-input { + width: 100%; + min-height: 80rpx; + max-height: 200rpx; + padding: 20rpx 24rpx; + background: #F5F6FA; + border-radius: 16rpx; + font-size: 28rpx; + line-height: 1.5; + box-sizing: border-box; +} + +.char-count { + position: absolute; + right: 24rpx; + bottom: 8rpx; + font-size: 20rpx; + color: #999999; +} + +.send-btn { + width: 120rpx; + height: 80rpx; + background: #CCCCCC; + color: #FFFFFF; + border-radius: 16rpx; + font-size: 28rpx; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + margin: 0; + padding: 0; + border: none; +} + +.send-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4); +} + +.send-btn:active { + transform: scale(0.95); +} + +.send-btn::after { + border: none; +} + +/* 清空按钮 */ +.clear-btn { + position: absolute; + top: 20rpx; + right: 20rpx; + padding: 16rpx 24rpx; + background: rgba(255, 59, 48, 0.9); + color: #FFFFFF; + border-radius: 30rpx; + font-size: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3); + z-index: 100; +} + +.clear-btn:active { + transform: scale(0.95); +} + +/* 动画 */ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes thinking { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 0.7; + } + 30% { + transform: translateY(-10rpx); + opacity: 1; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-20rpx); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/pages/countdown/countdown.js b/pages/countdown/countdown.js new file mode 100644 index 0000000..94b27cc --- /dev/null +++ b/pages/countdown/countdown.js @@ -0,0 +1,159 @@ +// pages/countdown/countdown.js +const { getCountdown, showSuccess, showError } = require('../../utils/util.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + countdowns: [], + newEventName: '', + newEventDate: '' + }, + + onLoad() { + this.loadCountdowns() + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('tools') + + this.startTimer() + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + + this.stopTimer() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + + this.stopTimer() + }, + + // 加载倒计时 + loadCountdowns() { + let countdowns = wx.getStorageSync('countdowns') || [] + + // 默认示例数据 + if (countdowns.length === 0) { + countdowns = [ + { + id: 1, + name: '高等数学期末考试', + date: '2025-12-20', + color: '#FF6B6B' + }, + { + id: 2, + name: '英语四级考试', + date: '2025-12-15', + color: '#4A90E2' + }, + { + id: 3, + name: '课程设计答辩', + date: '2025-12-25', + color: '#50C878' + } + ] + } + + this.setData({ countdowns }) + this.updateAllCountdowns() + }, + + // 开始定时器 + startTimer() { + this.timer = setInterval(() => { + this.updateAllCountdowns() + }, 1000) + }, + + // 停止定时器 + stopTimer() { + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + }, + + // 更新所有倒计时 + updateAllCountdowns() { + const { countdowns } = this.data + const updated = countdowns.map(item => ({ + ...item, + ...getCountdown(item.date) + })) + this.setData({ countdowns: updated }) + }, + + // 事件名称输入 + onNameInput(e) { + this.setData({ newEventName: e.detail.value }) + }, + + // 日期选择 + onDateChange(e) { + this.setData({ newEventDate: e.detail.value }) + }, + + // 添加倒计时 + onAddCountdown() { + const { newEventName, newEventDate, countdowns } = this.data + + if (!newEventName.trim()) { + showError('请输入事件名称') + return + } + + if (!newEventDate) { + showError('请选择日期') + return + } + + const colors = ['#FF6B6B', '#4A90E2', '#50C878', '#F39C12', '#9B59B6'] + + const newCountdown = { + id: Date.now(), + name: newEventName.trim(), + date: newEventDate, + color: colors[Math.floor(Math.random() * colors.length)] + } + + countdowns.push(newCountdown) + wx.setStorageSync('countdowns', countdowns) + + this.setData({ + countdowns, + newEventName: '', + newEventDate: '' + }) + + this.updateAllCountdowns() + showSuccess('添加成功') + }, + + // 删除倒计时 + onDelete(e) { + const { id } = e.currentTarget.dataset + + wx.showModal({ + title: '确认删除', + content: '确定要删除这个倒计时吗?', + success: (res) => { + if (res.confirm) { + let { countdowns } = this.data + countdowns = countdowns.filter(item => item.id !== id) + wx.setStorageSync('countdowns', countdowns) + + this.setData({ countdowns }) + showSuccess('删除成功') + } + } + }) + } +}) diff --git a/pages/countdown/countdown.json b/pages/countdown/countdown.json new file mode 100644 index 0000000..4d5253c --- /dev/null +++ b/pages/countdown/countdown.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "考试倒计时" +} diff --git a/pages/countdown/countdown.wxml b/pages/countdown/countdown.wxml new file mode 100644 index 0000000..58dd502 --- /dev/null +++ b/pages/countdown/countdown.wxml @@ -0,0 +1,78 @@ + + + + + 添加新倒计时 + + + + + {{newEventDate || '选择日期'}} + + + + + + + + + + + {{item.name}} + + × + + + + 📅 {{item.date}} + + + + {{item.days}} + + + + {{item.hours}} + + + + {{item.minutes}} + + + + {{item.seconds}} + + + + + + + 已到期 + + + + + + + 暂无倒计时,快来添加一个吧 + + + diff --git a/pages/countdown/countdown.wxss b/pages/countdown/countdown.wxss new file mode 100644 index 0000000..9d4d56b --- /dev/null +++ b/pages/countdown/countdown.wxss @@ -0,0 +1,163 @@ +/* pages/countdown/countdown.wxss */ +.container { + min-height: 100vh; + background-color: #F5F5F5; + padding: 30rpx; +} + +/* 添加区域 */ +.add-section { + background-color: #ffffff; + border-radius: 16rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); +} + +.add-title { + font-size: 32rpx; + font-weight: bold; + color: #333333; + margin-bottom: 25rpx; +} + +.add-form { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.form-input, .date-picker { + width: 100%; + height: 80rpx; + line-height: 80rpx; + padding: 0 25rpx; + background-color: #F5F5F5; + border-radius: 12rpx; + font-size: 28rpx; + box-sizing: border-box; +} + +.date-picker { + color: #333333; +} + +.add-btn { + height: 80rpx; + line-height: 80rpx; + background-color: #F39C12; + color: #ffffff; + border-radius: 40rpx; + font-size: 30rpx; + font-weight: bold; +} + +/* 倒计时列表 */ +.countdown-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.countdown-card { + background-color: #ffffff; + border-radius: 16rpx; + padding: 30rpx; + border-left: 8rpx solid #4A90E2; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15rpx; +} + +.event-name { + font-size: 32rpx; + font-weight: bold; + color: #333333; + flex: 1; +} + +.delete-btn { + width: 50rpx; + height: 50rpx; + line-height: 50rpx; + text-align: center; + font-size: 48rpx; + color: #CCCCCC; + font-weight: 300; +} + +.event-date { + font-size: 26rpx; + color: #999999; + margin-bottom: 25rpx; +} + +.countdown-display { + display: flex; + justify-content: space-between; + gap: 15rpx; +} + +.time-block { + flex: 1; + text-align: center; +} + +.time-value { + height: 90rpx; + line-height: 90rpx; + background-color: #4A90E2; + color: #ffffff; + border-radius: 12rpx; + font-size: 36rpx; + font-weight: bold; + margin-bottom: 10rpx; +} + +.time-label { + font-size: 22rpx; + color: #999999; +} + +.expired-tip { + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + padding: 30rpx; + background-color: #FFF0E6; + border-radius: 12rpx; +} + +.expired-icon { + font-size: 40rpx; +} + +.expired-text { + font-size: 28rpx; + color: #F39C12; + font-weight: bold; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 100rpx; + margin-bottom: 20rpx; +} + +.empty-text { + font-size: 28rpx; + color: #999999; +} diff --git a/pages/course-detail/course-detail.js b/pages/course-detail/course-detail.js new file mode 100644 index 0000000..074f564 --- /dev/null +++ b/pages/course-detail/course-detail.js @@ -0,0 +1,82 @@ +// pages/course-detail/course-detail.js +const { coursesData } = require('../../utils/data.js') +const { showSuccess, showError } = require('../../utils/util.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + course: null, + isFavorite: false, + activeTab: 0, + tabs: ['课程信息', '教学大纲', '课程评价'] + }, + + onLoad(options) { + const { id } = options + this.loadCourseDetail(id) + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('course') + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 加载课程详情 + loadCourseDetail(id) { + const course = coursesData.find(c => c.id === parseInt(id)) + if (course) { + const favorites = wx.getStorageSync('favoriteCourses') || [] + this.setData({ + course, + isFavorite: favorites.includes(course.id) + }) + } else { + showError('课程不存在') + setTimeout(() => { + wx.navigateBack() + }, 1500) + } + }, + + // 切换标签 + onTabChange(e) { + const { index } = e.currentTarget.dataset + this.setData({ activeTab: index }) + }, + + // 收藏/取消收藏 + onToggleFavorite() { + const { course, isFavorite } = this.data + let favorites = wx.getStorageSync('favoriteCourses') || [] + + if (isFavorite) { + favorites = favorites.filter(id => id !== course.id) + showSuccess('取消收藏') + } else { + favorites.push(course.id) + showSuccess('收藏成功') + } + + wx.setStorageSync('favoriteCourses', favorites) + this.setData({ isFavorite: !isFavorite }) + }, + + // 分享课程 + onShareAppMessage() { + const { course } = this.data + return { + title: `推荐课程:${course.name}`, + path: `/pages/course-detail/course-detail?id=${course.id}` + } + } +}) diff --git a/pages/course-detail/course-detail.json b/pages/course-detail/course-detail.json new file mode 100644 index 0000000..0a287c7 --- /dev/null +++ b/pages/course-detail/course-detail.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "课程详情" +} diff --git a/pages/course-detail/course-detail.wxml b/pages/course-detail/course-detail.wxml new file mode 100644 index 0000000..950c035 --- /dev/null +++ b/pages/course-detail/course-detail.wxml @@ -0,0 +1,121 @@ + + + + + {{course.name}} + + {{course.category}} + {{course.credit}}学分 + + + + + + + 👨‍🏫 + + 授课教师 + {{course.teacher}} + + + + + 🏫 + + 开课院系 + {{course.department}} + + + + + 📍 + + 上课地点 + {{course.location}} + + + + + + + 上课时间 + {{course.time}} + + + + + 👥 + + 选课人数 + {{course.enrolled}}/{{course.capacity}} + + + + + + + + {{item}} + + + + + + + + + 课程简介 + {{course.description}} + + + + + + + 教学内容 + + + {{index + 1}} + {{item}} + + + + + + + + + 💬 + 暂无评价,快来发表第一条评价吧 + + + + + + + + + + diff --git a/pages/course-detail/course-detail.wxss b/pages/course-detail/course-detail.wxss new file mode 100644 index 0000000..e59e964 --- /dev/null +++ b/pages/course-detail/course-detail.wxss @@ -0,0 +1,220 @@ +/* pages/course-detail/course-detail.wxss */ +.container { + padding-bottom: 150rpx; +} + +/* 课程头部 */ +.course-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 60rpx 30rpx 40rpx; + color: #ffffff; +} + +.course-name { + font-size: 40rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.course-meta { + display: flex; + gap: 15rpx; +} + +.meta-tag { + padding: 8rpx 20rpx; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 20rpx; + font-size: 24rpx; +} + +/* 信息卡片 */ +.info-card { + background-color: #ffffff; + margin: 20rpx 30rpx; + border-radius: 16rpx; + padding: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08); +} + +.info-row { + display: flex; + align-items: center; + padding: 20rpx 0; + border-bottom: 1rpx solid #F5F5F5; +} + +.info-row:last-child { + border-bottom: none; +} + +.info-icon { + font-size: 36rpx; + margin-right: 20rpx; +} + +.info-content { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.info-label { + font-size: 28rpx; + color: #999999; +} + +.info-value { + font-size: 28rpx; + color: #333333; + font-weight: 500; +} + +/* 标签栏 */ +.tab-bar { + display: flex; + background-color: #ffffff; + margin: 0 30rpx; + border-radius: 12rpx; + overflow: hidden; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 25rpx 0; + font-size: 28rpx; + color: #666666; + transition: all 0.3s; +} + +.tab-item.active { + color: #4A90E2; + font-weight: bold; + background-color: #E8F4FF; +} + +/* 内容区域 */ +.content-area { + margin: 20rpx 30rpx; +} + +.tab-content { + background-color: #ffffff; + border-radius: 16rpx; + padding: 30rpx; +} + +.section { + margin-bottom: 30rpx; +} + +.section:last-child { + margin-bottom: 0; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #333333; + margin-bottom: 20rpx; + padding-left: 20rpx; + border-left: 6rpx solid #4A90E2; +} + +.section-content { + font-size: 28rpx; + color: #666666; + line-height: 1.8; + text-align: justify; +} + +/* 教学大纲 */ +.syllabus-list { + padding-left: 10rpx; +} + +.syllabus-item { + display: flex; + align-items: center; + padding: 20rpx; + margin-bottom: 15rpx; + background-color: #F8F9FA; + border-radius: 12rpx; +} + +.syllabus-number { + width: 50rpx; + height: 50rpx; + line-height: 50rpx; + text-align: center; + background-color: #4A90E2; + color: #ffffff; + border-radius: 25rpx; + font-size: 24rpx; + margin-right: 20rpx; +} + +.syllabus-text { + flex: 1; + font-size: 28rpx; + color: #333333; +} + +/* 空状态 */ +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + padding: 80rpx 0; +} + +.empty-icon { + font-size: 100rpx; + margin-bottom: 20rpx; +} + +.empty-text { + font-size: 26rpx; + color: #999999; +} + +/* 底部操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + gap: 20rpx; + padding: 20rpx 30rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background-color: #ffffff; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08); +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + height: 90rpx; + border-radius: 45rpx; + font-size: 28rpx; + gap: 10rpx; +} + +.favorite-btn { + background-color: #FFE8E8; + color: #FF6B6B; +} + +.share-btn { + background-color: #E8F4FF; + color: #4A90E2; +} + +.btn-icon { + font-size: 32rpx; +} diff --git a/pages/courses/courses.js b/pages/courses/courses.js new file mode 100644 index 0000000..76d0aaf --- /dev/null +++ b/pages/courses/courses.js @@ -0,0 +1,204 @@ +// pages/courses/courses.js +const { coursesData } = require('../../utils/data.js') +const { showSuccess, showError } = require('../../utils/util.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + allCourses: [], + displayCourses: [], + searchKeyword: '', + selectedCategory: 'all', + categories: [ + { id: 'all', name: '全部' }, + { id: '必修', name: '必修' }, + { id: '专业必修', name: '专业必修' }, + { id: '选修', name: '选修' }, + { id: '专业选修', name: '专业选修' } + ], + showFilter: false, + selectedDepartment: '全部', // 修改为'全部'而不是'all' + departments: [ + '全部', + '数学系', + '物理系', + '计算机学院', + '外国语学院' + ] + }, + + onLoad() { + try { + this.loadCourses() + } catch (error) { + console.error('课程加载失败:', error) + showError('课程数据加载失败') + } + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('course') + + // 更新自定义TabBar选中状态 + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ + selected: 1 + }) + } + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 加载课程数据 + loadCourses() { + const courses = coursesData.map(course => ({ + ...course, + isFavorite: this.checkFavorite(course.id) + })) + + this.setData({ + allCourses: courses, + displayCourses: courses + }) + }, + + // 检查是否已收藏 + checkFavorite(courseId) { + const favorites = wx.getStorageSync('favoriteCourses') || [] + return favorites.includes(courseId) + }, + + // 搜索课程(防抖处理) + onSearchInput(e) { + const keyword = e.detail.value + this.setData({ searchKeyword: keyword }) + + // 清除之前的延迟搜索 + if (this.searchTimer) { + clearTimeout(this.searchTimer) + } + + // 设置300ms防抖 + this.searchTimer = setTimeout(() => { + this.filterCourses() + }, 300) + }, + + // 分类筛选 + onCategoryChange(e) { + const category = e.currentTarget.dataset.category + this.setData({ selectedCategory: category }) + this.filterCourses() + }, + + // 院系筛选 + onDepartmentChange(e) { + const index = e.detail.value + this.setData({ + selectedDepartment: this.data.departments[index], + showFilter: false + }) + this.filterCourses() + }, + + // 筛选课程 + filterCourses() { + const { allCourses, searchKeyword, selectedCategory, selectedDepartment } = this.data + + console.log('筛选条件:', { + searchKeyword, + selectedCategory, + selectedDepartment, + allCoursesCount: allCourses.length + }) + + let filtered = allCourses.filter(course => { + // 搜索关键词过滤(不区分大小写,去除空格) + const keyword = (searchKeyword || '').trim().toLowerCase() + const matchKeyword = !keyword || + (course.name && course.name.toLowerCase().includes(keyword)) || + (course.teacher && course.teacher.toLowerCase().includes(keyword)) || + (course.code && course.code.toLowerCase().includes(keyword)) + + // 分类过滤 + const matchCategory = selectedCategory === 'all' || + course.category === selectedCategory + + // 院系过滤 + const matchDepartment = selectedDepartment === '全部' || + course.department === selectedDepartment + + return matchKeyword && matchCategory && matchDepartment + }) + + console.log('筛选结果:', filtered.length, '门课程') + this.setData({ displayCourses: filtered }) + }, + + // 显示/隐藏筛选面板 + toggleFilter() { + this.setData({ showFilter: !this.data.showFilter }) + }, + + // 收藏课程 + onFavorite(e) { + const { id } = e.currentTarget.dataset + const { allCourses } = this.data + + // 更新课程收藏状态 + const updatedCourses = allCourses.map(course => { + if (course.id === id) { + course.isFavorite = !course.isFavorite + + // 保存到本地存储 + let favorites = wx.getStorageSync('favoriteCourses') || [] + if (course.isFavorite) { + favorites.push(id) + showSuccess('收藏成功') + } else { + favorites = favorites.filter(fid => fid !== id) + showSuccess('取消收藏') + } + wx.setStorageSync('favoriteCourses', favorites) + } + return course + }) + + this.setData({ allCourses: updatedCourses }) + this.filterCourses() + }, + + // 查看课程详情 + onCourseDetail(e) { + const { id } = e.currentTarget.dataset + wx.navigateTo({ + url: `/pages/course-detail/course-detail?id=${id}` + }) + }, + + // 清空筛选 + onResetFilter() { + this.setData({ + searchKeyword: '', + selectedCategory: 'all', + selectedDepartment: '全部', + showFilter: false + }) + this.filterCourses() + }, + + // 清空搜索 + onClearSearch() { + this.setData({ searchKeyword: '' }) + this.filterCourses() + } +}) diff --git a/pages/courses/courses.json b/pages/courses/courses.json new file mode 100644 index 0000000..77e94a3 --- /dev/null +++ b/pages/courses/courses.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "课程筛选" +} diff --git a/pages/courses/courses.wxml b/pages/courses/courses.wxml new file mode 100644 index 0000000..cb1dec3 --- /dev/null +++ b/pages/courses/courses.wxml @@ -0,0 +1,115 @@ + + + + + + + 🔍 + + + + 📋 + 筛选 + + + + + + + + {{item.name}} + + + + + + + + + 院系: + + + {{selectedDepartment}} ▼ + + + + + + + + + + + + + + 共找到 {{displayCourses.length}} 门课程 + + + + + {{item.name}} + + {{item.isFavorite ? '❤️' : '🤍'}} + + + + + + 👨‍🏫 教师: + {{item.teacher}} + + + 📍 地点: + {{item.location}} + + + ⏰ 时间: + {{item.time}} + + + + + + {{item.category}} + {{item.credit}}学分 + + + {{item.enrolled}}/{{item.capacity}} + + + + + + + 📭 + 暂无符合条件的课程 + + + diff --git a/pages/courses/courses.wxss b/pages/courses/courses.wxss new file mode 100644 index 0000000..4c1c1ad --- /dev/null +++ b/pages/courses/courses.wxss @@ -0,0 +1,482 @@ +/* pages/courses/courses.wxss */ +.container { + background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%); + min-height: 100vh; + /* 底部留出TabBar的空间 */ + padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx); +} + +/* 搜索栏 */ +.search-bar { + display: flex; + padding: 20rpx 30rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + gap: 20rpx; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.15); +} + +.search-input-wrap { + flex: 1; + position: relative; + animation: slideInLeft 0.5s ease-out; +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20rpx); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.search-input { + width: 100%; + height: 70rpx; + background-color: rgba(255, 255, 255, 0.95); + border-radius: 35rpx; + padding: 0 50rpx 0 30rpx; + font-size: 28rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.search-input:focus { + background-color: #ffffff; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12); +} + +.search-icon { + position: absolute; + right: 30rpx; + top: 50%; + transform: translateY(-50%); + font-size: 32rpx; + color: #999999; + transition: color 0.3s ease; +} + +.clear-icon { + position: absolute; + right: 30rpx; + top: 50%; + transform: translateY(-50%); + font-size: 32rpx; + color: #999999; + width: 40rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #E0E0E0 0%, #BDBDBD 100%); + border-radius: 50%; + font-size: 24rpx; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.clear-icon:active { + transform: translateY(-50%) scale(0.9); + box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.15); +} + +.filter-btn { + display: flex; + align-items: center; + gap: 10rpx; + padding: 0 30rpx; + background: rgba(255, 255, 255, 0.25); + color: #ffffff; + border-radius: 35rpx; + font-size: 28rpx; + backdrop-filter: blur(10rpx); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + animation: slideInRight 0.5s ease-out; +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20rpx); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.filter-btn:active { + background: rgba(255, 255, 255, 0.35); + transform: scale(0.95); +} + +.filter-icon { + font-size: 32rpx; +} + +/* 分类标签 */ +.category-scroll { + white-space: nowrap; + background-color: #ffffff; + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); +} + +.category-list { + display: inline-flex; + padding: 0 30rpx; + gap: 20rpx; +} + +.category-item { + display: inline-block; + padding: 14rpx 32rpx; + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + color: #666666; + border-radius: 30rpx; + font-size: 26rpx; + font-weight: 500; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06); + position: relative; + overflow: hidden; +} + +.category-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(102, 126, 234, 0.1) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.category-item:active::before { + opacity: 1; +} + +.category-item.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); + transform: translateY(-2rpx); +} + +.category-item.active::before { + opacity: 0; +} + +/* 筛选面板 */ +.filter-panel { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + pointer-events: none; + transition: all 0.3s ease; +} + +.filter-panel.show { + pointer-events: auto; +} + +.filter-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + transition: opacity 0.3s ease; + backdrop-filter: blur(4rpx); +} + +.filter-panel.show::before { + opacity: 1; +} + +.filter-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background-color: #ffffff; + border-radius: 30rpx 30rpx 0 0; + padding: 40rpx 30rpx; + transform: translateY(100%); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.filter-panel.show .filter-content { + transform: translateY(0); +} + +/* 课程列表 */ +.course-list { + padding: 20rpx 30rpx; +} + +.result-count { + font-size: 26rpx; + color: #999999; + margin-bottom: 20rpx; + animation: fadeIn 0.5s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.course-card { + background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + animation: slideInUp 0.5s ease-out both; +} + +.course-card:nth-child(1) { animation-delay: 0.05s; } +.course-card:nth-child(2) { animation-delay: 0.1s; } +.course-card:nth-child(3) { animation-delay: 0.15s; } +.course-card:nth-child(4) { animation-delay: 0.2s; } +.course-card:nth-child(5) { animation-delay: 0.25s; } + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.course-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 6rpx; + height: 100%; + background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.course-card:active { + transform: scale(0.98); + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.15); +} + +.course-card:active::before { + opacity: 1; +} + +/* 筛选面板 */ +.filter-panel { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 200; + display: none; +} + +.filter-panel.show { + display: block; +} + +.filter-content { + position: absolute; + top: 200rpx; + left: 30rpx; + right: 30rpx; + background-color: #ffffff; + border-radius: 16rpx; + padding: 40rpx; +} + +.filter-item { + display: flex; + align-items: center; + margin-bottom: 30rpx; +} + +.filter-label { + font-size: 28rpx; + color: #333333; + margin-right: 20rpx; +} + +.picker-value { + flex: 1; + padding: 20rpx; + background-color: #F5F5F5; + border-radius: 8rpx; + font-size: 28rpx; +} + +.filter-actions { + display: flex; + gap: 20rpx; + margin-top: 40rpx; +} + +.reset-btn, .confirm-btn { + flex: 1; + height: 80rpx; + line-height: 80rpx; + border-radius: 40rpx; + font-size: 28rpx; +} + +.reset-btn { + background-color: #F5F5F5; + color: #666666; +} + +.confirm-btn { + background-color: #4A90E2; + color: #ffffff; +} + +/* 课程列表 */ +.course-list { + padding: 20rpx 30rpx; +} + +.result-count { + font-size: 26rpx; + color: #999999; + margin-bottom: 20rpx; +} + +.count-number { + color: #4A90E2; + font-weight: bold; +} + +.course-card { + background-color: #ffffff; + border-radius: 16rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); +} + +.course-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20rpx; +} + +.course-title { + flex: 1; + font-size: 32rpx; + font-weight: bold; + color: #333333; +} + +.favorite-btn { + padding: 10rpx; +} + +.favorite-icon { + font-size: 36rpx; +} + +.course-info { + margin-bottom: 20rpx; +} + +.info-row { + display: flex; + margin-bottom: 12rpx; + font-size: 26rpx; +} + +.info-label { + color: #999999; + width: 150rpx; +} + +.info-value { + flex: 1; + color: #666666; +} + +.course-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 20rpx; + border-top: 1rpx solid #EEEEEE; +} + +.course-tags { + display: flex; + gap: 10rpx; +} + +.tag { + padding: 8rpx 16rpx; + border-radius: 6rpx; + font-size: 22rpx; +} + +.category-tag { + background-color: #E8F4FF; + color: #4A90E2; +} + +.credit-tag { + background-color: #FFF0E6; + color: #FF8C42; +} + +.enrollment-info { + font-size: 24rpx; + color: #999999; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 100rpx; + margin-bottom: 20rpx; +} + +.empty-text { + font-size: 28rpx; + color: #999999; +} diff --git a/pages/dashboard/dashboard.js b/pages/dashboard/dashboard.js new file mode 100644 index 0000000..b834894 --- /dev/null +++ b/pages/dashboard/dashboard.js @@ -0,0 +1,823 @@ +// pages/dashboard/dashboard.js +const gpaPredictor = require('../../utils/gpaPredictor'); +const util = require('../../utils/util'); +const learningTracker = require('../../utils/learningTracker'); + +Page({ + data: { + // 设备信息 + windowWidth: 375, + windowHeight: 667, + pixelRatio: 2, + + // 顶部统计 + stats: { + totalDays: 0, + totalHours: 0, + avgGPA: 0 + }, + + // 雷达图数据 + radarData: { + indicators: [ + { name: '专注度', max: 100 }, + { name: '活跃度', max: 100 }, + { name: '学习时长', max: 100 }, + { name: '知识广度', max: 100 }, + { name: '互动性', max: 100 }, + { name: '坚持度', max: 100 } + ], + values: [85, 90, 75, 88, 70, 95] + }, + + // GPA预测数据 + gpaHistory: [], + prediction: { + nextSemester: 0, + trend: 0 + }, + + // 饼图数据 + pieData: [], + + // 柱状图数据 + barData: { + aboveAvg: 0, + ranking: 0 + }, + + updateTime: '' + }, + + onLoad() { + // 清理可能存在的旧模拟数据 + this.cleanOldData(); + + // 获取设备信息 + const systemInfo = wx.getSystemInfoSync(); + this.setData({ + windowWidth: systemInfo.windowWidth, + windowHeight: systemInfo.windowHeight, + pixelRatio: systemInfo.pixelRatio + }); + + this.loadAllData(); + }, + + /** + * 清理旧的模拟数据 + */ + cleanOldData() { + // 检查是否需要清理(可通过版本号判断) + const dataVersion = wx.getStorageSync('data_version'); + if (dataVersion !== '2.0') { + console.log('[Dashboard] 清理旧数据...'); + + // 只清理GPA历史记录,让其从真实课程重新生成 + wx.removeStorageSync('gpa_history'); + + // 标记数据版本 + wx.setStorageSync('data_version', '2.0'); + + console.log('[Dashboard] 数据清理完成'); + } + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('tools') + + // 刷新数据 + this.loadAllData(); + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + onPullDownRefresh() { + this.loadAllData(); + setTimeout(() => { + wx.stopPullDownRefresh(); + }, 1000); + }, + + /** + * 加载所有数据 + */ + loadAllData() { + this.loadStats(); + this.loadRadarData(); + this.loadGPAData(); + this.loadPieData(); + this.loadBarData(); + + this.setData({ + updateTime: util.formatTime(new Date(), 'yyyy-MM-dd hh:mm') + }); + + // 延迟绘制图表,确保DOM已渲染 + setTimeout(() => { + this.drawRadarChart(); + this.drawLineChart(); + this.drawPieChart(); + this.drawBarChart(); + }, 300); + }, + + /** + * 加载统计数据 + */ + loadStats() { + // 从学习追踪器获取真实数据 + const stats = learningTracker.getStats(); + + // 加载GPA数据 + const gpaCourses = wx.getStorageSync('gpaCourses') || []; + const avgGPA = gpaPredictor.calculateAverageGPA(gpaCourses); + + this.setData({ + stats: { + totalDays: stats.continuousDays, + totalHours: stats.totalHours, + avgGPA: avgGPA || 0 + } + }); + }, + + /** + * 计算连续学习天数 + */ + calculateContinuousDays(records) { + if (!records || records.length === 0) return 0; + + // 按日期排序 + const sortedRecords = records.sort((a, b) => new Date(b.date) - new Date(a.date)); + + let continuousDays = 0; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + for (let i = 0; i < sortedRecords.length; i++) { + const recordDate = new Date(sortedRecords[i].date); + recordDate.setHours(0, 0, 0, 0); + + const daysDiff = Math.floor((today - recordDate) / (1000 * 60 * 60 * 24)); + + if (daysDiff === i) { + continuousDays++; + } else { + break; + } + } + + return continuousDays; + }, + + /** + * 加载雷达图数据 + */ + loadRadarData() { + // 从存储加载真实计算的学习画像 + const learningProfile = wx.getStorageSync('learning_profile') || { + focus: 0, // 专注度 + activity: 0, // 活跃度 + duration: 0, // 学习时长 + breadth: 0, // 知识广度 + interaction: 0, // 互动性 + persistence: 0 // 坚持度 + }; + + this.setData({ + 'radarData.values': [ + Math.min(100, Math.round(learningProfile.focus)), + Math.min(100, Math.round(learningProfile.activity)), + Math.min(100, Math.round(learningProfile.duration)), + Math.min(100, Math.round(learningProfile.breadth)), + Math.min(100, Math.round(learningProfile.interaction)), + Math.min(100, Math.round(learningProfile.persistence)) + ] + }); + }, + + /** + * 加载GPA数据和预测 + */ + loadGPAData() { + // 从GPA计算器获取真实数据 + const gpaCourses = wx.getStorageSync('gpaCourses') || []; + + console.log('[Dashboard] 读取到的课程数据:', gpaCourses); + + // 强制从课程数据重新生成历史记录(不使用缓存) + let gpaHistory = []; + + if (gpaCourses.length > 0) { + // 按学期分组计算平均GPA + const semesterMap = {}; + gpaCourses.forEach(course => { + const semester = course.semester || '2024-2'; + if (!semesterMap[semester]) { + semesterMap[semester] = { courses: [] }; + } + semesterMap[semester].courses.push(course); + }); + + console.log('[Dashboard] 学期分组:', semesterMap); + + // 计算每个学期的加权平均GPA + Object.keys(semesterMap).sort().forEach(semester => { + const data = semesterMap[semester]; + const avgGPA = gpaPredictor.calculateAverageGPA(data.courses); + gpaHistory.push({ + semester: semester, + gpa: parseFloat(avgGPA) + }); + }); + + // 保存到存储 + if (gpaHistory.length > 0) { + wx.setStorageSync('gpa_history', gpaHistory); + console.log('[Dashboard] GPA历史记录:', gpaHistory); + } + } + + // 设置数据(即使为空也设置,避免undefined) + this.setData({ + gpaHistory: gpaHistory.length > 0 ? gpaHistory : [] + }); + + // 只有在有足够数据时才进行预测(至少2个学期) + if (gpaHistory.length >= 2) { + const predictionResult = gpaPredictor.polynomialRegression(gpaHistory, 2, 3); + + console.log('[Dashboard] GPA预测结果:', predictionResult); + + this.setData({ + prediction: { + nextSemester: predictionResult.predictions[0]?.gpa || 0, + trend: predictionResult.trend, + confidence: predictionResult.confidence + } + }); + } else if (gpaHistory.length === 1) { + // 只有一个学期,无法预测,显示当前GPA + console.log('[Dashboard] 只有一个学期数据,无法预测'); + this.setData({ + prediction: { + nextSemester: gpaHistory[0].gpa, + trend: 0, + confidence: 0 + } + }); + } else { + // 没有数据 + console.log('[Dashboard] 没有GPA数据'); + this.setData({ + prediction: { + nextSemester: 0, + trend: 0, + confidence: 0 + } + }); + } + }, + + /** + * 加载饼图数据 + */ + loadPieData() { + // 从学习追踪器获取真实的模块使用数据 + const moduleUsage = wx.getStorageSync('module_usage') || { + course: 0, + forum: 0, + tools: 0, + ai: 0 + }; + + const total = Object.values(moduleUsage).reduce((sum, val) => sum + val, 0); + + // 如果没有数据,显示提示 + if (total === 0) { + this.setData({ + pieData: [ + { name: '课程中心', time: 0, percent: 25, color: '#4A90E2' }, + { name: '学科论坛', time: 0, percent: 25, color: '#50C878' }, + { name: '学习工具', time: 0, percent: 25, color: '#9B59B6' }, + { name: '启思AI', time: 0, percent: 25, color: '#6C5CE7' } + ] + }); + return; + } + + const pieData = [ + { + name: '课程中心', + time: parseFloat(moduleUsage.course.toFixed(1)), + percent: Math.round((moduleUsage.course / total) * 100), + color: '#4A90E2' + }, + { + name: '学科论坛', + time: parseFloat(moduleUsage.forum.toFixed(1)), + percent: Math.round((moduleUsage.forum / total) * 100), + color: '#50C878' + }, + { + name: '学习工具', + time: parseFloat(moduleUsage.tools.toFixed(1)), + percent: Math.round((moduleUsage.tools / total) * 100), + color: '#9B59B6' + }, + { + name: '启思AI', + time: parseFloat(moduleUsage.ai.toFixed(1)), + percent: Math.round((moduleUsage.ai / total) * 100), + color: '#6C5CE7' + } + ]; + + this.setData({ pieData }); + }, + + /** + * 加载柱状图数据 + */ + loadBarData() { + const gpaCourses = wx.getStorageSync('gpaCourses') || []; + + // 计算超过平均分的课程数 + let aboveAvg = 0; + gpaCourses.forEach(course => { + if (course.score > 75) { // 假设75为平均分 + aboveAvg++; + } + }); + + // 模拟排名(基于平均GPA) + const avgGPA = parseFloat(this.data.stats.avgGPA); + let ranking = 30; // 默认前30% + if (avgGPA >= 3.5) ranking = 10; + else if (avgGPA >= 3.0) ranking = 20; + + this.setData({ + barData: { + aboveAvg, + ranking + } + }); + }, + + /** + * 绘制雷达图 + */ + drawRadarChart() { + const query = wx.createSelectorQuery(); + query.select('#radarChart').boundingClientRect(); + query.exec((res) => { + if (!res[0]) return; + + const canvasWidth = res[0].width; + const canvasHeight = res[0].height; + const ctx = wx.createCanvasContext('radarChart', this); + const centerX = canvasWidth / 2; + const centerY = canvasHeight / 2; + const radius = Math.min(canvasWidth, canvasHeight) / 2 - 60; + const data = this.data.radarData.values; + const indicators = this.data.radarData.indicators; + const angleStep = (Math.PI * 2) / indicators.length; + + // 清空画布 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + // 绘制背景网格 + ctx.setLineWidth(1); + ctx.setStrokeStyle('#E0E0E0'); + + for (let level = 1; level <= 5; level++) { + ctx.beginPath(); + const r = (radius / 5) * level; + for (let i = 0; i <= indicators.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + ctx.closePath(); + ctx.stroke(); + } + + // 绘制轴线和标签 + ctx.setFillStyle('#333333'); + ctx.setFontSize(24); + ctx.setTextAlign('center'); + + for (let i = 0; i < indicators.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.lineTo(x, y); + ctx.stroke(); + + // 绘制维度标签 + const labelDistance = radius + 30; + const labelX = centerX + labelDistance * Math.cos(angle); + const labelY = centerY + labelDistance * Math.sin(angle); + + ctx.fillText(indicators[i].name, labelX, labelY); + } + + // 绘制数据区域 + ctx.beginPath(); + ctx.setFillStyle('rgba(102, 126, 234, 0.3)'); + ctx.setStrokeStyle('#667eea'); + ctx.setLineWidth(2); + + for (let i = 0; i <= data.length; i++) { + const index = i % data.length; + const value = data[index]; + const angle = index * angleStep - Math.PI / 2; + const r = (value / 100) * radius; + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + + // 绘制数据点 + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.moveTo(x, y); + } + + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + ctx.draw(); + }); + }, + + /** + * 绘制折线图(GPA趋势) + */ + drawLineChart() { + const query = wx.createSelectorQuery(); + query.select('#lineChart').boundingClientRect(); + query.exec((res) => { + if (!res[0]) return; + + const canvasWidth = res[0].width; + const canvasHeight = res[0].height; + const ctx = wx.createCanvasContext('lineChart', this); + const history = this.data.gpaHistory; + const prediction = this.data.prediction; + + // 清空画布 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + // 如果没有数据,显示提示 + if (!history || history.length === 0) { + ctx.setFillStyle('#999999'); + ctx.setFontSize(24); + ctx.setTextAlign('center'); + ctx.fillText('暂无GPA数据', canvasWidth / 2, canvasHeight / 2); + ctx.fillText('请先在GPA工具中录入成绩', canvasWidth / 2, canvasHeight / 2 + 30); + ctx.draw(); + return; + } + + console.log('[绘制折线图] 历史数据:', history); + + const padding = { top: 40, right: 40, bottom: 60, left: 60 }; + const width = canvasWidth - padding.left - padding.right; + const height = canvasHeight - padding.top - padding.bottom; + + // 合并历史和预测数据 + const allData = [...history]; + const predictionResult = gpaPredictor.polynomialRegression(history, 2, 3); + if (predictionResult.predictions && predictionResult.predictions.length > 0) { + allData.push(...predictionResult.predictions); + console.log('[绘制折线图] 预测数据:', predictionResult.predictions); + } + + const maxGPA = 4.5; // 调整最大值为4.5 + const minGPA = 0; + const stepX = allData.length > 1 ? width / (allData.length - 1) : width / 2; + const scaleY = height / (maxGPA - minGPA); + + // 绘制坐标轴 + ctx.setStrokeStyle('#CCCCCC'); + ctx.setLineWidth(1); + ctx.beginPath(); + ctx.moveTo(padding.left, padding.top); + ctx.lineTo(padding.left, padding.top + height); + ctx.lineTo(padding.left + width, padding.top + height); + ctx.stroke(); + + // 绘制网格线和Y轴刻度 + ctx.setStrokeStyle('#F0F0F0'); + ctx.setFillStyle('#999999'); + ctx.setFontSize(20); + ctx.setTextAlign('right'); + + for (let i = 0; i <= 4; i++) { + const gpaValue = i * 1.0; + const y = padding.top + height - (gpaValue * scaleY); + ctx.beginPath(); + ctx.moveTo(padding.left, y); + ctx.lineTo(padding.left + width, y); + ctx.stroke(); + ctx.fillText(gpaValue.toFixed(1), padding.left - 10, y + 6); + } + + // 绘制历史数据折线 + if (history.length > 0) { + ctx.setStrokeStyle('#667eea'); + ctx.setLineWidth(3); + ctx.beginPath(); + + history.forEach((item, index) => { + const x = padding.left + index * stepX; + const y = padding.top + height - (item.gpa * scaleY); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.stroke(); + + // 绘制历史数据点和数值 + ctx.setFillStyle('#667eea'); + history.forEach((item, index) => { + const x = padding.left + index * stepX; + const y = padding.top + height - (item.gpa * scaleY); + ctx.beginPath(); + ctx.arc(x, y, 5, 0, Math.PI * 2); + ctx.fill(); + + // 显示GPA值 + ctx.setFillStyle('#333333'); + ctx.setFontSize(18); + ctx.setTextAlign('center'); + ctx.fillText(item.gpa.toFixed(2), x, y - 15); + }); + } + + // 绘制预测数据折线(虚线) + if (predictionResult.predictions && predictionResult.predictions.length > 0 && history.length > 0) { + ctx.setStrokeStyle('#A29BFE'); + ctx.setLineDash([5, 5]); + ctx.setLineWidth(2); + ctx.beginPath(); + + const startIndex = history.length - 1; + const startX = padding.left + startIndex * stepX; + const startY = padding.top + height - (history[startIndex].gpa * scaleY); + ctx.moveTo(startX, startY); + + predictionResult.predictions.forEach((item, index) => { + const x = padding.left + (startIndex + index + 1) * stepX; + const y = padding.top + height - (item.gpa * scaleY); + ctx.lineTo(x, y); + }); + + ctx.stroke(); + ctx.setLineDash([]); + + // 绘制预测数据点和数值 + ctx.setFillStyle('#A29BFE'); + predictionResult.predictions.forEach((item, index) => { + const x = padding.left + (startIndex + index + 1) * stepX; + const y = padding.top + height - (item.gpa * scaleY); + ctx.beginPath(); + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.fill(); + + // 显示预测GPA值 + ctx.setFillStyle('#A29BFE'); + ctx.setFontSize(18); + ctx.setTextAlign('center'); + ctx.fillText(item.gpa.toFixed(2), x, y - 15); + }); + } + + // 绘制X轴标签(旋转避免重叠) + ctx.setFillStyle('#999999'); + ctx.setFontSize(16); + ctx.setTextAlign('left'); + + allData.forEach((item, index) => { + const x = padding.left + index * stepX; + const y = padding.top + height + 20; + + // 旋转文字45度 + ctx.save(); + ctx.translate(x, y); + ctx.rotate(Math.PI / 4); + ctx.fillText(item.semester, 0, 0); + ctx.restore(); + }); + + ctx.draw(); + }); + }, + + /** + * 绘制饼图 + */ + drawPieChart() { + const query = wx.createSelectorQuery(); + query.select('#pieChart').boundingClientRect(); + query.exec((res) => { + if (!res[0]) return; + + const canvasWidth = res[0].width; + const canvasHeight = res[0].height; + const ctx = wx.createCanvasContext('pieChart', this); + const data = this.data.pieData; + const centerX = canvasWidth / 2; + const centerY = canvasHeight / 2; + const radius = Math.min(canvasWidth, canvasHeight) / 2 - 40; + + // 清空画布 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + // 计算总数(用于处理全0情况) + const total = data.reduce((sum, item) => sum + item.time, 0); + + if (total === 0) { + // 如果没有数据,绘制完整的灰色圆环 + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); + ctx.setFillStyle('#EEEEEE'); + ctx.fill(); + + // 绘制中心圆 + ctx.beginPath(); + ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2); + ctx.setFillStyle('#FFFFFF'); + ctx.fill(); + } else { + // 有数据时正常绘制 + let startAngle = -Math.PI / 2; + + data.forEach((item, index) => { + if (item.time > 0) { // 只绘制有数据的扇形 + const angle = (item.percent / 100) * Math.PI * 2; + const endAngle = startAngle + angle; + + // 绘制扇形 + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, startAngle, endAngle); + ctx.closePath(); + ctx.setFillStyle(item.color); + ctx.fill(); + + startAngle = endAngle; + } + }); + + // 绘制中心圆(甜甜圈效果) + ctx.beginPath(); + ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2); + ctx.setFillStyle('#FFFFFF'); + ctx.fill(); + } + + ctx.draw(); + }); + }, + + /** + * 绘制柱状图 + */ + drawBarChart() { + const query = wx.createSelectorQuery(); + query.select('#barChart').boundingClientRect(); + query.exec((res) => { + if (!res[0]) return; + + const canvasWidth = res[0].width; + const canvasHeight = res[0].height; + const ctx = wx.createCanvasContext('barChart', this); + const gpaCourses = wx.getStorageSync('gpaCourses') || []; + + // 清空画布 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + // 取前6门课程 + const courses = gpaCourses.slice(0, 6); + + if (courses.length === 0) { + ctx.setFillStyle('#999999'); + ctx.setFontSize(28); + ctx.fillText('暂无成绩数据', canvasWidth / 2 - 70, canvasHeight / 2); + ctx.draw(); + return; + } + + const padding = { top: 40, right: 40, bottom: 80, left: 60 }; + const width = canvasWidth - padding.left - padding.right; + const height = canvasHeight - padding.top - padding.bottom; + const barWidth = width / (courses.length * 2 + 1); + const maxScore = 100; + + // 绘制坐标轴 + ctx.setStrokeStyle('#CCCCCC'); + ctx.setLineWidth(1); + ctx.beginPath(); + ctx.moveTo(padding.left, padding.top); + ctx.lineTo(padding.left, padding.top + height); + ctx.lineTo(padding.left + width, padding.top + height); + ctx.stroke(); + + // 绘制网格线 + ctx.setStrokeStyle('#F0F0F0'); + for (let i = 0; i <= 5; i++) { + const y = padding.top + (height / 5) * i; + ctx.beginPath(); + ctx.moveTo(padding.left, y); + ctx.lineTo(padding.left + width, y); + ctx.stroke(); + + // Y轴刻度 + ctx.setFillStyle('#999999'); + ctx.setFontSize(20); + ctx.fillText((100 - i * 20).toString(), padding.left - 35, y + 6); + } + + // 绘制柱状图 + courses.forEach((course, index) => { + const x = padding.left + (index * 2 + 1) * barWidth; + const scoreHeight = (course.score / maxScore) * height; + const avgHeight = (75 / maxScore) * height; // 假设75为平均分 + + // 个人成绩 + ctx.setFillStyle('#667eea'); + ctx.fillRect(x, padding.top + height - scoreHeight, barWidth * 0.8, scoreHeight); + + // 平均成绩(对比) + ctx.setFillStyle('#CCCCCC'); + ctx.fillRect(x + barWidth, padding.top + height - avgHeight, barWidth * 0.8, avgHeight); + + // 课程名称 + ctx.setFillStyle('#333333'); + ctx.setFontSize(18); + ctx.save(); + ctx.translate(x + barWidth, padding.top + height + 20); + ctx.rotate(Math.PI / 4); + const courseName = course.name.length > 4 ? course.name.substring(0, 4) + '...' : course.name; + ctx.fillText(courseName, 0, 0); + ctx.restore(); + }); + + // 图例 + ctx.setFillStyle('#667eea'); + ctx.fillRect(padding.left + width - 120, padding.top - 30, 20, 15); + ctx.setFillStyle('#333333'); + ctx.setFontSize(20); + ctx.fillText('个人', padding.left + width - 95, padding.top - 18); + + ctx.setFillStyle('#CCCCCC'); + ctx.fillRect(padding.left + width - 50, padding.top - 30, 20, 15); + ctx.fillText('平均', padding.left + width - 25, padding.top - 18); + + ctx.draw(); + }); + }, + + /** + * 显示某天详情 + */ + showDayDetail(e) { + const { date } = e.currentTarget.dataset; + const dailyActivity = wx.getStorageSync('daily_activity') || {}; + const activity = dailyActivity[date] || 0; + + wx.showModal({ + title: date, + content: `学习活跃度: ${activity}分钟`, + showCancel: false + }); + } +}); diff --git a/pages/dashboard/dashboard.json b/pages/dashboard/dashboard.json new file mode 100644 index 0000000..f22ba8e --- /dev/null +++ b/pages/dashboard/dashboard.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "学习数据", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true, + "backgroundColor": "#F5F6FA" +} diff --git a/pages/dashboard/dashboard.wxml b/pages/dashboard/dashboard.wxml new file mode 100644 index 0000000..60b5697 --- /dev/null +++ b/pages/dashboard/dashboard.wxml @@ -0,0 +1,123 @@ + + + + + + {{stats.totalDays}} + 连续学习天数 + 🔥 + + + {{stats.totalHours}} + 累计学习小时 + + + + {{stats.avgGPA}} + 平均绩点 + 📊 + + + + + + + + 🎯 + 学习能力画像 + + 6维度综合评估 + + + + + + + {{item.name}} + {{radarData.values[index]}} + + + + + + + + + + 📈 + GPA趋势预测 + + 基于多项式回归分析 + + + + + + 预测下学期: + {{prediction.nextSemester}} + + + 趋势: + + {{prediction.trend > 0 ? '上升' : '下降'}} {{prediction.trend}}% + + + + + + + + + + + ⏱️ + 时间分配 + + 各功能使用占比 + + + + + + + {{item.name}} + {{item.percent}}% + {{item.time}}h + + + + + + + + + + 📊 + 成绩对比 + + 个人 vs 班级平均 + + + + + + 超过平均: + {{barData.aboveAvg}}门课程 + + + 排名: + 前{{barData.ranking}}% + + + + + + + + ℹ️ + + 数据说明 + 以上数据基于您的学习记录自动生成,更新时间: {{updateTime}} + + + diff --git a/pages/dashboard/dashboard.wxss b/pages/dashboard/dashboard.wxss new file mode 100644 index 0000000..110d4d9 --- /dev/null +++ b/pages/dashboard/dashboard.wxss @@ -0,0 +1,279 @@ +/* pages/dashboard/dashboard.wxss */ +@import "/styles/design-tokens.wxss"; +@import "/styles/premium-animations.wxss"; + +.dashboard-container { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 30rpx; + padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx); +} + +/* 顶部统计卡片 */ +.stats-header { + display: flex; + justify-content: space-between; + margin-bottom: 30rpx; + gap: 20rpx; +} + +.stats-card { + flex: 1; + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 30rpx 20rpx; + text-align: center; + position: relative; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); + animation: fadeInUp 0.6s ease; +} + +.stat-value { + font-size: 48rpx; + font-weight: bold; + color: #667eea; + margin-bottom: 8rpx; +} + +.stat-label { + font-size: 22rpx; + color: #666666; +} + +.stat-icon { + position: absolute; + top: 20rpx; + right: 20rpx; + font-size: 32rpx; + opacity: 0.3; +} + +/* 图表区域 */ +.chart-section { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); + animation: fadeInUp 0.6s ease; +} + +.section-header { + margin-bottom: 30rpx; + padding-bottom: 20rpx; + border-bottom: 2rpx solid #F0F0F0; +} + +.section-title { + display: flex; + align-items: center; + margin-bottom: 8rpx; +} + +.title-icon { + font-size: 36rpx; + margin-right: 12rpx; +} + +.title-text { + font-size: 32rpx; + font-weight: bold; + color: #333333; +} + +.section-desc { + font-size: 24rpx; + color: #999999; + margin-left: 48rpx; +} + +/* 画布容器 */ +.chart-container { + width: 100%; +} + +.chart-canvas { + width: 100%; + height: 500rpx; +} + +.pie-canvas { + height: 400rpx; +} + +/* 雷达图图例 */ +.chart-legend { + margin-top: 30rpx; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20rpx; +} + +.legend-item { + display: flex; + align-items: center; + font-size: 24rpx; +} + +.legend-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + margin-right: 12rpx; +} + +.legend-name { + flex: 1; + color: #666666; +} + +.legend-value { + font-weight: bold; + color: #667eea; +} + +/* 预测信息 */ +.prediction-info { + margin-top: 30rpx; + display: flex; + justify-content: space-around; + padding: 20rpx; + background: #F8F9FA; + border-radius: 16rpx; +} + +.prediction-item { + text-align: center; +} + +.pred-label { + font-size: 24rpx; + color: #999999; + display: block; + margin-bottom: 8rpx; +} + +.pred-value { + font-size: 32rpx; + font-weight: bold; + color: #667eea; +} + +.trend-up { + color: #4CAF50; +} + +.trend-down { + color: #FF5252; +} + +/* 饼图图例 */ +.pie-legend { + margin-top: 30rpx; +} + +.pie-legend-item { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid #F0F0F0; + font-size: 26rpx; +} + +.pie-legend-item:last-child { + border-bottom: none; +} + +.pie-dot { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + margin-right: 16rpx; +} + +.pie-name { + flex: 1; + color: #333333; +} + +.pie-percent { + font-weight: bold; + color: #667eea; + margin-right: 20rpx; +} + +.pie-time { + color: #999999; +} + +/* 柱状图总结 */ +.bar-summary { + margin-top: 30rpx; + display: flex; + justify-content: space-around; + padding: 20rpx; + background: #F8F9FA; + border-radius: 16rpx; +} + +.summary-item { + text-align: center; +} + +.summary-label { + font-size: 24rpx; + color: #999999; + display: block; + margin-bottom: 8rpx; +} + +.summary-value { + font-size: 32rpx; + font-weight: bold; + color: #667eea; +} + +/* 数据来源说明 */ +.data-source { + display: flex; + align-items: flex-start; + padding: 30rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 16rpx; + margin-top: 20rpx; +} + +.source-icon { + font-size: 36rpx; + margin-right: 16rpx; +} + +.source-text { + flex: 1; +} + +.source-title { + font-size: 26rpx; + font-weight: bold; + color: #FFFFFF; + display: block; + margin-bottom: 8rpx; +} + +.source-desc { + font-size: 22rpx; + color: rgba(255, 255, 255, 0.9); + line-height: 1.6; +} + +/* 动画 */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/pages/forum-detail/forum-detail.js b/pages/forum-detail/forum-detail.js new file mode 100644 index 0000000..4a0348a --- /dev/null +++ b/pages/forum-detail/forum-detail.js @@ -0,0 +1,272 @@ +// pages/forum-detail/forum-detail.js +const {forumData} = require('../../utils/data.js') +const learningTracker = require('../../utils/learningTracker.js') +const { showSuccess } = require('../../utils/util.js') + +// pages/forum-detail/forum-detail.js +const userManager = require('../../utils/userManager.js') + +Page({ + data: { + post: null, + commentText: '', + comments: [], + postId: null + }, + + onLoad(options) { + const { id } = options + this.setData({ postId: parseInt(id) }) + this.loadPostDetail(id) + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('forum') + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 加载帖子详情 + loadPostDetail(id) { + let posts = wx.getStorageSync('forumPosts') || forumData + const post = posts.find(p => p.id === parseInt(id)) + + if (post) { + // 增加浏览量 + post.views += 1 + + // 检查是否已收藏 + const favoritePosts = wx.getStorageSync('favoritePosts') || [] + post.isFavorite = favoritePosts.some(fav => fav.id === post.id) + + posts = posts.map(p => p.id === post.id ? post : p) + wx.setStorageSync('forumPosts', posts) + + // 加载评论 + const comments = this.loadComments(parseInt(id)) + + this.setData({ + post, + comments + }) + } + }, + + // 加载评论 + loadComments(postId) { + const allComments = wx.getStorageSync('forumComments') || {} + return allComments[postId] || [] + }, + + // 保存评论 + saveComments(postId, comments) { + const allComments = wx.getStorageSync('forumComments') || {} + allComments[postId] = comments + wx.setStorageSync('forumComments', allComments) + + // 更新帖子的评论数 + let posts = wx.getStorageSync('forumPosts') || forumData + posts = posts.map(p => { + if (p.id === postId) { + p.comments = comments.length + } + return p + }) + wx.setStorageSync('forumPosts', posts) + }, + + // 点赞 + onLike() { + const { post } = this.data + let posts = wx.getStorageSync('forumPosts') || forumData + + posts = posts.map(p => { + if (p.id === post.id) { + p.isLiked = !p.isLiked + p.likes = p.isLiked ? p.likes + 1 : p.likes - 1 + } + return p + }) + + wx.setStorageSync('forumPosts', posts) + + this.setData({ + post: { + ...post, + isLiked: !post.isLiked, + likes: post.isLiked ? post.likes - 1 : post.likes + 1 + } + }) + }, + + // 收藏帖子 + onFavorite() { + const { post } = this.data + + if (!post || !post.id || !post.title) { + wx.showToast({ + title: '帖子数据异常', + icon: 'none' + }) + console.error('帖子数据不完整:', post) + return + } + + let favoritePosts = wx.getStorageSync('favoritePosts') || [] + const index = favoritePosts.findIndex(p => p.id === post.id) + + if (index > -1) { + // 已收藏,取消收藏 + favoritePosts.splice(index, 1) + wx.setStorageSync('favoritePosts', favoritePosts) + + console.log('取消收藏后的列表:', favoritePosts) + + // 更新帖子状态 + let posts = wx.getStorageSync('forumPosts') || [] + posts = posts.map(p => { + if (p.id === post.id) { + p.isFavorite = false + } + return p + }) + wx.setStorageSync('forumPosts', posts) + + this.setData({ + post: { + ...post, + isFavorite: false + } + }) + + wx.showToast({ + title: '已取消收藏', + icon: 'success' + }) + } else { + // 未收藏,添加收藏 + const favoritePost = { + id: post.id, + title: post.title, + category: post.category, + time: new Date().toLocaleString() + } + + favoritePosts.push(favoritePost) + wx.setStorageSync('favoritePosts', favoritePosts) + + console.log('收藏成功,当前收藏列表:', favoritePosts) + + // 更新帖子状态 + let posts = wx.getStorageSync('forumPosts') || [] + posts = posts.map(p => { + if (p.id === post.id) { + p.isFavorite = true + } + return p + }) + wx.setStorageSync('forumPosts', posts) + + this.setData({ + post: { + ...post, + isFavorite: true + } + }) + + wx.showToast({ + title: '收藏成功', + icon: 'success' + }) + } + }, + + // 评论输入 + onCommentInput(e) { + this.setData({ commentText: e.detail.value }) + }, + + // 发表评论 + onSubmitComment() { + const { commentText, comments, postId } = this.data + + if (!commentText.trim()) { + wx.showToast({ + title: '请输入评论内容', + icon: 'none' + }) + return + } + + // 获取用户信息 + const userInfo = userManager.getUserInfo() + const userName = userInfo.nickname || '我' + const userAvatar = userInfo.avatar || '/images/avatar-default.png' + + const newComment = { + id: Date.now(), + author: userName, + avatar: userAvatar, + content: commentText, + time: '刚刚' + } + + const newComments = [newComment, ...comments] + + // 保存评论 + this.saveComments(postId, newComments) + + // 同时将评论保存到帖子的commentList中 + let posts = wx.getStorageSync('forumPosts') || [] + posts = posts.map(p => { + if (p.id === postId) { + if (!p.commentList) { + p.commentList = [] + } + p.commentList.unshift(newComment) + } + return p + }) + wx.setStorageSync('forumPosts', posts) + + this.setData({ + comments: newComments, + commentText: '' + }) + + showSuccess('评论成功') + + console.log('评论已发布', { + 作者: userName, + 内容: commentText, + 帖子ID: postId + }) + }, + + // 预览图片 + onPreviewImage(e) { + const { url, urls } = e.currentTarget.dataset + wx.previewImage({ + current: url, // 当前显示图片的链接 + urls: urls // 需要预览的图片链接列表 + }) + }, + + // 分享 + onShareAppMessage() { + const { post } = this.data + return { + title: post.title, + path: `/pages/forum-detail/forum-detail?id=${post.id}` + } + } +}) diff --git a/pages/forum-detail/forum-detail.json b/pages/forum-detail/forum-detail.json new file mode 100644 index 0000000..6e8904a --- /dev/null +++ b/pages/forum-detail/forum-detail.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "帖子详情" +} diff --git a/pages/forum-detail/forum-detail.wxml b/pages/forum-detail/forum-detail.wxml new file mode 100644 index 0000000..fffdfe6 --- /dev/null +++ b/pages/forum-detail/forum-detail.wxml @@ -0,0 +1,111 @@ + + + + + + + + + {{post.author}} + {{post.time}} + + {{post.category}} + + + + {{post.title}} + + + {{post.content}} + + + + + + + + + + 👀 + {{post.views}}次浏览 + + + ❤️ + {{post.likes}}次点赞 + + + 💬 + {{comments.length}}条评论 + + + + + + + + 全部评论 + ({{comments.length}}) + + + + + + + {{item.author}} + {{item.content}} + {{item.time}} + + + + + 暂无评论,快来发表第一条评论吧~ + + + + + + + + + + + + + + + + {{post.isLiked ? '❤️' : '🤍'}} + 点赞 + + + + {{post.isFavorite ? '⭐' : '☆'}} + 收藏 + + + + diff --git a/pages/forum-detail/forum-detail.wxss b/pages/forum-detail/forum-detail.wxss new file mode 100644 index 0000000..79137ee --- /dev/null +++ b/pages/forum-detail/forum-detail.wxss @@ -0,0 +1,278 @@ +/* pages/forum-detail/forum-detail.wxss */ +.container { + padding-bottom: 150rpx; + background-color: #F5F5F5; +} + +/* 帖子详情 */ +.post-detail { + background-color: #ffffff; + padding: 30rpx; + margin-bottom: 20rpx; +} + +.post-header { + display: flex; + align-items: center; + margin-bottom: 30rpx; +} + +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 40rpx; + margin-right: 20rpx; + background-color: #E0E0E0; +} + +.author-info { + flex: 1; +} + +.author-name { + font-size: 30rpx; + font-weight: bold; + color: #333333; + margin-bottom: 8rpx; +} + +.post-time { + font-size: 24rpx; + color: #999999; +} + +.category-tag { + padding: 10rpx 24rpx; + background-color: #E8F8F0; + color: #50C878; + border-radius: 20rpx; + font-size: 24rpx; +} + +.post-title { + font-size: 36rpx; + font-weight: bold; + color: #333333; + line-height: 1.5; + margin-bottom: 20rpx; +} + +.post-content { + font-size: 30rpx; + color: #666666; + line-height: 1.8; + margin-bottom: 20rpx; + text-align: justify; +} + +.post-images { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15rpx; + margin-bottom: 20rpx; +} + +.post-image { + width: 100%; + height: 200rpx; + border-radius: 8rpx; + background-color: #F5F5F5; +} + +.post-stats { + display: flex; + gap: 40rpx; + padding-top: 20rpx; + border-top: 1rpx solid #EEEEEE; +} + +.stat-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 24rpx; + color: #999999; +} + +.stat-icon { + font-size: 28rpx; +} + +/* 评论区 */ +.comments-section { + background-color: #ffffff; + padding: 30rpx; +} + +.section-title { + display: flex; + align-items: baseline; + margin-bottom: 30rpx; +} + +.title-text { + font-size: 32rpx; + font-weight: bold; + color: #333333; +} + +.title-count { + font-size: 26rpx; + color: #999999; + margin-left: 10rpx; +} + +.comments-list { + margin-top: 20rpx; +} + +.comment-item { + display: flex; + padding: 20rpx 0; + border-bottom: 1rpx solid #F5F5F5; +} + +.comment-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 30rpx; + margin-right: 15rpx; + background-color: #E0E0E0; +} + +.comment-content { + flex: 1; +} + +.comment-author { + font-size: 26rpx; + font-weight: bold; + color: #333333; + margin-bottom: 10rpx; +} + +.comment-text { + font-size: 28rpx; + color: #666666; + line-height: 1.6; + margin-bottom: 10rpx; +} + +.comment-time { + font-size: 22rpx; + color: #999999; +} + +.no-comments { + text-align: center; + padding: 60rpx 0; +} + +.no-comments-text { + font-size: 26rpx; + color: #999999; +} + +/* 底部操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #ffffff; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08); + padding: 20rpx 30rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); +} + +/* 输入行 */ +.input-row { + display: flex; + align-items: center; + gap: 15rpx; + margin-bottom: 15rpx; +} + +.comment-input { + flex: 1; + height: 70rpx; + background-color: #F5F5F5; + border-radius: 35rpx; + padding: 0 30rpx; + font-size: 28rpx; +} + +.send-btn { + padding: 0 35rpx; + height: 70rpx; + line-height: 70rpx; + background: linear-gradient(135deg, #50C878 0%, #3CB371 100%); + color: #ffffff; + border-radius: 35rpx; + font-size: 28rpx; + font-weight: bold; + box-shadow: 0 4rpx 12rpx rgba(80, 200, 120, 0.3); + transition: all 0.3s ease; +} + +.send-btn:active { + transform: scale(0.95); +} + +/* 操作按钮行 */ +.action-row { + display: flex; + justify-content: space-around; + align-items: center; + gap: 20rpx; +} + +.action-btn { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 12rpx 0; + background-color: transparent; + border: none; + margin: 0; + line-height: 1; + transition: all 0.3s ease; +} + +.action-btn::after { + border: none; +} + +.action-btn:active { + transform: scale(1.1); +} + +.action-icon { + font-size: 36rpx; + margin-bottom: 6rpx; +} + +.action-text { + font-size: 22rpx; + color: #666666; +} + +.action-btn.liked .action-icon { + animation: pulse 0.6s ease; +} + +.action-btn.liked .action-text { + color: #FF6B6B; + font-weight: bold; +} + +.action-btn.favorited .action-icon { + animation: pulse 0.6s ease; +} + +.action-btn.favorited .action-text { + color: #FFD700; + font-weight: bold; +} diff --git a/pages/forum/forum.js b/pages/forum/forum.js new file mode 100644 index 0000000..414df38 --- /dev/null +++ b/pages/forum/forum.js @@ -0,0 +1,191 @@ +// pages/forum/forum.js +const {forumData} = require('../../utils/data.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + posts: [], + categories: ['全部', '数学', '物理', '计算机', '英语', '其他'], + selectedCategory: '全部' + }, + + onLoad() { + this.loadPosts() + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('forum') + + // 每次显示时重新加载,以获取最新发布的帖子 + this.loadPosts() + + // 更新自定义TabBar选中状态 + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ + selected: 2 + }) + } + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 加载帖子 + loadPosts() { + try { + let posts = wx.getStorageSync('forumPosts') || forumData + + // 同步评论数量 + const allComments = wx.getStorageSync('forumComments') || {} + // 获取收藏的帖子列表 + const favoritePosts = wx.getStorageSync('favoritePosts') || [] + + posts = posts.map(post => { + const comments = allComments[post.id] || [] + post.comments = comments.length + // 检查是否已收藏 + post.isFavorite = favoritePosts.some(fav => fav.id === post.id) + return post + }) + + // 保存更新后的帖子数据 + wx.setStorageSync('forumPosts', posts) + + // 按时间排序(最新的在前) + posts.sort((a, b) => b.id - a.id) + + this.setData({ posts }) + this.filterPosts() + } catch (error) { + console.error('加载帖子失败:', error) + wx.showToast({ + title: '数据加载失败', + icon: 'none' + }) + } + }, + + // 分类筛选 + onCategoryChange(e) { + const category = e.currentTarget.dataset.category + this.setData({ selectedCategory: category }) + this.filterPosts() + }, + + // 筛选帖子 + filterPosts() { + const { selectedCategory } = this.data + let allPosts = wx.getStorageSync('forumPosts') || forumData + + // 同步评论数量和收藏状态 + const allComments = wx.getStorageSync('forumComments') || {} + const favoritePosts = wx.getStorageSync('favoritePosts') || [] + + allPosts = allPosts.map(post => { + const comments = allComments[post.id] || [] + post.comments = comments.length + // 检查是否已收藏 + post.isFavorite = favoritePosts.some(fav => fav.id === post.id) + return post + }) + + if (selectedCategory === '全部') { + this.setData({ posts: allPosts }) + } else { + const filtered = allPosts.filter(post => post.category === selectedCategory) + this.setData({ posts: filtered }) + } + }, + + // 查看帖子详情 + onPostDetail(e) { + const { id } = e.currentTarget.dataset + wx.navigateTo({ + url: `/pages/forum-detail/forum-detail?id=${id}` + }) + }, + + // 发布新帖子 + onNewPost() { + wx.navigateTo({ + url: '/pages/post/post' + }) + }, + + // 点赞 + onLike(e) { + const { id } = e.currentTarget.dataset + let posts = wx.getStorageSync('forumPosts') || forumData + + posts = posts.map(post => { + if (post.id === id) { + post.isLiked = !post.isLiked + post.likes = post.isLiked ? post.likes + 1 : post.likes - 1 + } + return post + }) + + wx.setStorageSync('forumPosts', posts) + this.loadPosts() + }, + + // 预览图片 + onPreviewImage(e) { + const { url, urls } = e.currentTarget.dataset + wx.previewImage({ + current: url, // 当前显示图片的链接 + urls: urls // 需要预览的图片链接列表 + }) + }, + + // 收藏/取消收藏 + onFavorite(e) { + const { id } = e.currentTarget.dataset + let favoritePosts = wx.getStorageSync('favoritePosts') || [] + let posts = wx.getStorageSync('forumPosts') || forumData + + // 找到当前帖子 + const currentPost = posts.find(post => post.id === id) + if (!currentPost) return + + // 检查是否已收藏 + const index = favoritePosts.findIndex(fav => fav.id === id) + + if (index > -1) { + // 已收藏,取消收藏 + favoritePosts.splice(index, 1) + wx.showToast({ + title: '取消收藏', + icon: 'success', + duration: 1500 + }) + } else { + // 未收藏,添加收藏 + favoritePosts.push({ + id: currentPost.id, + title: currentPost.title, + category: currentPost.category, + time: new Date().toLocaleString() + }) + wx.showToast({ + title: '收藏成功', + icon: 'success', + duration: 1500 + }) + } + + // 保存收藏列表 + wx.setStorageSync('favoritePosts', favoritePosts) + + // 重新加载帖子列表 + this.loadPosts() + } +}) diff --git a/pages/forum/forum.json b/pages/forum/forum.json new file mode 100644 index 0000000..d2794de --- /dev/null +++ b/pages/forum/forum.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "学科论坛" +} diff --git a/pages/forum/forum.wxml b/pages/forum/forum.wxml new file mode 100644 index 0000000..261557c --- /dev/null +++ b/pages/forum/forum.wxml @@ -0,0 +1,99 @@ + + + + + + + {{item}} + + + + + + + + + + + + {{item.author}} + {{item.time}} + + {{item.category}} + + + + + {{item.title}} + {{item.content}} + + + + + + + + + + + + + + 📝 + 暂无帖子,快来发布第一条吧 + + + + + + ✏️ + + diff --git a/pages/forum/forum.wxss b/pages/forum/forum.wxss new file mode 100644 index 0000000..18d393f --- /dev/null +++ b/pages/forum/forum.wxss @@ -0,0 +1,338 @@ +/* pages/forum/forum.wxss */ +.container { + padding-bottom: 30rpx; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + min-height: 100vh; + animation: fadeIn 0.5s ease-out; + /* 底部留出TabBar的空间 */ + padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx); +} + +/* 分类标签 */ +.category-scroll { + white-space: nowrap; + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + padding: 24rpx 0; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); + backdrop-filter: blur(10rpx); + animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.category-list { + display: inline-flex; + padding: 0 30rpx; + gap: 20rpx; +} + +.category-item { + display: inline-block; + padding: 14rpx 32rpx; + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + color: #666666; + border-radius: 32rpx; + font-size: 26rpx; + font-weight: 500; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); +} + +.category-item:active { + transform: scale(0.95); +} + +.category-item.active { + background: linear-gradient(135deg, #50C878 0%, #3CB371 100%); + color: #ffffff; + box-shadow: 0 4rpx 12rpx rgba(80, 200, 120, 0.3); + transform: scale(1.05); +} + +/* 帖子列表 */ +.post-list { + padding: 24rpx 30rpx; +} + +.post-card { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + padding: 35rpx; + margin-bottom: 24rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + animation: slideInUp 0.6s ease-out; + position: relative; + overflow: hidden; +} + +.post-card::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6rpx; + background: linear-gradient(to bottom, #50C878, #3CB371); + transform: scaleY(0); + transition: transform 0.3s ease; +} + +.post-card:active { + transform: translateY(-4rpx); + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.12); +} + +.post-card:active::before { + transform: scaleY(1); +} + +.post-card:nth-child(1) { animation-delay: 0.1s; } +.post-card:nth-child(2) { animation-delay: 0.15s; } +.post-card:nth-child(3) { animation-delay: 0.2s; } +.post-card:nth-child(4) { animation-delay: 0.25s; } +.post-card:nth-child(5) { animation-delay: 0.3s; } + +/* 帖子头部 */ +.post-header { + display: flex; + align-items: center; + margin-bottom: 24rpx; +} + +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin-right: 18rpx; + background: linear-gradient(135deg, #e0e0e0 0%, #c8c8c8 100%); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.avatar:active { + transform: scale(1.1); +} + +.author-info { + flex: 1; +} + +.author-name { + font-size: 30rpx; + font-weight: bold; + color: #333333; + margin-bottom: 8rpx; +} + +.post-time { + font-size: 24rpx; + color: #999999; + opacity: 0.8; +} + +.category-tag { + padding: 10rpx 22rpx; + background: linear-gradient(135deg, #E8F8F0 0%, #D5F2E3 100%); + color: #50C878; + border-radius: 24rpx; + font-size: 22rpx; + font-weight: 600; + box-shadow: 0 2rpx 6rpx rgba(80, 200, 120, 0.2); +} + +/* 帖子内容 */ +.post-content { + margin-bottom: 24rpx; +} + +.post-title { + font-size: 34rpx; + font-weight: bold; + color: #333333; + margin-bottom: 18rpx; + line-height: 1.6; + position: relative; +} + +.post-text { + font-size: 28rpx; + color: #666666; + line-height: 1.8; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +.post-images { + display: flex; + gap: 12rpx; + margin-top: 18rpx; + flex-wrap: wrap; +} + +.post-image { + width: 200rpx; + height: 200rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.post-image:active { + transform: scale(0.95); +} + +/* 帖子底部 */ +.post-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 2rpx solid rgba(0, 0, 0, 0.05); +} + +.footer-left { + display: flex; + align-items: center; + gap: 45rpx; + flex: 1; +} + +.stat-item { + display: flex; + align-items: center; + gap: 10rpx; + font-size: 24rpx; + color: #999999; + padding: 8rpx 16rpx; + border-radius: 20rpx; + transition: all 0.3s ease; +} + +.stat-item:active { + background-color: rgba(0, 0, 0, 0.03); + transform: scale(1.1); +} + +.stat-icon { + font-size: 32rpx; + transition: all 0.3s ease; +} + +.like-item.liked .stat-text { + color: #FF6B6B; + font-weight: bold; +} + +.like-item.liked .stat-icon { + animation: pulse 0.6s ease; +} + +/* 收藏按钮 */ +.favorite-btn { + display: flex; + align-items: center; + justify-content: center; + width: 68rpx; + height: 68rpx; + border-radius: 50%; + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); +} + +.favorite-btn:active { + transform: scale(0.9) rotate(72deg); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12); +} + +.favorite-btn.favorited { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); + box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.4); +} + +.favorite-icon { + font-size: 38rpx; + transition: all 0.3s ease; + color: #999999; +} + +.favorite-btn.favorited .favorite-icon { + color: #ffffff; + animation: bounce 0.6s ease; +} + +/* 收藏动画 */ +@keyframes bounce { + 0%, 100% { + transform: scale(1); + } + 25% { + transform: scale(1.3); + } + 50% { + transform: scale(0.9); + } + 75% { + transform: scale(1.1); + } +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 60rpx; + animation: fadeIn 0.8s ease-out; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 24rpx; + opacity: 0.4; + animation: pulse 2s ease-in-out infinite; +} + +.empty-text { + font-size: 28rpx; + color: #999999; +} + +/* 发布按钮 */ +.fab { + position: fixed; + right: 30rpx; + bottom: calc(env(safe-area-inset-bottom) + 120rpx); + width: 110rpx; + height: 110rpx; + border-radius: 50%; + background: linear-gradient(135deg, #50C878 0%, #3CB371 100%); + box-shadow: 0 12rpx 32rpx rgba(80, 200, 120, 0.5); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + animation: scaleIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.5s both; + z-index: 999; +} + +.fab:active { + transform: scale(0.88) rotate(90deg); + box-shadow: 0 6rpx 16rpx rgba(80, 200, 120, 0.5); +} + +.fab-icon { + font-size: 52rpx; + color: #ffffff; + transition: all 0.3s ease; +} + +.fab:active .fab-icon { + transform: rotate(-90deg); +} diff --git a/pages/gpa/gpa.js b/pages/gpa/gpa.js new file mode 100644 index 0000000..f5c2dad --- /dev/null +++ b/pages/gpa/gpa.js @@ -0,0 +1,150 @@ +// pages/gpa/gpa.js +const { calculateGPA, showSuccess, showError } = require('../../utils/util.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + courses: [], + courseName: '', + courseScore: '', + courseCredit: '', + totalGPA: 0, + totalCredits: 0 + }, + + onLoad() { + this.loadCourses() + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('tools') + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 加载已保存的课程 + loadCourses() { + const courses = wx.getStorageSync('gpaCourses') || [] + this.setData({ courses }) + this.calculateTotal() + }, + + // 课程名称输入 + onNameInput(e) { + this.setData({ courseName: e.detail.value }) + }, + + // 成绩输入 + onScoreInput(e) { + this.setData({ courseScore: e.detail.value }) + }, + + // 学分输入 + onCreditInput(e) { + this.setData({ courseCredit: e.detail.value }) + }, + + // 添加课程 + onAddCourse() { + const { courseName, courseScore, courseCredit, courses } = this.data + + if (!courseName.trim()) { + showError('请输入课程名称') + return + } + + const score = parseFloat(courseScore) + const credit = parseFloat(courseCredit) + + if (isNaN(score) || score < 0 || score > 100) { + showError('请输入有效的成绩(0-100)') + return + } + + if (isNaN(credit) || credit <= 0) { + showError('请输入有效的学分') + return + } + + const newCourse = { + id: Date.now(), + name: courseName.trim(), + score: score, + credit: credit + } + + courses.push(newCourse) + wx.setStorageSync('gpaCourses', courses) + + this.setData({ + courses, + courseName: '', + courseScore: '', + courseCredit: '' + }) + + this.calculateTotal() + showSuccess('添加成功') + }, + + // 删除课程 + onDeleteCourse(e) { + const { id } = e.currentTarget.dataset + + wx.showModal({ + title: '确认删除', + content: '确定要删除这门课程吗?', + success: (res) => { + if (res.confirm) { + let { courses } = this.data + courses = courses.filter(course => course.id !== id) + wx.setStorageSync('gpaCourses', courses) + + this.setData({ courses }) + this.calculateTotal() + showSuccess('删除成功') + } + } + }) + }, + + // 计算总GPA + calculateTotal() { + const { courses } = this.data + const gpa = calculateGPA(courses) + const totalCredits = courses.reduce((sum, course) => sum + course.credit, 0) + + this.setData({ + totalGPA: gpa, + totalCredits: totalCredits + }) + }, + + // 清空所有 + onClearAll() { + wx.showModal({ + title: '确认清空', + content: '确定要清空所有课程吗?', + success: (res) => { + if (res.confirm) { + wx.removeStorageSync('gpaCourses') + this.setData({ + courses: [], + totalGPA: 0, + totalCredits: 0 + }) + showSuccess('已清空') + } + } + }) + } +}) diff --git a/pages/gpa/gpa.json b/pages/gpa/gpa.json new file mode 100644 index 0000000..83cfbd3 --- /dev/null +++ b/pages/gpa/gpa.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "GPA计算器" +} diff --git a/pages/gpa/gpa.wxml b/pages/gpa/gpa.wxml new file mode 100644 index 0000000..360ce4b --- /dev/null +++ b/pages/gpa/gpa.wxml @@ -0,0 +1,72 @@ + + + + + 当前GPA + {{totalGPA}} + 总学分:{{totalCredits}} + + + + + 添加课程 + + + + + + + + + + + + + + + + + + + + + 课程列表 + 清空 + + + + + {{item.name}} + + 成绩:{{item.score}} + 学分:{{item.credit}} + + + + 🗑️ + + + + + diff --git a/pages/gpa/gpa.wxss b/pages/gpa/gpa.wxss new file mode 100644 index 0000000..efe4247 --- /dev/null +++ b/pages/gpa/gpa.wxss @@ -0,0 +1,328 @@ +/* pages/gpa/gpa.wxss */ +.container { + min-height: 100vh; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + padding: 30rpx; + animation: fadeIn 0.5s ease-out; +} + +/* GPA卡片 */ +.gpa-card { + background: linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%); + border-radius: 24rpx; + padding: 50rpx 30rpx; + text-align: center; + color: #ffffff; + margin-bottom: 30rpx; + box-shadow: 0 12rpx 32rpx rgba(255, 107, 107, 0.4); + animation: scaleIn 0.6s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.gpa-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%); + animation: float 6s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(30rpx, 30rpx); } +} + +.gpa-label { + font-size: 28rpx; + opacity: 0.95; + margin-bottom: 15rpx; + position: relative; + letter-spacing: 2rpx; +} + +.gpa-value { + font-size: 96rpx; + font-weight: bold; + margin-bottom: 15rpx; + position: relative; + text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2); + animation: pulse 2s ease-in-out infinite; +} + +.credits-info { + font-size: 26rpx; + opacity: 0.9; + position: relative; +} + +/* 表单卡片 */ +.form-card { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + padding: 35rpx; + margin-bottom: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + animation: slideInUp 0.6s ease-out 0.2s both; +} + +.form-title { + font-size: 34rpx; + font-weight: bold; + color: #333333; + margin-bottom: 30rpx; + position: relative; + padding-bottom: 15rpx; +} + +.form-title::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60rpx; + height: 4rpx; + background: linear-gradient(to right, #FF6B6B, #FF8E8E); + border-radius: 2rpx; +} + +.form-row { + margin-bottom: 25rpx; +} + +.form-row-group { + display: flex; + gap: 20rpx; + margin-bottom: 25rpx; +} + +.form-row.half { + flex: 1; + margin-bottom: 0; +} + +.form-input { + width: 100%; + height: 88rpx; + padding: 0 28rpx; + background-color: #f8f9fa; + border: 2rpx solid #e9ecef; + border-radius: 16rpx; + font-size: 28rpx; + box-sizing: border-box; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.form-input:focus { + background-color: #ffffff; + border-color: #FF6B6B; + box-shadow: 0 0 0 4rpx rgba(255, 107, 107, 0.1); +} + +.add-btn { + width: 100%; + height: 96rpx; + line-height: 96rpx; + background: linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%); + color: #ffffff; + border-radius: 48rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.add-btn:active { + transform: scale(0.98); + box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3); +} + +/* 课程列表 */ +.courses-section { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + padding: 35rpx; + margin-bottom: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + animation: slideInUp 0.6s ease-out 0.3s both; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.section-title { + font-size: 34rpx; + font-weight: bold; + color: #333333; + position: relative; + padding-bottom: 10rpx; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 50rpx; + height: 3rpx; + background: linear-gradient(to right, #FF6B6B, #FF8E8E); + border-radius: 2rpx; +} + +.clear-btn { + font-size: 26rpx; + color: #FF6B6B; + padding: 12rpx 24rpx; + background-color: rgba(255, 107, 107, 0.1); + border-radius: 20rpx; + transition: all 0.3s ease; +} + +.clear-btn:active { + background-color: rgba(255, 107, 107, 0.2); + transform: scale(0.95); +} + +.course-item { + display: flex; + align-items: center; + padding: 30rpx; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: 16rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + animation: slideInLeft 0.5s ease-out; +} + +.course-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6rpx; + background: linear-gradient(to bottom, #FF6B6B, #FF8E8E); + transform: scaleY(0); + transition: transform 0.3s ease; +} + +.course-item:active::before { + transform: scaleY(1); +} + +.course-item:active { + transform: translateX(8rpx); + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1); +} + +.course-item:last-child { + margin-bottom: 0; +} + +.course-item:nth-child(1) { animation-delay: 0.1s; } +.course-item:nth-child(2) { animation-delay: 0.15s; } +.course-item:nth-child(3) { animation-delay: 0.2s; } +.course-item:nth-child(4) { animation-delay: 0.25s; } +.course-item:nth-child(5) { animation-delay: 0.3s; } + +.course-info { + flex: 1; +} + +.course-name { + font-size: 32rpx; + font-weight: bold; + color: #333333; + margin-bottom: 12rpx; +} + +.course-details { + display: flex; + gap: 30rpx; +} + +.detail-item { + font-size: 24rpx; + color: #666666; + padding: 6rpx 16rpx; + background-color: rgba(255, 107, 107, 0.08); + border-radius: 12rpx; +} + +.delete-btn { + font-size: 44rpx; + padding: 12rpx; + color: #ff4757; + transition: all 0.3s ease; +} + +.delete-btn:active { + transform: scale(1.2) rotate(90deg); + color: #ff6b6b; +} + +/* 提示卡片 */ +.tips-card { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + padding: 35rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + animation: slideInUp 0.6s ease-out 0.4s both; +} + +.tips-title { + font-size: 30rpx; + font-weight: bold; + color: #333333; + margin-bottom: 24rpx; + position: relative; + padding-left: 20rpx; +} + +.tips-title::before { + content: '💡'; + position: absolute; + left: 0; + top: -2rpx; + font-size: 32rpx; +} + +.tips-content { + font-size: 26rpx; + color: #666666; + line-height: 2.2; + padding-left: 20rpx; +} + +.tip-item { + margin-bottom: 12rpx; + position: relative; + padding-left: 24rpx; + animation: fadeIn 0.8s ease-out; +} + +.tip-item::before { + content: '•'; + position: absolute; + left: 0; + color: #FF6B6B; + font-weight: bold; + font-size: 32rpx; + line-height: 1.5; +} + +.tip-item:nth-child(1) { animation-delay: 0.5s; } +.tip-item:nth-child(2) { animation-delay: 0.6s; } +.tip-item:nth-child(3) { animation-delay: 0.7s; } +.tip-item:nth-child(4) { animation-delay: 0.8s; } +.tip-item:nth-child(5) { animation-delay: 0.9s; } diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..3c285d5 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,283 @@ +// pages/index/index.js + +const app = getApp() +const userManager = require('../../utils/userManager.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + userInfo: null, + features: [ + { + id: 1, + name: '启思AI', + icon: '🤖', + desc: '启迪思维,智慧学习助手', + path: '/pages/ai-assistant/ai-assistant', + color: '#6C5CE7', + badge: 'AI', + badgeColor: '#A29BFE' + }, + { + id: 2, + name: '学习数据', + icon: '📊', + desc: '可视化数据分析看板', + path: '/pages/dashboard/dashboard', + color: '#667eea', + badge: 'NEW', + badgeColor: '#4CAF50' + }, + { + id: 3, + name: '课程中心', + icon: '📚', + desc: '海量课程资源,精准筛选', + path: '/pages/courses/courses', + color: '#4A90E2', + badge: 'HOT', + badgeColor: '#FF5252' + }, + { + id: 4, + name: '学科论坛', + icon: '💬', + desc: '学术交流,思维碰撞', + path: '/pages/forum/forum', + color: '#50C878' + }, + { + id: 5, + name: '学习工具', + icon: '🛠️', + desc: 'GPA计算、课表、倒计时', + path: '/pages/tools/tools', + color: '#9B59B6' + }, + { + id: 6, + name: '个人中心', + icon: '👤', + desc: '个性设置,数据管理', + path: '/pages/my/my', + color: '#F39C12' + } + ], + notices: [ + '欢迎使用知芽小筑!', + '新增课程筛选功能,快来体验吧', + '学科论坛上线,与同学们一起交流学习' + ], + currentNotice: 0 + }, + + onLoad() { + // 获取用户信息 + this.getUserInfo() + + // 启动公告轮播 + this.startNoticeCarousel() + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('index') + + // 每次显示时刷新用户信息 + this.getUserInfo() + + // 更新自定义TabBar选中状态 + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ + selected: 0 + }) + } + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + + // 清除定时器 + if (this.noticeTimer) { + clearInterval(this.noticeTimer) + } + }, + + // 获取用户信息 + getUserInfo() { + const userInfo = userManager.getUserInfo() + if (userInfo && userInfo.isLogin) { + this.setData({ userInfo }) + } else { + // 未登录状态 + this.setData({ userInfo: null }) + } + }, + + // 退出登录 + onLogout() { + wx.showModal({ + title: '退出登录', + content: '确定要退出登录吗?', + confirmText: '退出', + cancelText: '取消', + confirmColor: '#FF5252', + success: (res) => { + if (res.confirm) { + // 清除用户信息 + userManager.clearUserInfo() + this.setData({ userInfo: null }) + + wx.showToast({ + title: '已退出登录', + icon: 'success' + }) + + // 震动反馈 + wx.vibrateShort({ + type: 'light' + }) + } + } + }) + }, + + // 微信授权登录 + onLogin() { + const that = this + + // 显示加载提示 + wx.showLoading({ + title: '正在登录...', + mask: true + }) + + // 获取用户信息 + wx.getUserProfile({ + desc: '完善用户资料', + lang: 'zh_CN', + success: (res) => { + console.log('获取用户信息成功', res) + + // 使用 userManager 保存用户信息 + const userInfo = { + nickname: res.userInfo.nickName, + avatar: res.userInfo.avatarUrl, + gender: res.userInfo.gender, + country: res.userInfo.country, + province: res.userInfo.province, + city: res.userInfo.city, + isLogin: true, + loginTime: new Date().getTime() + } + + // 保存用户信息(会自动处理字段兼容性) + userManager.saveUserInfo(userInfo) + that.setData({ userInfo }) + + console.log('登录成功,保存的用户信息:', userInfo) + + // 隐藏加载提示 + wx.hideLoading() + + // 显示成功提示 + wx.showToast({ + title: `欢迎,${userInfo.nickname}`, + icon: 'success', + duration: 2000 + }) + + // 震动反馈 + wx.vibrateShort({ + type: 'light' + }) + }, + fail: (err) => { + console.error('获取用户信息失败', err) + + // 隐藏加载提示 + wx.hideLoading() + + // 显示错误提示 + wx.showModal({ + title: '登录提示', + content: '登录已取消,部分功能可能受限', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#667eea' + }) + } + }) + }, + + // 公告轮播 + startNoticeCarousel() { + this.noticeTimer = setInterval(() => { + const { currentNotice, notices } = this.data + this.setData({ + currentNotice: (currentNotice + 1) % notices.length + }) + }, 3000) + }, + + // 功能卡片点击 + onFeatureClick(e) { + console.log('功能卡片被点击', e) + const { path } = e.currentTarget.dataset + console.log('跳转路径:', path) + + if (!path) { + wx.showToast({ + title: '路径配置错误', + icon: 'none' + }) + return + } + + // TabBar 页面列表 + const tabBarPages = [ + '/pages/index/index', + '/pages/courses/courses', + '/pages/tools/tools', + '/pages/forum/forum', + '/pages/my/my' + ] + + // 判断是否为 TabBar 页面 + if (tabBarPages.includes(path)) { + wx.switchTab({ + url: path, + success: () => { + console.log('切换TabBar成功:', path) + }, + fail: (err) => { + console.error('TabBar切换失败:', err) + wx.showToast({ + title: '页面切换失败', + icon: 'none' + }) + } + }) + } else { + wx.navigateTo({ + url: path, + success: () => { + console.log('跳转成功:', path) + }, + fail: (err) => { + console.error('页面跳转失败:', err) + wx.showToast({ + title: '页面跳转失败', + icon: 'none' + }) + } + }) + } + } +}) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..c25eb58 --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "知芽小筑" +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..1f20d69 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + 📢 + + + {{item}} + + + + + + + + 功能中心 + + + + + + + + + + {{item.icon}} + + {{item.badge}} + + + + {{item.name}} + {{item.desc}} + + + + 立即使用 + + + + + + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..e408be5 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,643 @@ +/* pages/index/index.wxss */ +.container { + padding: 0; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + animation: gradient-shift 15s ease infinite; + /* 底部留出TabBar的空间 */ + padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx); +} + +@keyframes gradient-shift { + 0%, 100% { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + 50% { + background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); + } +} + +/* 头部区域 */ +.header { + padding: 60rpx 30rpx 40rpx; + background: transparent; + position: relative; + overflow: hidden; +} + +.header::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); + animation: float 20s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0) rotate(0deg); + } + 50% { + transform: translate(-20rpx, -20rpx) rotate(180deg); + } +} + +.welcome-section { + color: #ffffff; + position: relative; + z-index: 1; +} + +/* 已登录状态 */ +.user-info { + display: flex; + align-items: center; + gap: 20rpx; + animation: slideInLeft 0.6s ease-out; +} + +.user-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 60rpx; + border: 4rpx solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2); +} + +.user-content { + flex: 1; +} + +.logout-btn { + width: 70rpx; + height: 70rpx; + border-radius: 35rpx; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10rpx); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + border: 2rpx solid rgba(255, 255, 255, 0.3); +} + +.logout-btn:active { + background: rgba(255, 255, 255, 0.3); + transform: scale(0.95); +} + +.logout-icon { + font-size: 36rpx; +} + +/* 未登录状态 */ +.login-section { + padding: 20rpx 0; + animation: slideInUp 0.6s ease-out; +} + +.login-card { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%); + backdrop-filter: blur(20rpx); + border-radius: 30rpx; + padding: 50rpx 40rpx; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15); + position: relative; + overflow: hidden; +} + +.login-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(102, 126, 234, 0.08) 0%, transparent 70%); + animation: rotate-slow 30s linear infinite; +} + +@keyframes rotate-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.login-header { + text-align: center; + margin-bottom: 40rpx; + position: relative; + z-index: 1; +} + +.header-icon { + font-size: 80rpx; + display: block; + margin-bottom: 20rpx; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10rpx); } +} + +.header-title { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #667eea; + margin-bottom: 10rpx; + letter-spacing: 2rpx; +} + +.header-subtitle { + display: block; + font-size: 22rpx; + color: #999; + font-style: italic; +} + +.login-prompt { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 35rpx; + padding: 25rpx; + background: linear-gradient(135deg, #F0F4FF 0%, #E8EFFF 100%); + border-radius: 20rpx; + border-left: 6rpx solid #667eea; + position: relative; + z-index: 1; +} + +.prompt-icon { + font-size: 56rpx; + animation: wave 2s ease-in-out infinite; + flex-shrink: 0; +} + +@keyframes wave { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-20deg); } + 75% { transform: rotate(20deg); } +} + +.prompt-content { + flex: 1; +} + +.prompt-text { + display: block; + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; +} + +.prompt-desc { + display: block; + font-size: 24rpx; + color: #666; +} + +.login-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-radius: 25rpx; + padding: 0; + height: 130rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 35rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 15rpx 40rpx rgba(102, 126, 234, 0.35); + border: none; + margin-bottom: 35rpx; + position: relative; + z-index: 1; + overflow: hidden; + transition: all 0.3s ease; +} + +.login-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + transition: left 0.6s ease; +} + +.login-btn:active::before { + left: 100%; +} + +.login-btn::after { + border: none; +} + +.login-btn:active { + transform: scale(0.98); + box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4); +} + +.btn-content { + display: flex; + align-items: center; + gap: 20rpx; +} + +.login-icon { + font-size: 44rpx; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.btn-text-group { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.login-text { + font-size: 32rpx; + font-weight: bold; + display: block; +} + +.login-subtext { + font-size: 22rpx; + opacity: 0.9; + display: block; + margin-top: 4rpx; +} + +.arrow-icon { + font-size: 40rpx; + font-weight: bold; + animation: arrow-move 1.5s ease-in-out infinite; +} + +@keyframes arrow-move { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(8rpx); } +} + +.login-features { + display: flex; + justify-content: space-around; + margin-bottom: 30rpx; + padding: 0 20rpx; + position: relative; + z-index: 1; +} + +.feature-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 10rpx; +} + +.feature-icon { + font-size: 36rpx; + display: block; +} + +.feature-text { + font-size: 22rpx; + color: #666; + font-weight: 500; +} + +.login-tip { + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + padding: 20rpx; + background: rgba(102, 126, 234, 0.05); + border-radius: 15rpx; + position: relative; + z-index: 1; +} + +.tip-icon { + font-size: 28rpx; + color: #667eea; +} + +.tip-text { + font-size: 22rpx; + color: #999; +} + +.greeting { + display: flex; + align-items: baseline; + margin-bottom: 15rpx; +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-30rpx); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.greeting-text { + font-size: 32rpx; + margin-right: 10rpx; + opacity: 0.95; +} + +.greeting-name { + font-size: 40rpx; + font-weight: bold; + text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); +} + +.slogan { + font-size: 28rpx; + opacity: 0.9; + letter-spacing: 2rpx; + animation: slideInLeft 0.6s ease-out 0.2s both; +} + +/* 公告栏 */ +.notice-bar { + display: flex; + align-items: center; + background: linear-gradient(135deg, #FFF9E6 0%, #FFF3CD 100%); + padding: 20rpx 30rpx; + margin: 0 30rpx 30rpx; + border-radius: 16rpx; + box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.2); + animation: slideInDown 0.6s ease-out 0.3s both; +} + +@keyframes slideInDown { + from { + opacity: 0; + transform: translateY(-20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.notice-icon { + font-size: 32rpx; + margin-right: 15rpx; + animation: bell-ring 2s ease-in-out infinite; +} + +@keyframes bell-ring { + 0%, 100% { + transform: rotate(0deg); + } + 10%, 30% { + transform: rotate(-10deg); + } + 20%, 40% { + transform: rotate(10deg); + } + 50% { + transform: rotate(0deg); + } +} + +.notice-swiper { + flex: 1; + height: 40rpx; +} + +.notice-text { + font-size: 26rpx; + color: #856404; + line-height: 40rpx; + font-weight: 500; +} + +/* 功能区 */ +.features-section { + background-color: #f8f9fa; + border-radius: 30rpx 30rpx 0 0; + padding: 40rpx 30rpx; + min-height: calc(100vh - 400rpx); + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.08); + animation: slideInUp 0.6s ease-out 0.4s both; +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.section-title { + margin-bottom: 30rpx; +} + +.title-text { + font-size: 36rpx; + font-weight: bold; + color: #333333; + position: relative; + display: inline-block; +} + +.title-line { + width: 60rpx; + height: 6rpx; + background: linear-gradient(to right, #667eea, #764ba2); + border-radius: 3rpx; + margin-top: 10rpx; + animation: expand 0.6s ease-out; +} + +@keyframes expand { + from { + width: 0; + } + to { + width: 60rpx; + } +} + +.features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 25rpx; +} + +.feature-card { + background: #ffffff; + border-radius: 25rpx; + padding: 0; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.08); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + animation: fadeInScale 0.6s ease-out both; + display: flex; + flex-direction: column; +} + +.card-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transition: opacity 0.3s ease; +} + +.feature-card:active .card-bg { + opacity: 0.5; +} + +@keyframes fadeInScale { + from { + opacity: 0; + transform: scale(0.9) translateY(20rpx); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.feature-card:nth-child(1) { animation-delay: 0.1s; } +.feature-card:nth-child(2) { animation-delay: 0.15s; } +.feature-card:nth-child(3) { animation-delay: 0.2s; } +.feature-card:nth-child(4) { animation-delay: 0.25s; } +.feature-card:nth-child(5) { animation-delay: 0.3s; } +.feature-card:nth-child(6) { animation-delay: 0.35s; } + +.feature-card:active { + transform: translateY(-8rpx) scale(1.02); + box-shadow: 0 20rpx 50rpx rgba(102, 126, 234, 0.25); +} + +.feature-header { + position: relative; + z-index: 1; + padding: 30rpx 25rpx 20rpx; + display: flex; + align-items: flex-start; + justify-content: space-between; +} + +.feature-icon { + width: 90rpx; + height: 90rpx; + border-radius: 22rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; + box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15); + transition: transform 0.3s ease; +} + +.feature-card:active .feature-icon { + transform: scale(1.1) rotate(-5deg); +} + +.icon-text { + font-size: 50rpx; + filter: drop-shadow(0 2rpx 4rpx rgba(0,0,0,0.1)); +} + +.badge { + position: absolute; + top: 25rpx; + right: 25rpx; + padding: 6rpx 16rpx; + border-radius: 20rpx; + font-size: 20rpx; + color: #fff; + font-weight: bold; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2); + animation: badge-pulse 2s ease-in-out infinite; +} + +@keyframes badge-pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +.feature-info { + position: relative; + z-index: 1; + padding: 0 25rpx 20rpx; + flex: 1; +} + +.feature-name { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 12rpx; + position: relative; +} + +.feature-desc { + font-size: 24rpx; + color: #999; + line-height: 1.6; +} + +.card-footer { + position: relative; + z-index: 1; + padding: 20rpx 25rpx; + border-top: 1rpx solid rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(0, 0, 0, 0.02); +} + +.footer-text { + font-size: 24rpx; + color: #667eea; + font-weight: 600; +} + +.footer-arrow { + font-size: 28rpx; + color: #667eea; + font-weight: bold; + transition: transform 0.3s ease; +} + +.feature-card:active .footer-arrow { + transform: translateX(6rpx); +} diff --git a/pages/my/my.js b/pages/my/my.js new file mode 100644 index 0000000..8092dff --- /dev/null +++ b/pages/my/my.js @@ -0,0 +1,726 @@ +// pages/my/my.js +const userManager = require('../../utils/userManager.js') +const learningTracker = require('../../utils/learningTracker.js') + +Page({ + data: { + userInfo: null, + stats: { + favoriteCourses: 0, + myPosts: 0, + myComments: 0 + }, + menuList: [ + { + id: 1, + icon: '✏️', + title: '编辑资料', + desc: '修改昵称和头像', + arrow: true, + type: 'edit' + }, + { + id: 2, + icon: '❤️', + title: '我的收藏', + desc: '收藏的课程和帖子', + arrow: true, + type: 'favorite' + }, + { + id: 3, + icon: '📝', + title: '我的帖子', + desc: '查看发布的帖子', + arrow: true, + type: 'posts' + }, + { + id: 4, + icon: '🔔', + title: '消息通知', + desc: '系统消息和互动提醒', + arrow: true, + type: 'notification' + }, + { + id: 5, + icon: '⚙️', + title: '通用设置', + desc: '隐私、通知等设置', + arrow: true, + type: 'settings' + }, + { + id: 6, + icon: '❓', + title: '帮助与反馈', + desc: '使用指南和问题反馈', + arrow: true, + type: 'help' + }, + { + id: 7, + icon: '📊', + title: '数据统计', + desc: '学习数据和使用记录', + arrow: true, + type: 'statistics' + }, + { + id: 8, + icon: '🚪', + title: '退出登录', + desc: '退出当前账号', + arrow: false, + type: 'logout', + danger: true + } + ], + showEditDialog: false, + editNickname: '' + }, + + onLoad() { + this.loadUserInfo() + this.loadStats() + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('tools') + + this.loadUserInfo() + this.loadStats() + + // 更新自定义TabBar选中状态 + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ + selected: 4 + }) + } + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 获取用户信息 + loadUserInfo() { + const userInfo = userManager.getUserInfo() + this.setData({ + userInfo: userInfo + }) + console.log('加载用户信息:', userInfo) + }, + + // 点击头像上传 + onChooseAvatar() { + const that = this + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success(res) { + const tempFilePath = res.tempFilePaths[0] + + // 更新头像 + userManager.updateAvatar(tempFilePath) + + // 更新页面显示 + const updatedUserInfo = userManager.getUserInfo() + that.setData({ + userInfo: updatedUserInfo + }) + + console.log('头像更新成功,保存的数据:', updatedUserInfo) + + wx.showToast({ + title: '头像更新成功', + icon: 'success' + }) + }, + fail() { + wx.showToast({ + title: '取消选择', + icon: 'none' + }) + } + }) + }, + + // 显示编辑昵称对话框 + showEditNicknameDialog() { + const { userInfo } = this.data + this.setData({ + showEditDialog: true, + editNickname: userInfo.nickname || '' + }) + }, + + // 关闭编辑对话框 + closeEditDialog() { + this.setData({ + showEditDialog: false, + editNickname: '' + }) + }, + + // 昵称输入 + onNicknameInput(e) { + this.setData({ + editNickname: e.detail.value + }) + }, + + // 保存昵称 + saveNickname() { + const { editNickname } = this.data + + if (!editNickname.trim()) { + wx.showToast({ + title: '请输入昵称', + icon: 'none' + }) + return + } + + if (editNickname.length > 10) { + wx.showToast({ + title: '昵称最多10个字', + icon: 'none' + }) + return + } + + // 更新昵称 + userManager.updateNickname(editNickname.trim()) + + // 更新页面显示 + const updatedUserInfo = userManager.getUserInfo() + this.setData({ + userInfo: updatedUserInfo, + showEditDialog: false, + editNickname: '' + }) + + console.log('昵称更新成功,保存的数据:', updatedUserInfo) + + wx.showToast({ + title: '昵称修改成功', + icon: 'success' + }) + }, + + // 加载统计数据 + loadStats() { + const userInfo = userManager.getUserInfo() + const favoriteCourses = wx.getStorageSync('favoriteCourses') || [] + const forumPosts = wx.getStorageSync('forumPosts') || [] + + // 获取当前用户的昵称 + const currentUserName = userInfo.nickname || '同学' + + // 统计我的帖子 + const myPosts = forumPosts.filter(post => { + return post.author === currentUserName || post.author === '我' + }) + + // 统计我的评论(从帖子详情中统计) + let totalComments = 0 + forumPosts.forEach(post => { + if (post.commentList && Array.isArray(post.commentList)) { + const myComments = post.commentList.filter(comment => { + return comment.author === currentUserName || comment.author === '我' + }) + totalComments += myComments.length + } + }) + + this.setData({ + stats: { + favoriteCourses: favoriteCourses.length, + myPosts: myPosts.length, + myComments: totalComments + } + }) + + console.log('统计数据更新:', { + 用户名: currentUserName, + 收藏课程: favoriteCourses.length, + 发布帖子: myPosts.length, + 评论数: totalComments + }) + }, + + // 菜单点击 + onMenuClick(e) { + const { id } = e.currentTarget.dataset + const menuItem = this.data.menuList.find(item => item.id === id) + + if (!menuItem) return + + switch(menuItem.type) { + case 'edit': + // 编辑资料 + this.showEditNicknameDialog() + break + + case 'favorite': + // 我的收藏 + this.showMyFavorites() + break + + case 'posts': + // 我的帖子 + this.showMyPosts() + break + + case 'notification': + // 消息通知 + this.showNotifications() + break + + case 'settings': + // 通用设置 + this.showSettings() + break + + case 'help': + // 帮助与反馈 + this.showHelp() + break + + case 'statistics': + // 数据统计 + this.showStatistics() + break + + case 'logout': + // 退出登录 + this.handleLogout() + break + + default: + wx.showToast({ + title: '功能开发中', + icon: 'none' + }) + } + }, + + // 我的收藏 + showMyFavorites() { + const favoriteCourseIds = wx.getStorageSync('favoriteCourses') || [] + const favoritePosts = wx.getStorageSync('favoritePosts') || [] + + // 从课程数据中获取收藏的课程详细信息 + const { coursesData } = require('../../utils/data.js') + const favoriteCourses = coursesData.filter(course => + favoriteCourseIds.includes(course.id) + ) + + const totalFavorites = favoriteCourses.length + favoritePosts.length + + if (totalFavorites === 0) { + wx.showModal({ + title: '我的收藏', + content: '暂无收藏内容\n\n可以收藏:\n• 喜欢的课程\n• 有用的论坛帖子', + showCancel: false, + confirmText: '去看看', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/courses/courses' }) + } + } + }) + } else { + wx.showActionSheet({ + itemList: ['查看收藏的课程', '查看收藏的帖子', '查看所有收藏', '取消'], + success: (res) => { + if (res.tapIndex === 0) { + // 查看课程收藏 + this.showFavoriteCoursesList(favoriteCourses) + } else if (res.tapIndex === 1) { + // 查看帖子收藏 + this.showFavoritePostsList(favoritePosts) + } else if (res.tapIndex === 2) { + // 查看所有收藏 + this.showAllFavorites(favoriteCourses, favoritePosts) + } + } + }) + } + }, + + // 显示所有收藏概览 + showAllFavorites(favoriteCourses, favoritePosts) { + let contentLines = [] + + // 课程收藏 + if (favoriteCourses.length > 0) { + contentLines.push(`📚 收藏课程 (${favoriteCourses.length}门)`) + favoriteCourses.slice(0, 5).forEach((course, index) => { + contentLines.push(`${index + 1}. ${course.name} - ${course.teacher}`) + }) + if (favoriteCourses.length > 5) { + contentLines.push(` ...还有${favoriteCourses.length - 5}门课程`) + } + contentLines.push('') + } + + // 帖子收藏 - 过滤有效数据 + const validPosts = favoritePosts.filter(post => post && post.title) + if (validPosts.length > 0) { + contentLines.push(`📝 收藏帖子 (${validPosts.length}条)`) + validPosts.slice(0, 5).forEach((post, index) => { + contentLines.push(`${index + 1}. ${post.title}`) + }) + if (validPosts.length > 5) { + contentLines.push(` ...还有${validPosts.length - 5}条帖子`) + } + } + + wx.showModal({ + title: `⭐ 我的收藏`, + content: contentLines.join('\n'), + showCancel: false, + confirmText: '知道了', + confirmColor: '#667eea' + }) + }, + + // 显示收藏课程列表 + showFavoriteCoursesList(favoriteCourses) { + if (favoriteCourses.length === 0) { + wx.showModal({ + title: '收藏的课程', + content: '还没有收藏任何课程\n去课程中心看看吧!', + showCancel: false, + confirmText: '去看看', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/courses/courses' }) + } + } + }) + return + } + + const courseTitles = favoriteCourses.map(course => + `${course.name} - ${course.teacher}` + ) + + wx.showActionSheet({ + itemList: courseTitles, + success: (res) => { + const selectedCourse = favoriteCourses[res.tapIndex] + // 跳转到课程详情 + wx.navigateTo({ + url: `/pages/course-detail/course-detail?id=${selectedCourse.id}` + }) + } + }) + }, + + // 显示收藏帖子列表 + showFavoritePostsList(favoritePosts) { + console.log('收藏的帖子数据:', favoritePosts) + console.log('收藏的帖子数量:', favoritePosts.length) + + if (favoritePosts.length === 0) { + wx.showModal({ + title: '收藏的帖子', + content: '还没有收藏任何帖子\n去论坛看看吧!', + showCancel: false, + confirmText: '去看看', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/forum/forum' }) + } + } + }) + return + } + + // 确保每个帖子都有title和id + const validPosts = favoritePosts.filter(post => post && post.title && post.id) + + if (validPosts.length === 0) { + wx.showModal({ + title: '收藏的帖子', + content: '收藏数据异常\n请重新收藏帖子', + showCancel: false, + confirmText: '知道了', + confirmColor: '#667eea' + }) + return + } + + const postTitles = validPosts.map(post => post.title) + + wx.showActionSheet({ + itemList: postTitles, + success: (res) => { + const selectedPost = validPosts[res.tapIndex] + console.log('选中的帖子:', selectedPost) + // 跳转到帖子详情 + wx.navigateTo({ + url: `/pages/forum-detail/forum-detail?id=${selectedPost.id}` + }) + } + }) + }, + + // 我的帖子 + showMyPosts() { + const userInfo = userManager.getUserInfo() + const currentUserName = userInfo.nickname || '同学' + const forumPosts = wx.getStorageSync('forumPosts') || [] + + const myPosts = forumPosts.filter(post => { + return post.author === currentUserName || post.author === '我' + }) + + console.log('我的帖子查询', { + 当前用户名: currentUserName, + 总帖子数: forumPosts.length, + 我的帖子数: myPosts.length, + 我的帖子: myPosts.map(p => ({ 标题: p.title, 作者: p.author })) + }) + + if (myPosts.length === 0) { + wx.showModal({ + title: '我的帖子', + content: '还没有发布过帖子\n去论坛分享你的想法吧!', + showCancel: false, + confirmText: '去发帖', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + wx.switchTab({ + url: '/pages/forum/forum' + }) + } + } + }) + } else { + // 显示帖子列表供用户选择 + const postTitles = myPosts.map(post => { + const commentCount = post.comments || 0 + const likeCount = post.likes || 0 + return `${post.title} (💬${commentCount} ❤️${likeCount})` + }) + + wx.showActionSheet({ + itemList: postTitles, + success: (res) => { + const selectedPost = myPosts[res.tapIndex] + // 跳转到帖子详情 + wx.navigateTo({ + url: `/pages/forum-detail/forum-detail?id=${selectedPost.id}` + }) + } + }) + } + }, + + // 消息通知 + showNotifications() { + wx.showModal({ + title: '💌 消息通知', + content: '暂无新消息\n\n系统会在这里提醒您:\n• 帖子的点赞和评论\n• 课程更新通知\n• 系统公告', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#667eea' + }) + }, + + // 通用设置 + showSettings() { + const settingsContent = [ + '⚙️ 通用设置', + '', + '✓ 消息推送:已开启', + '✓ 隐私保护:已开启', + '✓ 缓存管理:自动清理', + '✓ 深色模式:跟随系统', + '', + '点击确定返回' + ].join('\n') + + wx.showModal({ + title: '设置中心', + content: settingsContent, + showCancel: false, + confirmText: '确定', + confirmColor: '#667eea' + }) + }, + + // 帮助与反馈 + showHelp() { + wx.showModal({ + title: '📖 帮助中心', + content: '使用指南:\n\n1. 课程中心:浏览和收藏课程\n2. 学科论坛:发帖交流学习\n3. 学习工具:GPA计算等工具\n4. 个人中心:管理个人信息\n\n遇到问题?请联系管理员', + showCancel: true, + cancelText: '返回', + confirmText: '联系我们', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + this.showContactInfo() + } + } + }) + }, + + // 显示联系方式 + showContactInfo() { + wx.showModal({ + title: '📞 联系我们', + content: '客服邮箱:\n19354618812@163.com\n\n客服电话:\n19354618812\n\n工作时间:\n周一至周五 9:00-18:00\n\n欢迎您的咨询和建议!', + showCancel: true, + cancelText: '关闭', + confirmText: '复制邮箱', + confirmColor: '#667eea', + success: (res) => { + if (res.confirm) { + wx.setClipboardData({ + data: '19354618812@163.com', + success: () => { + wx.showToast({ + title: '邮箱已复制', + icon: 'success' + }) + } + }) + } + } + }) + }, + + // 数据统计 + showStatistics() { + const { stats } = this.data + const userInfo = userManager.getUserInfo() + const currentUserName = userInfo.nickname || '同学' + const forumPosts = wx.getStorageSync('forumPosts') || [] + + // 获取我的评论详情 + let myCommentsList = [] + forumPosts.forEach(post => { + if (post.commentList && Array.isArray(post.commentList)) { + post.commentList.forEach(comment => { + if (comment.author === currentUserName || comment.author === '我') { + myCommentsList.push({ + postTitle: post.title, + content: comment.content, + time: comment.time + }) + } + }) + } + }) + + // 构建详细内容 + let contentLines = [ + '📊 我的数据', + '', + `📚 收藏课程:${stats.favoriteCourses} 门`, + `📝 发布帖子:${stats.myPosts} 条`, + `💬 评论数量:${stats.myComments} 次`, + '' + ] + + // 显示最近的评论(最多3条) + if (myCommentsList.length > 0) { + contentLines.push('最近评论:') + myCommentsList.slice(0, 3).forEach((comment, index) => { + const shortContent = comment.content.length > 20 + ? comment.content.substring(0, 20) + '...' + : comment.content + contentLines.push(`${index + 1}. 在《${comment.postTitle}》中评论`) + contentLines.push(` "${shortContent}"`) + }) + if (myCommentsList.length > 3) { + contentLines.push(` ...还有${myCommentsList.length - 3}条评论`) + } + } else { + contentLines.push('暂无评论记录') + } + + contentLines.push('') + contentLines.push('持续学习,不断进步!') + + wx.showModal({ + title: '数据统计', + content: contentLines.join('\n'), + showCancel: false, + confirmText: '继续加油', + confirmColor: '#667eea' + }) + }, + + // 退出登录 + handleLogout() { + if (!userManager.isUserLogin()) { + wx.showToast({ + title: '您还未登录', + icon: 'none' + }) + return + } + + wx.showModal({ + title: '退出登录', + content: '确定要退出登录吗?\n退出后部分功能将无法使用', + confirmText: '退出', + cancelText: '取消', + confirmColor: '#FF5252', + success: (res) => { + if (res.confirm) { + // 清除登录状态 + userManager.clearUserInfo() + + // 更新页面数据 + this.setData({ + userInfo: userManager.getUserInfo() + }) + + wx.showToast({ + title: '已退出登录', + icon: 'success', + duration: 2000 + }) + + // 震动反馈 + wx.vibrateShort({ + type: 'light' + }) + + // 1秒后跳转到首页 + setTimeout(() => { + wx.switchTab({ + url: '/pages/index/index' + }) + }, 1500) + } + } + }) + }, + + // 阻止事件冒泡 + doNothing() { + // 空函数,防止点击对话框内容时关闭对话框 + } +}) diff --git a/pages/my/my.json b/pages/my/my.json new file mode 100644 index 0000000..4ec55be --- /dev/null +++ b/pages/my/my.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "我的" +} diff --git a/pages/my/my.wxml b/pages/my/my.wxml new file mode 100644 index 0000000..fcda648 --- /dev/null +++ b/pages/my/my.wxml @@ -0,0 +1,90 @@ + + + + + + + + + 📷 + + + + + ✏️ + + + + + + + {{stats.favoriteCourses}} + 收藏课程 + + + + {{stats.myPosts}} + 发布帖子 + + + + {{stats.myComments}} + 评论数 + + + + + + + + {{item.icon}} + + {{item.title}} + {{item.desc}} + + + + + + + + 关于小程序 + + 版本号:v2.0.0 + 开发团队:知芽小筑工作组 + 主题:知芽小筑,智启未来 + + + + + + + + 修改昵称 + + + + + + + + + + + + diff --git a/pages/my/my.wxss b/pages/my/my.wxss new file mode 100644 index 0000000..7516dc1 --- /dev/null +++ b/pages/my/my.wxss @@ -0,0 +1,421 @@ +/* pages/my/my.wxss */ +.container { + min-height: 100vh; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + animation: fadeIn 0.5s ease-out; + /* 底部留出TabBar的空间 */ + padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx); +} + +/* 用户卡片 */ +.user-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 65rpx 30rpx 45rpx; + margin-bottom: 30rpx; + position: relative; + overflow: hidden; + animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.user-card::before { + content: ''; + position: absolute; + top: -50%; + right: -25%; + width: 400rpx; + height: 400rpx; + background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%); + border-radius: 50%; + animation: float 8s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(-30rpx, 30rpx) scale(1.1); } +} + +.user-header { + display: flex; + align-items: center; + margin-bottom: 45rpx; + position: relative; +} + +.avatar-wrap { + position: relative; + margin-right: 28rpx; +} + +.avatar { + width: 130rpx; + height: 130rpx; + border-radius: 50%; + background-color: #ffffff; + border: 5rpx solid rgba(255, 255, 255, 0.4); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.avatar-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.avatar-wrap:active .avatar-mask { + background-color: rgba(0, 0, 0, 0.3); +} + +.avatar-tip { + font-size: 48rpx; + opacity: 0; + transition: all 0.3s ease; +} + +.avatar-wrap:active .avatar-tip { + opacity: 1; +} + +.user-info { + flex: 1; + color: #ffffff; + position: relative; +} + +.nickname { + font-size: 40rpx; + font-weight: bold; + margin-bottom: 12rpx; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); +} + +.user-desc { + font-size: 26rpx; + opacity: 0.95; + letter-spacing: 1rpx; +} + +.edit-btn { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10rpx); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.edit-btn:active { + transform: scale(1.1) rotate(15deg); + background-color: rgba(255, 255, 255, 0.3); +} + +.edit-icon { + font-size: 36rpx; +} + +/* 统计数据 */ +.stats-row { + display: flex; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 20rpx; + padding: 35rpx 0; + backdrop-filter: blur(15rpx); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); + position: relative; +} + +.stat-item { + flex: 1; + text-align: center; + color: #ffffff; + transition: all 0.3s ease; + cursor: pointer; +} + +.stat-item:active { + transform: scale(1.1); +} + +.stat-value { + font-size: 48rpx; + font-weight: bold; + margin-bottom: 12rpx; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); + animation: pulse 2s ease-in-out infinite; +} + +.stat-label { + font-size: 24rpx; + opacity: 0.95; + letter-spacing: 1rpx; +} + +.stat-divider { + width: 2rpx; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); +} + +/* 菜单区域 */ +.menu-section { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + margin: 0 30rpx 30rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + animation: slideInUp 0.6s ease-out 0.2s both; +} + +.menu-item { + display: flex; + align-items: center; + padding: 35rpx; + border-bottom: 2rpx solid rgba(0, 0, 0, 0.03); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.menu-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6rpx; + background: linear-gradient(to bottom, #667eea, #764ba2); + transform: scaleY(0); + transition: transform 0.3s ease; +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-item:active { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + transform: translateX(8rpx); +} + +.menu-item:active::before { + transform: scaleY(1); +} + +.menu-icon { + font-size: 48rpx; + margin-right: 28rpx; + transition: all 0.3s ease; +} + +.menu-item:active .menu-icon { + transform: scale(1.2) rotate(10deg); +} + +.menu-content { + flex: 1; +} + +.menu-title { + font-size: 32rpx; + color: #333333; + font-weight: 600; + margin-bottom: 10rpx; +} + +.menu-desc { + font-size: 24rpx; + color: #999999; + opacity: 0.8; +} + +.menu-arrow { + font-size: 32rpx; + color: #CCCCCC; + font-weight: 300; + transition: all 0.3s ease; +} + +.menu-item:active .menu-arrow { + transform: translateX(8rpx); + color: #667eea; +} + +/* 关于信息 */ +.about-section { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 20rpx; + margin: 0 30rpx; + padding: 35rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + animation: slideInUp 0.6s ease-out 0.3s both; +} + +.about-title { + font-size: 30rpx; + font-weight: bold; + color: #333333; + margin-bottom: 24rpx; + position: relative; + padding-left: 20rpx; +} + +.about-title::before { + content: '📚'; + position: absolute; + left: 0; + top: 0; + font-size: 32rpx; +} + +.about-content { + font-size: 26rpx; + color: #666666; + line-height: 2.2; + padding-left: 20rpx; +} + +.about-item { + margin-bottom: 14rpx; + position: relative; + padding-left: 24rpx; + animation: fadeIn 0.8s ease-out; +} + +.about-item::before { + content: '•'; + position: absolute; + left: 0; + color: #667eea; + font-weight: bold; + font-size: 32rpx; + line-height: 1.5; +} + +.about-item:nth-child(1) { animation-delay: 0.4s; } +.about-item:nth-child(2) { animation-delay: 0.5s; } +.about-item:nth-child(3) { animation-delay: 0.6s; } + +/* 编辑对话框 */ +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + animation: fadeIn 0.3s ease-out; +} + +.modal-dialog { + width: 600rpx; + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-radius: 24rpx; + box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2); + animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 35rpx 35rpx 25rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.modal-title { + font-size: 36rpx; + font-weight: bold; + color: #333333; +} + +.modal-close { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + color: #999999; + border-radius: 50%; + transition: all 0.3s ease; +} + +.modal-close:active { + background-color: #f5f5f5; + transform: rotate(90deg); +} + +.modal-content { + padding: 35rpx; +} + +.modal-input { + width: 100%; + height: 88rpx; + padding: 0 28rpx; + background-color: #f8f9fa; + border: 2rpx solid #e9ecef; + border-radius: 16rpx; + font-size: 30rpx; + box-sizing: border-box; + transition: all 0.3s ease; +} + +.modal-input:focus { + background-color: #ffffff; + border-color: #667eea; + box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1); +} + +.modal-footer { + display: flex; + gap: 20rpx; + padding: 25rpx 35rpx 35rpx; +} + +.modal-btn { + flex: 1; + height: 88rpx; + line-height: 88rpx; + border-radius: 44rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + transition: all 0.3s ease; +} + +.cancel-btn { + background-color: #f5f5f5; + color: #666666; +} + +.cancel-btn:active { + background-color: #e8e8e8; + transform: scale(0.98); +} + +.confirm-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3); +} + +.confirm-btn:active { + transform: scale(0.98); + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} diff --git a/pages/post/post.js b/pages/post/post.js new file mode 100644 index 0000000..8fe5d25 --- /dev/null +++ b/pages/post/post.js @@ -0,0 +1,120 @@ +// pages/post/post.js +const { forumData } = require('../../utils/data.js') +const { showSuccess, showError } = require('../../utils/util.js') +const learningTracker = require('../../utils/learningTracker.js') + +// pages/post/post.js +const userManager = require('../../utils/userManager.js') + +Page({ + data: { + title: '', + content: '', + images: [], + categories: ['数学', '物理', '计算机', '英语', '其他'], + selectedCategory: 0 + }, + + onShow() { + // 开始跟踪学习时间 + learningTracker.onPageShow('forum') + }, + + onHide() { + // 停止跟踪学习时间 + learningTracker.onPageHide() + }, + + onUnload() { + // 记录学习时长 + learningTracker.onPageUnload() + }, + + // 标题输入 + onTitleInput(e) { + this.setData({ title: e.detail.value }) + }, + + // 内容输入 + onContentInput(e) { + this.setData({ content: e.detail.value }) + }, + + // 选择分类 + onCategoryChange(e) { + this.setData({ selectedCategory: parseInt(e.detail.value) }) + }, + + // 选择图片 + onChooseImage() { + const maxCount = 3 - this.data.images.length + + wx.chooseImage({ + count: maxCount, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePaths = res.tempFilePaths + const images = this.data.images.concat(tempFilePaths) + this.setData({ images }) + } + }) + }, + + // 删除图片 + onDeleteImage(e) { + const index = e.currentTarget.dataset.index + const images = this.data.images + images.splice(index, 1) + this.setData({ images }) + }, + + // 发布帖子 + onPublish() { + const { title, content, images, categories, selectedCategory } = this.data + + if (!title.trim()) { + showError('请输入标题') + return + } + + if (!content.trim()) { + showError('请输入内容') + return + } + + // 获取用户信息 + const userInfo = userManager.getUserInfo() + const userName = userInfo.nickname || '同学' + const userAvatar = userInfo.avatar || '/images/avatar-default.png' + + // 创建新帖子 + const newPost = { + id: Date.now(), + title: title.trim(), + content: content.trim(), + author: userName, + avatar: userAvatar, + category: categories[selectedCategory], + views: 0, + likes: 0, + comments: 0, + time: '刚刚', + images: images, // 保存图片数组 + isLiked: false, // 确保默认未点赞 + commentList: [] // 初始化评论列表 + } + + // 保存到本地存储 + let posts = wx.getStorageSync('forumPosts') || forumData + posts.unshift(newPost) + wx.setStorageSync('forumPosts', posts) + + showSuccess('发布成功') + + // 返回论坛列表 + setTimeout(() => { + wx.navigateBack() + }, 1000) + } +}) diff --git a/pages/post/post.json b/pages/post/post.json new file mode 100644 index 0000000..3c15189 --- /dev/null +++ b/pages/post/post.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "发布帖子" +} diff --git a/pages/post/post.wxml b/pages/post/post.wxml new file mode 100644 index 0000000..9e8ebd9 --- /dev/null +++ b/pages/post/post.wxml @@ -0,0 +1,82 @@ + + + + + + 选择分类 + + + {{categories[selectedCategory]}} ▼ + + + + + + + 标题 + + {{title.length}}/50 + + + + + 内容 +