This commit is contained in:
ChuXun
2025-10-19 20:28:31 +08:00
parent c81f8a8b03
commit eaab9a762a
100 changed files with 23416 additions and 0 deletions

185
utils/aiService.js Normal file
View File

@@ -0,0 +1,185 @@
/**
* AI服务 - DeepSeek API集成
* 提供智能对话、场景化提示词等功能
*/
const API_BASE_URL = 'https://api.deepseek.com';
const API_KEY = 'sk-0bdae24178904e5d9e59598cf4ecace6'; // 请替换为实际的API密钥
/**
* 场景化提示词模板
*/
const SCENARIO_PROMPTS = {
study_method: {
name: '学习方法咨询',
systemPrompt: '你是一位经验丰富的学习顾问,擅长根据学生的具体情况提供个性化的学习方法建议。请用简洁、实用的语言回答,每个建议都要具体可执行。'
},
course_advice: {
name: '课程建议',
systemPrompt: '你是一位专业的课程规划师,擅长根据学生的兴趣和目标推荐合适的课程。请提供具体的选课建议,包括课程难度、学习顺序等。'
},
post_summary: {
name: '帖子总结',
systemPrompt: '你是一位擅长信息提炼的助手,能够快速总结论坛帖子的核心内容。请用简洁的语言概括要点,突出关键信息。'
},
study_plan: {
name: '学习计划',
systemPrompt: '你是一位时间管理专家,擅长制定科学合理的学习计划。请根据学生的时间和目标,制定详细的、可执行的学习计划。'
},
problem_explain: {
name: '问题讲解',
systemPrompt: '你是一位耐心的老师,擅长用通俗易懂的方式讲解复杂问题。请分步骤讲解,确保学生能够理解每个环节。'
}
};
/**
* 调用DeepSeek Chat API
* @param {Array} messages - 消息历史数组
* @param {String} scenarioId - 场景ID(可选)
* @returns {Promise<String>} AI回复内容
*/
async function chat(messages, scenarioId = null) {
try {
// 构建请求消息
const requestMessages = [];
// 添加系统提示词(如果有场景)
if (scenarioId && SCENARIO_PROMPTS[scenarioId]) {
requestMessages.push({
role: 'system',
content: SCENARIO_PROMPTS[scenarioId].systemPrompt
});
} else {
// 默认系统提示词
requestMessages.push({
role: 'system',
content: '你是一位智能学习助手,名叫"启思AI",寓意"启迪思维,智慧学习"。你致力于帮助大学生更好地学习和成长,提供个性化的学习指导。请用友好、专业且富有启发性的语言回答问题,让学生在获得答案的同时也能学会独立思考。'
});
}
// 添加对话历史
messages.forEach(msg => {
requestMessages.push({
role: msg.role,
content: msg.content
});
});
// 调用API
return new Promise((resolve, reject) => {
wx.request({
url: `${API_BASE_URL}/chat/completions`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
data: {
model: 'deepseek-chat',
messages: requestMessages,
stream: false,
temperature: 0.7,
max_tokens: 2000
},
timeout: 30000,
success: (res) => {
if (res.statusCode === 200 && res.data.choices && res.data.choices.length > 0) {
const reply = res.data.choices[0].message.content;
resolve(reply);
} else {
reject(new Error('API返回格式错误'));
}
},
fail: (err) => {
reject(err);
}
});
});
} catch (error) {
console.error('AI服务调用失败:', error);
throw error;
}
}
/**
* 获取场景列表
* @returns {Array} 场景列表
*/
function getScenarios() {
return [
{
id: 'study_method',
name: '学习方法',
icon: '📚',
prompt: '你好!我想了解一些高效的学习方法,有什么建议吗?'
},
{
id: 'course_advice',
name: '课程建议',
icon: '🎯',
prompt: '我想请教一下选课方面的建议。'
},
{
id: 'post_summary',
name: '帖子总结',
icon: '📝',
prompt: '请帮我总结一下这个帖子的主要内容。'
},
{
id: 'study_plan',
name: '学习计划',
icon: '📅',
prompt: '能帮我制定一个学习计划吗?'
},
{
id: 'problem_explain',
name: '问题讲解',
icon: '💡',
prompt: '我有一个问题不太理解,能帮我讲解一下吗?'
}
];
}
/**
* 错误处理 - 返回友好的错误提示
* @param {Error} error - 错误对象
* @returns {String} 错误提示文本
*/
function getErrorMessage(error) {
console.error('AI服务错误:', error);
// 域名不在白名单
if (error.errMsg && error.errMsg.includes('url not in domain list')) {
return '⚠️ API域名未配置\n\n请在开发者工具中\n详情 → 本地设置 → 勾选"不校验合法域名"\n\n或在小程序后台添加:\nhttps://api.deepseek.com';
}
if (error.errMsg && error.errMsg.includes('timeout')) {
return '网络超时,请检查网络连接后重试 🌐';
}
if (error.errMsg && error.errMsg.includes('fail')) {
return '网络请求失败,请稍后重试 📡';
}
if (error.statusCode === 401) {
return 'API密钥无效,请联系管理员 🔑';
}
if (error.statusCode === 429) {
return '请求过于频繁,请稍后再试 ⏰';
}
if (error.statusCode === 500) {
return '服务器繁忙,请稍后重试 🔧';
}
return '抱歉,AI助手暂时无法回复,请稍后重试 😢';
}
module.exports = {
chat,
getScenarios,
getErrorMessage,
SCENARIO_PROMPTS
};

350
utils/analytics.js Normal file
View File

@@ -0,0 +1,350 @@
/**
* 数据统计和分析工具
*/
const { Storage } = require('./storage.js')
const { logger } = require('./logger.js')
class Analytics {
/**
* 获取学习统计数据
*/
static getStudyStats() {
const gpaCourses = Storage.get('gpaCourses', [])
const countdowns = Storage.get('countdowns', [])
const favoriteCourses = Storage.get('favoriteCourses', [])
const stats = {
// GPA统计
gpa: {
total: gpaCourses.length,
average: this._calculateAverageGPA(gpaCourses),
highest: this._getHighestScore(gpaCourses),
lowest: this._getLowestScore(gpaCourses),
trend: this._calculateGPATrend(gpaCourses)
},
// 课程统计
courses: {
total: favoriteCourses.length,
byCategory: this._groupByCategory(gpaCourses),
completionRate: this._calculateCompletionRate(gpaCourses)
},
// 考试倒计时
exams: {
total: countdowns.length,
upcoming: countdowns.filter(c => c.days > 0).length,
thisWeek: countdowns.filter(c => c.days <= 7 && c.days > 0).length
},
// 学习时长统计(模拟数据)
studyTime: {
today: 0,
week: 0,
month: 0
}
}
return stats
}
/**
* 计算平均GPA
*/
static _calculateAverageGPA(courses) {
if (courses.length === 0) return 0
let totalPoints = 0
let totalCredits = 0
courses.forEach(course => {
const gradePoint = this._scoreToGradePoint(course.score)
totalPoints += gradePoint * course.credit
totalCredits += course.credit
})
return totalCredits > 0 ? (totalPoints / totalCredits).toFixed(2) : 0
}
/**
* 分数转绩点
*/
static _scoreToGradePoint(score) {
if (score >= 60) {
return parseFloat((score / 10 - 5).toFixed(2))
}
return 0
}
/**
* 获取最高分
*/
static _getHighestScore(courses) {
if (courses.length === 0) return 0
return Math.max(...courses.map(c => c.score))
}
/**
* 获取最低分
*/
static _getLowestScore(courses) {
if (courses.length === 0) return 0
return Math.min(...courses.map(c => c.score))
}
/**
* 计算GPA趋势
*/
static _calculateGPATrend(courses) {
if (courses.length < 2) return []
// 按时间排序假设ID越大时间越新
const sorted = [...courses].sort((a, b) => a.id - b.id)
const trend = []
let runningTotal = 0
let runningCredits = 0
sorted.forEach(course => {
const gradePoint = this._scoreToGradePoint(course.score)
runningTotal += gradePoint * course.credit
runningCredits += course.credit
const gpa = runningCredits > 0 ? (runningTotal / runningCredits).toFixed(2) : 0
trend.push({
name: course.name,
gpa: parseFloat(gpa),
score: course.score
})
})
return trend
}
/**
* 按分类分组
*/
static _groupByCategory(courses) {
const grouped = {}
courses.forEach(course => {
const category = course.category || '其他'
if (!grouped[category]) {
grouped[category] = {
count: 0,
totalScore: 0,
avgScore: 0
}
}
grouped[category].count++
grouped[category].totalScore += course.score
})
// 计算平均分
Object.keys(grouped).forEach(key => {
grouped[key].avgScore = (grouped[key].totalScore / grouped[key].count).toFixed(1)
})
return grouped
}
/**
* 计算完成率
*/
static _calculateCompletionRate(courses) {
if (courses.length === 0) return 0
const passed = courses.filter(c => c.score >= 60).length
return ((passed / courses.length) * 100).toFixed(1)
}
/**
* 生成学习报告
*/
static generateReport() {
const stats = this.getStudyStats()
const report = {
summary: {
gpa: stats.gpa.average,
totalCourses: stats.courses.total,
passRate: stats.courses.completionRate
},
highlights: [],
suggestions: []
}
// 添加亮点
if (parseFloat(stats.gpa.average) >= 3.5) {
report.highlights.push('GPA优秀继续保持')
}
if (parseFloat(stats.courses.completionRate) === 100) {
report.highlights.push('所有课程全部通过,非常棒!')
}
// 添加建议
if (parseFloat(stats.gpa.average) < 2.5) {
report.suggestions.push('建议加强学习提高GPA')
}
if (stats.exams.thisWeek > 0) {
report.suggestions.push(`本周有${stats.exams.thisWeek}场考试,请做好准备`)
}
logger.info('生成学习报告', report)
return report
}
/**
* 导出数据
*/
static exportData() {
const data = {
gpaCourses: Storage.get('gpaCourses', []),
favoriteCourses: Storage.get('favoriteCourses', []),
schedule: Storage.get('schedule', {}),
countdowns: Storage.get('countdowns', []),
exportTime: new Date().toISOString()
}
logger.info('导出数据', { recordCount: data.gpaCourses.length })
return data
}
/**
* 导入数据
*/
static importData(data) {
try {
if (data.gpaCourses) {
Storage.set('gpaCourses', data.gpaCourses)
}
if (data.favoriteCourses) {
Storage.set('favoriteCourses', data.favoriteCourses)
}
if (data.schedule) {
Storage.set('schedule', data.schedule)
}
if (data.countdowns) {
Storage.set('countdowns', data.countdowns)
}
logger.info('导入数据成功', { recordCount: data.gpaCourses?.length || 0 })
return { success: true }
} catch (error) {
logger.error('导入数据失败', error)
return { success: false, error }
}
}
}
/**
* 智能推荐系统
*/
class RecommendationEngine {
/**
* 推荐课程
*/
static recommendCourses(allCourses, userCourses) {
const recommendations = []
// 基于用户已选课程推荐相关课程
const userCategories = [...new Set(userCourses.map(c => c.category))]
allCourses.forEach(course => {
let score = 0
// 相同分类加分
if (userCategories.includes(course.category)) {
score += 3
}
// 热门课程加分
if (course.enrolled / course.capacity > 0.8) {
score += 2
}
// 高评分课程加分
if (course.rating && course.rating >= 4.5) {
score += 2
}
// 还有名额的课程加分
if (course.enrolled < course.capacity) {
score += 1
}
if (score > 0) {
recommendations.push({
course,
score,
reason: this._getRecommendReason(score)
})
}
})
// 按分数排序
recommendations.sort((a, b) => b.score - a.score)
return recommendations.slice(0, 5)
}
/**
* 获取推荐理由
*/
static _getRecommendReason(score) {
if (score >= 6) return '强烈推荐'
if (score >= 4) return '推荐'
return '可以考虑'
}
/**
* 学习建议
*/
static getStudySuggestions(stats) {
const suggestions = []
// GPA建议
if (parseFloat(stats.gpa.average) < 2.0) {
suggestions.push({
type: 'gpa',
level: 'warning',
message: '当前GPA较低建议加强学习',
action: '查看学习计划'
})
} else if (parseFloat(stats.gpa.average) >= 3.5) {
suggestions.push({
type: 'gpa',
level: 'success',
message: 'GPA优秀继续保持',
action: '分享经验'
})
}
// 考试提醒
if (stats.exams.thisWeek > 0) {
suggestions.push({
type: 'exam',
level: 'info',
message: `本周有${stats.exams.thisWeek}场考试`,
action: '查看倒计时'
})
}
return suggestions
}
}
module.exports = {
Analytics,
RecommendationEngine
}

316
utils/auth.js Normal file
View File

@@ -0,0 +1,316 @@
/**
* 用户认证和权限管理系统
*/
const { Storage } = require('./storage.js')
const { logger } = require('./logger.js')
const { store } = require('./store.js')
class AuthManager {
constructor() {
this.token = null
this.userInfo = null
this.permissions = []
this.init()
}
/**
* 初始化
*/
init() {
this.token = Storage.get('token')
this.userInfo = Storage.get('userInfo')
this.permissions = Storage.get('permissions', [])
if (this.token && this.userInfo) {
store.setState({
isLogin: true,
userInfo: this.userInfo
})
}
}
/**
* 微信登录
*/
async wxLogin() {
try {
// 1. 获取微信登录code
const { code } = await this._wxLogin()
logger.info('微信登录code获取成功', { code })
// 2. 获取用户信息
const userInfo = await this._getUserProfile()
logger.info('用户信息获取成功', { userInfo })
// 3. 调用后端登录接口
// TODO: 替换为实际的后端登录接口
const response = await this._mockLogin(code, userInfo)
// 4. 保存登录状态
this.setAuth(response.token, response.userInfo, response.permissions)
logger.info('登录成功', { userId: response.userInfo.id })
return response
} catch (error) {
logger.error('登录失败', error)
throw error
}
}
/**
* 微信登录 - 获取code
*/
_wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
})
})
}
/**
* 获取用户信息
*/
_getUserProfile() {
return new Promise((resolve, reject) => {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => resolve(res.userInfo),
fail: reject
})
})
}
/**
* 模拟登录(开发用)
*/
async _mockLogin(code, userInfo) {
// TODO: 替换为实际的API调用
return new Promise((resolve) => {
setTimeout(() => {
resolve({
token: 'mock_token_' + Date.now(),
userInfo: {
id: Math.random().toString(36).substr(2, 9),
nickName: userInfo.nickName,
avatarUrl: userInfo.avatarUrl,
studentId: '2021001',
grade: '2021级',
major: '计算机科学与技术',
email: 'student@example.com'
},
permissions: ['user', 'student']
})
}, 500)
})
}
/**
* 设置认证信息
*/
setAuth(token, userInfo, permissions = []) {
this.token = token
this.userInfo = userInfo
this.permissions = permissions
Storage.set('token', token)
Storage.set('userInfo', userInfo)
Storage.set('permissions', permissions)
store.setState({
isLogin: true,
userInfo: userInfo
})
logger.info('认证信息已保存')
}
/**
* 退出登录
*/
logout() {
this.token = null
this.userInfo = null
this.permissions = []
Storage.remove('token')
Storage.remove('userInfo')
Storage.remove('permissions')
store.setState({
isLogin: false,
userInfo: null
})
logger.info('已退出登录')
wx.showToast({
title: '已退出登录',
icon: 'success'
})
}
/**
* 检查是否登录
*/
isLogin() {
return !!this.token && !!this.userInfo
}
/**
* 获取用户信息
*/
getUserInfo() {
return this.userInfo
}
/**
* 获取Token
*/
getToken() {
return this.token
}
/**
* 检查权限
*/
hasPermission(permission) {
return this.permissions.includes(permission)
}
/**
* 检查多个权限(需要全部满足)
*/
hasAllPermissions(permissions) {
return permissions.every(p => this.hasPermission(p))
}
/**
* 检查多个权限(满足任一即可)
*/
hasAnyPermission(permissions) {
return permissions.some(p => this.hasPermission(p))
}
/**
* 更新用户信息
*/
async updateUserInfo(updates) {
try {
// TODO: 调用后端API更新用户信息
const updatedUserInfo = { ...this.userInfo, ...updates }
this.userInfo = updatedUserInfo
Storage.set('userInfo', updatedUserInfo)
store.setState({ userInfo: updatedUserInfo })
logger.info('用户信息更新成功', updates)
wx.showToast({
title: '更新成功',
icon: 'success'
})
return updatedUserInfo
} catch (error) {
logger.error('用户信息更新失败', error)
throw error
}
}
/**
* 刷新Token
*/
async refreshToken() {
try {
// TODO: 调用后端API刷新token
const newToken = 'new_token_' + Date.now()
this.token = newToken
Storage.set('token', newToken)
logger.info('Token刷新成功')
return newToken
} catch (error) {
logger.error('Token刷新失败', error)
this.logout()
throw error
}
}
}
/**
* 页面权限守卫
*/
class RouteGuard {
/**
* 检查页面访问权限
*/
static check(requiredPermissions = []) {
const authManager = new AuthManager()
// 检查是否登录
if (!authManager.isLogin()) {
wx.showModal({
title: '提示',
content: '请先登录',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/login/login'
})
} else {
wx.navigateBack()
}
}
})
return false
}
// 检查权限
if (requiredPermissions.length > 0) {
if (!authManager.hasAllPermissions(requiredPermissions)) {
wx.showToast({
title: '没有访问权限',
icon: 'none'
})
wx.navigateBack()
return false
}
}
return true
}
/**
* 页面装饰器
*/
static requireAuth(requiredPermissions = []) {
return function(target) {
const originalOnLoad = target.prototype.onLoad
target.prototype.onLoad = function(options) {
if (RouteGuard.check(requiredPermissions)) {
if (originalOnLoad) {
originalOnLoad.call(this, options)
}
}
}
return target
}
}
}
// 创建全局实例
const authManager = new AuthManager()
module.exports = {
AuthManager,
authManager,
RouteGuard
}

158
utils/data.js Normal file
View File

@@ -0,0 +1,158 @@
// 模拟课程数据
const coursesData = [
{
id: 1,
name: '高等数学A',
teacher: '张教授',
credit: 4,
time: '周一 1-2节、周三 3-4节',
location: '东大主楼A201',
category: '必修',
department: '数学系',
capacity: 120,
enrolled: 98,
description: '本课程主要讲授微积分、级数、微分方程等内容,培养学生的数学思维和解决实际问题的能力。',
syllabus: ['极限与连续', '导数与微分', '积分学', '级数', '微分方程'],
isFavorite: false
},
{
id: 2,
name: '大学物理',
teacher: '李老师',
credit: 3,
time: '周二 3-4节、周四 1-2节',
location: '东大主楼B305',
category: '必修',
department: '物理系',
capacity: 100,
enrolled: 85,
description: '涵盖力学、热学、电磁学、光学和近代物理基础知识。',
syllabus: ['力学基础', '热力学', '电磁学', '光学', '量子物理'],
isFavorite: false
},
{
id: 3,
name: '数据结构与算法',
teacher: '王副教授',
credit: 4,
time: '周一 3-4节、周五 1-2节',
location: '信息学馆301',
category: '专业必修',
department: '计算机学院',
capacity: 80,
enrolled: 80,
description: '学习各种数据结构的特点及应用,掌握常用算法设计方法。',
syllabus: ['线性表', '栈与队列', '树与二叉树', '图', '排序与查找'],
isFavorite: false
},
{
id: 4,
name: 'Python程序设计',
teacher: '赵老师',
credit: 3,
time: '周三 5-6节',
location: '信息学馆实验室A',
category: '选修',
department: '计算机学院',
capacity: 60,
enrolled: 45,
description: 'Python语言基础、面向对象编程、常用库的使用。',
syllabus: ['Python基础', '函数与模块', '面向对象', '文件操作', '数据分析库'],
isFavorite: false
},
{
id: 5,
name: '大学英语',
teacher: '陈老师',
credit: 2,
time: '周二 1-2节',
location: '外语楼205',
category: '必修',
department: '外国语学院',
capacity: 40,
enrolled: 38,
description: '提升英语听说读写能力,培养跨文化交际能力。',
syllabus: ['听力训练', '口语表达', '阅读理解', '写作技巧', '翻译基础'],
isFavorite: false
},
{
id: 6,
name: '机器学习',
teacher: '刘教授',
credit: 3,
time: '周四 5-7节',
location: '信息学馆505',
category: '专业选修',
department: '计算机学院',
capacity: 50,
enrolled: 42,
description: '介绍机器学习的基本概念、算法及应用。',
syllabus: ['监督学习', '无监督学习', '神经网络', '深度学习', '实战项目'],
isFavorite: false
}
];
// 模拟论坛帖子数据
const forumData = [
{
id: 1,
title: '高数期中考试重点总结',
author: '学霸小明',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132',
content: '整理了高数期中考试的重点内容,包括极限、导数、积分等章节的核心知识点和典型例题...',
category: '数学',
views: 256,
likes: 45,
comments: 0, // 修改为0与实际评论数一致
time: '2小时前',
images: [],
isLiked: false
},
{
id: 2,
title: '数据结构课程设计求组队',
author: '编程爱好者',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',
content: '老师布置了一个课程设计项目,需要实现一个校园导航系统,有没有同学想一起做的?',
category: '计算机',
views: 128,
likes: 23,
comments: 0, // 修改为0
time: '5小时前',
images: [],
isLiked: false
},
{
id: 3,
title: 'Python爬虫实战分享',
author: '技术达人',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLL1byctY955Kv9oj8rR9pp0VfyGWXJL5gdZ8gibYJ0K4lP26icg7ib7Ws47kicIwmJxWnWJGcVQbGZOQ/132',
content: '分享一个用Python爬取教务系统课程信息的小项目附带完整代码和详细注释...',
category: '计算机',
views: 389,
likes: 67,
comments: 0, // 修改为0
time: '1天前',
images: [],
isLiked: false
},
{
id: 4,
title: '英语四级备考资料汇总',
author: '考试小助手',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJq06KicLiatnXnWKg8eh6ibEF9EOJSqIbLJiaEPjKlPvBiatkvR2oeib9dN8TlBMRib2ib3EuY84sibVD1JibA/132',
content: '收集了历年四级真题、高频词汇、听力材料等资源,需要的同学可以下载...',
category: '英语',
views: 512,
likes: 89,
comments: 0, // 修改为0
time: '2天前',
images: [],
isLiked: false
}
];
module.exports = {
coursesData,
forumData
};

265
utils/dataManager.js Normal file
View File

@@ -0,0 +1,265 @@
/**
* 数据初始化和管理工具
* 在微信开发者工具控制台中直接粘贴执行
*/
// ========================================
// 1⃣ 初始化完整的示例数据
// ========================================
function initAllDemoData() {
console.group('🚀 开始初始化示例数据');
// GPA课程数据16门课程4个学期
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('✅ GPA课程数据已初始化16门');
// 学习时长数据最近7天
const today = new Date();
const learningData = {
totalDays: 7,
totalHours: 24.5,
dailyRecords: []
};
const dailyActivity = {};
const moduleUsage = {
course: 8.5,
forum: 6.2,
tools: 7.3,
ai: 2.5
};
for (let i = 6; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = formatDate(date);
const duration = 1 + Math.random() * 4;
learningData.dailyRecords.push({
date: dateStr,
duration: parseFloat(duration.toFixed(1))
});
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('✅ 学习时长数据已初始化7天');
// 学习画像
const learningProfile = {
focus: 85,
activity: 90,
duration: 75,
breadth: 88,
interaction: 72,
persistence: 95
};
wx.setStorageSync('learning_profile', learningProfile);
console.log('✅ 学习画像已初始化');
// 课表数据
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('✅ 课表数据已初始化8门课');
// 倒计时数据
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('✅ 倒计时数据已初始化4个事件');
// 清除GPA历史记录让系统重新生成
wx.removeStorageSync('gpa_history');
console.log('✅ 已清除GPA历史记录将自动重新生成');
console.log('');
console.log('🎉 所有示例数据初始化完成!');
console.log('📌 请进入"学习数据"页面查看效果');
console.groupEnd();
}
// ========================================
// 2⃣ 查看所有数据
// ========================================
function viewAllData() {
console.group('📊 当前所有数据');
const gpaCourses = wx.getStorageSync('gpaCourses');
console.log('GPA课程数量:', gpaCourses?.length || 0);
console.log('GPA课程:', gpaCourses);
const gpaHistory = wx.getStorageSync('gpa_history');
console.log('GPA历史:', gpaHistory);
const learningData = wx.getStorageSync('learning_data');
console.log('学习数据:', learningData);
const dailyActivity = wx.getStorageSync('daily_activity');
console.log('每日活动:', dailyActivity);
const moduleUsage = wx.getStorageSync('module_usage');
console.log('模块使用:', moduleUsage);
const learningProfile = wx.getStorageSync('learning_profile');
console.log('学习画像:', learningProfile);
const schedule = wx.getStorageSync('schedule');
console.log('课表:', schedule);
const countdowns = wx.getStorageSync('countdowns');
console.log('倒计时:', countdowns);
console.groupEnd();
}
// ========================================
// 3⃣ 清除所有数据
// ========================================
function clearAllData() {
const confirm = window.confirm('⚠️ 确定要清除所有数据吗?此操作不可恢复!');
if (!confirm) {
console.log('❌ 已取消清除操作');
return;
}
wx.clearStorage();
console.log('✅ 所有数据已清除');
console.log('💡 请重启小程序或刷新页面');
}
// ========================================
// 4⃣ 只清除GPA数据
// ========================================
function clearGPAData() {
wx.removeStorageSync('gpaCourses');
wx.removeStorageSync('gpa_history');
console.log('✅ GPA数据已清除');
}
// ========================================
// 5⃣ 只清除学习时长数据
// ========================================
function clearLearningData() {
wx.removeStorageSync('learning_data');
wx.removeStorageSync('daily_activity');
wx.removeStorageSync('module_usage');
wx.removeStorageSync('learning_profile');
console.log('✅ 学习时长数据已清除');
}
// ========================================
// 6⃣ 诊断数据完整性
// ========================================
function diagnoseData() {
console.group('🔍 数据完整性诊断');
const checks = [
{ key: 'gpaCourses', name: 'GPA课程数据', required: true },
{ key: 'gpa_history', name: 'GPA历史记录', required: false },
{ key: 'learning_data', name: '学习时长数据', required: true },
{ key: 'daily_activity', name: '每日活动数据', required: true },
{ key: 'module_usage', name: '模块使用数据', required: true },
{ key: 'learning_profile', name: '学习画像数据', required: true },
{ key: 'schedule', name: '课表数据', required: false },
{ key: 'countdowns', name: '倒计时数据', required: false }
];
let allGood = true;
checks.forEach(check => {
const data = wx.getStorageSync(check.key);
const exists = data && (Array.isArray(data) ? data.length > 0 : Object.keys(data).length > 0);
if (exists) {
console.log(`${check.name}: 正常`);
} else if (check.required) {
console.error(`${check.name}: 缺失(必需)`);
allGood = false;
} else {
console.warn(`⚠️ ${check.name}: 缺失(可选)`);
}
});
console.log('');
if (allGood) {
console.log('🎉 所有必需数据完整!');
} else {
console.error('⚠️ 有必需数据缺失,建议运行 initAllDemoData()');
}
console.groupEnd();
}
// ========================================
// 辅助函数
// ========================================
function 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}`;
}
// ========================================
// 使用说明
// ========================================
console.log('');
console.log('╔════════════════════════════════════════╗');
console.log('║ 数据管理工具已加载 ║');
console.log('╚════════════════════════════════════════╝');
console.log('');
console.log('📌 可用命令:');
console.log('');
console.log('1⃣ initAllDemoData() - 初始化所有示例数据');
console.log('2⃣ viewAllData() - 查看所有数据');
console.log('3⃣ clearAllData() - 清除所有数据');
console.log('4⃣ clearGPAData() - 只清除GPA数据');
console.log('5⃣ clearLearningData() - 只清除学习时长数据');
console.log('6⃣ diagnoseData() - 诊断数据完整性');
console.log('');
console.log('💡 使用示例:');
console.log(' initAllDemoData(); // 初始化示例数据');
console.log(' diagnoseData(); // 检查数据状态');
console.log('');

276
utils/gpaPredictor.js Normal file
View File

@@ -0,0 +1,276 @@
/**
* GPA预测算法 - 多项式回归
*/
/**
* 多项式回归预测
* @param {Array} data - 历史GPA数据 [{semester: '2023-1', gpa: 3.5}, ...]
* @param {Number} degree - 多项式阶数默认2(二次)
* @param {Number} future - 预测未来几个学期
* @returns {Object} 预测结果
*/
function polynomialRegression(data, degree = 2, future = 3) {
if (!data || data.length < 2) {
return {
predictions: [],
trend: 0,
confidence: 0
};
}
// 提取数据点
const x = data.map((_, index) => index); // 学期编号 [0, 1, 2, ...]
const y = data.map(item => item.gpa); // GPA值
// 构建范德蒙德矩阵并求解系数
const coefficients = fitPolynomial(x, y, degree);
// 预测未来学期
const predictions = [];
const startIndex = data.length;
for (let i = 0; i < future; i++) {
const xValue = startIndex + i;
const yValue = evaluatePolynomial(coefficients, xValue);
// 限制GPA在合理范围内 [0, 4.0]
const predictedGPA = Math.max(0, Math.min(4.0, yValue));
predictions.push({
semester: generateSemesterName(data.length + i + 1),
gpa: Number(predictedGPA.toFixed(2)),
isPrediction: true
});
}
// 计算趋势(与当前学期对比)
const currentGPA = y[y.length - 1];
const nextGPA = predictions[0].gpa;
const trend = ((nextGPA - currentGPA) / currentGPA * 100).toFixed(1);
// 计算置信度基于R²
const rSquared = calculateRSquared(x, y, coefficients);
const confidence = Math.round(rSquared * 100);
return {
predictions,
trend: Number(trend),
confidence,
coefficients
};
}
/**
* 拟合多项式 - 使用最小二乘法
*/
function fitPolynomial(x, y, degree) {
const n = x.length;
const m = degree + 1;
// 构建设计矩阵 X 和目标向量 Y
const X = [];
for (let i = 0; i < n; i++) {
const row = [];
for (let j = 0; j <= degree; j++) {
row.push(Math.pow(x[i], j));
}
X.push(row);
}
// 计算 X^T * X
const XTX = multiplyMatrices(transpose(X), X);
// 计算 X^T * Y
const XTY = multiplyMatrixVector(transpose(X), y);
// 求解方程 (X^T * X) * coefficients = X^T * Y
const coefficients = solveLinearSystem(XTX, XTY);
return coefficients;
}
/**
* 计算多项式值
*/
function evaluatePolynomial(coefficients, x) {
let result = 0;
for (let i = 0; i < coefficients.length; i++) {
result += coefficients[i] * Math.pow(x, i);
}
return result;
}
/**
* 计算R²决定系数
*/
function calculateRSquared(x, y, coefficients) {
const n = x.length;
const yMean = y.reduce((sum, val) => sum + val, 0) / n;
let ssTotal = 0;
let ssResidual = 0;
for (let i = 0; i < n; i++) {
const yPred = evaluatePolynomial(coefficients, x[i]);
ssTotal += Math.pow(y[i] - yMean, 2);
ssResidual += Math.pow(y[i] - yPred, 2);
}
return 1 - (ssResidual / ssTotal);
}
/**
* 矩阵转置
*/
function transpose(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
const result = [];
for (let j = 0; j < cols; j++) {
const row = [];
for (let i = 0; i < rows; i++) {
row.push(matrix[i][j]);
}
result.push(row);
}
return result;
}
/**
* 矩阵乘法
*/
function multiplyMatrices(a, b) {
const rowsA = a.length;
const colsA = a[0].length;
const colsB = b[0].length;
const result = [];
for (let i = 0; i < rowsA; i++) {
const row = [];
for (let j = 0; j < colsB; j++) {
let sum = 0;
for (let k = 0; k < colsA; k++) {
sum += a[i][k] * b[k][j];
}
row.push(sum);
}
result.push(row);
}
return result;
}
/**
* 矩阵向量乘法
*/
function multiplyMatrixVector(matrix, vector) {
const rows = matrix.length;
const result = [];
for (let i = 0; i < rows; i++) {
let sum = 0;
for (let j = 0; j < vector.length; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
/**
* 求解线性方程组 - 高斯消元法
*/
function solveLinearSystem(A, b) {
const n = A.length;
const augmented = A.map((row, i) => [...row, b[i]]);
// 前向消元
for (let i = 0; i < n; i++) {
// 选主元
let maxRow = i;
for (let k = i + 1; k < n; k++) {
if (Math.abs(augmented[k][i]) > Math.abs(augmented[maxRow][i])) {
maxRow = k;
}
}
// 交换行
[augmented[i], augmented[maxRow]] = [augmented[maxRow], augmented[i]];
// 消元
for (let k = i + 1; k < n; k++) {
const factor = augmented[k][i] / augmented[i][i];
for (let j = i; j <= n; j++) {
augmented[k][j] -= factor * augmented[i][j];
}
}
}
// 回代
const x = new Array(n);
for (let i = n - 1; i >= 0; i--) {
x[i] = augmented[i][n];
for (let j = i + 1; j < n; j++) {
x[i] -= augmented[i][j] * x[j];
}
x[i] /= augmented[i][i];
}
return x;
}
/**
* 生成学期名称
*/
function generateSemesterName(index) {
const year = 2023 + Math.floor(index / 2);
const term = index % 2 === 1 ? '1' : '2';
return `${year}-${term}`;
}
/**
* 计算GPA根据分数
* @param {Number} score - 分数
* @returns {Number} GPA绩点
*/
function calculateGPA(score) {
if (score >= 60) {
return (score / 10 - 5);
}
return 0;
}
/**
* 批量计算GPA
*/
function batchCalculateGPA(courses) {
return courses.map(course => ({
...course,
gpa: calculateGPA(course.score)
}));
}
/**
* 计算平均GPA
*/
function calculateAverageGPA(courses) {
if (!courses || courses.length === 0) return 0;
const totalCredits = courses.reduce((sum, c) => sum + (c.credit || 1), 0);
const weightedSum = courses.reduce((sum, c) => {
const gpa = c.gpa || calculateGPA(c.score);
return sum + gpa * (c.credit || 1);
}, 0);
return (weightedSum / totalCredits).toFixed(2);
}
module.exports = {
polynomialRegression,
calculateGPA,
batchCalculateGPA,
calculateAverageGPA
};

278
utils/learningTracker.js Normal file
View File

@@ -0,0 +1,278 @@
/**
* 学习时长追踪服务
* 自动记录用户在各页面的学习时长
*/
class LearningTimeTracker {
constructor() {
this.currentPage = null;
this.startTime = null;
this.sessionData = {};
}
/**
* 开始追踪页面
* @param {String} pageName - 页面名称
*/
startTracking(pageName) {
// 如果之前有页面在追踪,先结束它
if (this.currentPage && this.startTime) {
this.endTracking();
}
this.currentPage = pageName;
this.startTime = Date.now();
console.log(`[学习追踪] 开始: ${pageName}`);
}
/**
* 结束追踪
*/
endTracking() {
if (!this.currentPage || !this.startTime) {
return;
}
const endTime = Date.now();
const duration = Math.floor((endTime - this.startTime) / 1000); // 秒
// 只记录超过5秒的有效学习时长过滤快速切换
if (duration >= 5) {
this.recordDuration(this.currentPage, duration);
console.log(`[学习追踪] 结束: ${this.currentPage}, 时长: ${duration}`);
}
this.currentPage = null;
this.startTime = null;
}
/**
* 记录学习时长
* @param {String} pageName - 页面名称
* @param {Number} duration - 时长(秒)
*/
recordDuration(pageName, duration) {
const today = this.getTodayString();
const now = new Date();
// 1. 更新总学习数据
const learningData = wx.getStorageSync('learning_data') || {
totalHours: 0,
totalSeconds: 0,
dailyRecords: []
};
learningData.totalSeconds = (learningData.totalSeconds || 0) + duration;
learningData.totalHours = parseFloat((learningData.totalSeconds / 3600).toFixed(1));
// 更新每日记录
let todayRecord = learningData.dailyRecords.find(r => r.date === today);
if (!todayRecord) {
todayRecord = {
date: today,
seconds: 0,
hours: 0,
sessions: []
};
learningData.dailyRecords.push(todayRecord);
}
todayRecord.seconds += duration;
todayRecord.hours = parseFloat((todayRecord.seconds / 3600).toFixed(2));
todayRecord.sessions.push({
page: pageName,
duration: duration,
timestamp: now.getTime()
});
// 只保留最近365天的记录
if (learningData.dailyRecords.length > 365) {
learningData.dailyRecords = learningData.dailyRecords
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 365);
}
wx.setStorageSync('learning_data', learningData);
// 2. 更新每日活跃度(用于热力图)
const dailyActivity = wx.getStorageSync('daily_activity') || {};
dailyActivity[today] = (dailyActivity[today] || 0) + Math.floor(duration / 60); // 转为分钟
wx.setStorageSync('daily_activity', dailyActivity);
// 3. 更新模块使用时长
const moduleMapping = {
'courses': 'course',
'course-detail': 'course',
'forum': 'forum',
'forum-detail': 'forum',
'post': 'forum',
'gpa': 'tools',
'schedule': 'tools',
'countdown': 'tools',
'tools': 'tools',
'ai-assistant': 'ai',
'dashboard': 'tools'
};
const module = moduleMapping[pageName] || 'other';
const moduleUsage = wx.getStorageSync('module_usage') || {
course: 0,
forum: 0,
tools: 0,
ai: 0
};
moduleUsage[module] = (moduleUsage[module] || 0) + parseFloat((duration / 3600).toFixed(2));
wx.setStorageSync('module_usage', moduleUsage);
// 4. 更新学习画像
this.updateLearningProfile(pageName, duration);
}
/**
* 更新学习画像
*/
updateLearningProfile(pageName, duration) {
const profile = wx.getStorageSync('learning_profile') || {
focus: 0, // 专注度
activity: 0, // 活跃度
duration: 0, // 学习时长
breadth: 0, // 知识广度
interaction: 0, // 互动性
persistence: 0 // 坚持度
};
// 专注度基于单次学习时长超过30分钟+10超过1小时+20
if (duration >= 3600) {
profile.focus = Math.min(100, profile.focus + 2);
} else if (duration >= 1800) {
profile.focus = Math.min(100, profile.focus + 1);
}
// 活跃度:每次学习+1
profile.activity = Math.min(100, profile.activity + 0.5);
// 学习时长:每小时+5
profile.duration = Math.min(100, profile.duration + (duration / 3600) * 5);
// 知识广度:访问课程相关页面+1
if (pageName.includes('course')) {
profile.breadth = Math.min(100, profile.breadth + 0.3);
}
// 互动性:论坛相关+2
if (pageName.includes('forum') || pageName.includes('post')) {
profile.interaction = Math.min(100, profile.interaction + 1);
}
// 坚持度:每天学习+3
const learningData = wx.getStorageSync('learning_data') || {};
const continuousDays = this.calculateContinuousDays(learningData.dailyRecords || []);
profile.persistence = Math.min(100, continuousDays * 3);
wx.setStorageSync('learning_profile', profile);
}
/**
* 计算连续学习天数
*/
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;
}
/**
* 获取今天的日期字符串
*/
getTodayString() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 获取学习统计
*/
getStats() {
const learningData = wx.getStorageSync('learning_data') || {
totalHours: 0,
totalSeconds: 0,
dailyRecords: []
};
const continuousDays = this.calculateContinuousDays(learningData.dailyRecords);
return {
totalHours: learningData.totalHours || 0,
totalSeconds: learningData.totalSeconds || 0,
continuousDays: continuousDays,
todayHours: this.getTodayHours()
};
}
/**
* 获取今天的学习时长
*/
getTodayHours() {
const today = this.getTodayString();
const learningData = wx.getStorageSync('learning_data') || { dailyRecords: [] };
const todayRecord = learningData.dailyRecords.find(r => r.date === today);
return todayRecord ? todayRecord.hours : 0;
}
}
// 创建全局实例
const tracker = new LearningTimeTracker();
module.exports = {
tracker,
/**
* 页面onShow时调用
*/
onPageShow(pageName) {
tracker.startTracking(pageName);
},
/**
* 页面onHide时调用
*/
onPageHide() {
tracker.endTracking();
},
/**
* 页面onUnload时调用
*/
onPageUnload() {
tracker.endTracking();
},
/**
* 获取统计数据
*/
getStats() {
return tracker.getStats();
}
};

310
utils/logger.js Normal file
View File

@@ -0,0 +1,310 @@
/**
* 日志管理系统
* 提供分级日志记录、错误追踪、性能监控
*/
class Logger {
constructor() {
this.logs = []
this.maxLogs = 1000 // 最多保存1000条日志
this.level = 'info' // debug, info, warn, error
this.enableConsole = true
this.enableStorage = true
}
/**
* 日志级别枚举
*/
static LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3
}
/**
* 记录调试信息
*/
debug(message, data = {}) {
this._log('DEBUG', message, data)
}
/**
* 记录普通信息
*/
info(message, data = {}) {
this._log('INFO', message, data)
}
/**
* 记录警告信息
*/
warn(message, data = {}) {
this._log('WARN', message, data)
}
/**
* 记录错误信息
*/
error(message, data = {}) {
this._log('ERROR', message, data)
}
/**
* 核心日志方法
*/
_log(level, message, data) {
const log = {
level,
message,
data,
timestamp: new Date().toISOString(),
page: getCurrentPages().length > 0 ? getCurrentPages()[getCurrentPages().length - 1].route : 'unknown'
}
// 添加到内存
this.logs.push(log)
if (this.logs.length > this.maxLogs) {
this.logs.shift()
}
// 输出到控制台
if (this.enableConsole) {
const consoleMethod = level.toLowerCase()
console[consoleMethod](`[${level}] ${message}`, data)
}
// 保存到本地存储
if (this.enableStorage && level === 'ERROR') {
this._persistErrorLog(log)
}
// 上报严重错误
if (level === 'ERROR') {
this._reportError(log)
}
}
/**
* 持久化错误日志
*/
_persistErrorLog(log) {
try {
const errorLogs = wx.getStorageSync('errorLogs') || []
errorLogs.push(log)
// 只保留最近100条错误
if (errorLogs.length > 100) {
errorLogs.shift()
}
wx.setStorageSync('errorLogs', errorLogs)
} catch (e) {
console.error('日志持久化失败:', e)
}
}
/**
* 上报错误
*/
_reportError(log) {
// TODO: 接入错误监控平台(如 Sentry
console.log('错误上报:', log)
}
/**
* 获取日志
*/
getLogs(filter = {}) {
let logs = this.logs
if (filter.level) {
logs = logs.filter(log => log.level === filter.level)
}
if (filter.page) {
logs = logs.filter(log => log.page === filter.page)
}
if (filter.startTime) {
logs = logs.filter(log => new Date(log.timestamp) >= new Date(filter.startTime))
}
return logs
}
/**
* 清空日志
*/
clearLogs() {
this.logs = []
wx.removeStorageSync('errorLogs')
}
/**
* 导出日志
*/
exportLogs() {
return {
logs: this.logs,
exportTime: new Date().toISOString(),
systemInfo: wx.getSystemInfoSync()
}
}
}
/**
* 错误处理器
*/
class ErrorHandler {
constructor() {
this.logger = new Logger()
this.setupGlobalErrorHandler()
}
/**
* 设置全局错误捕获
*/
setupGlobalErrorHandler() {
// 捕获未处理的Promise错误
wx.onUnhandledRejection((res) => {
this.handleError('UnhandledRejection', res.reason)
})
// 捕获小程序错误
wx.onError((error) => {
this.handleError('AppError', error)
})
}
/**
* 处理错误
*/
handleError(type, error, context = {}) {
const errorInfo = {
type,
message: error.message || error,
stack: error.stack || '',
context,
userAgent: wx.getSystemInfoSync(),
timestamp: Date.now()
}
this.logger.error(`${type}: ${errorInfo.message}`, errorInfo)
// 显示用户友好的错误提示
this.showUserFriendlyError(type, error)
return errorInfo
}
/**
* 显示用户友好的错误提示
*/
showUserFriendlyError(type, error) {
const errorMessages = {
'NetworkError': '网络连接失败,请检查网络设置',
'StorageError': '存储空间不足,请清理缓存',
'UnhandledRejection': '操作失败,请重试',
'AppError': '应用出现异常,请重启小程序'
}
const message = errorMessages[type] || '操作失败,请稍后重试'
wx.showToast({
title: message,
icon: 'none',
duration: 3000
})
}
/**
* 性能监控
*/
monitorPerformance(name, startTime) {
const duration = Date.now() - startTime
if (duration > 1000) {
this.logger.warn(`性能警告: ${name} 耗时 ${duration}ms`)
}
return duration
}
/**
* 获取错误统计
*/
getErrorStats() {
const errorLogs = wx.getStorageSync('errorLogs') || []
const stats = {
total: errorLogs.length,
byType: {},
byPage: {},
recent: errorLogs.slice(-10)
}
errorLogs.forEach(log => {
stats.byType[log.data.type] = (stats.byType[log.data.type] || 0) + 1
stats.byPage[log.page] = (stats.byPage[log.page] || 0) + 1
})
return stats
}
}
// 创建全局实例
const logger = new Logger()
const errorHandler = new ErrorHandler()
/**
* 用户反馈系统
*/
class FeedbackSystem {
/**
* 提交反馈
*/
static submit(feedback) {
const feedbackData = {
...feedback,
timestamp: Date.now(),
systemInfo: wx.getSystemInfoSync(),
logs: logger.getLogs({ level: 'ERROR' }).slice(-20)
}
logger.info('用户反馈', feedbackData)
// TODO: 上传到服务器
this._saveLocal(feedbackData)
return feedbackData
}
/**
* 保存到本地
*/
static _saveLocal(feedback) {
try {
const feedbacks = wx.getStorageSync('userFeedbacks') || []
feedbacks.push(feedback)
wx.setStorageSync('userFeedbacks', feedbacks)
} catch (e) {
console.error('反馈保存失败:', e)
}
}
/**
* 获取反馈列表
*/
static getList() {
return wx.getStorageSync('userFeedbacks') || []
}
}
module.exports = {
logger,
errorHandler,
Logger,
ErrorHandler,
FeedbackSystem
}

390
utils/performance.js Normal file
View File

@@ -0,0 +1,390 @@
/**
* 性能优化工具集
* 包含防抖、节流、懒加载、缓存等功能
*/
/**
* 防抖函数
* @param {Function} func 要执行的函数
* @param {Number} wait 延迟时间(毫秒)
* @param {Boolean} immediate 是否立即执行
*/
function debounce(func, wait = 300, immediate = false) {
let timeout
return function executedFunction(...args) {
const context = this
const later = function() {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
/**
* 节流函数
* @param {Function} func 要执行的函数
* @param {Number} limit 时间限制(毫秒)
*/
function throttle(func, limit = 300) {
let inThrottle
return function(...args) {
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
/**
* 缓存管理器
*/
class CacheManager {
constructor() {
this.cache = new Map()
this.maxSize = 50 // 最大缓存数量
this.ttl = 5 * 60 * 1000 // 默认5分钟过期
}
/**
* 设置缓存
*/
set(key, value, ttl = this.ttl) {
if (this.cache.size >= this.maxSize) {
// 删除最早的缓存
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, {
value,
expires: Date.now() + ttl
})
}
/**
* 获取缓存
*/
get(key) {
const item = this.cache.get(key)
if (!item) {
return null
}
if (Date.now() > item.expires) {
this.cache.delete(key)
return null
}
return item.value
}
/**
* 删除缓存
*/
delete(key) {
return this.cache.delete(key)
}
/**
* 清空缓存
*/
clear() {
this.cache.clear()
}
/**
* 获取缓存大小
*/
size() {
return this.cache.size
}
/**
* 清理过期缓存
*/
cleanup() {
const now = Date.now()
for (const [key, item] of this.cache.entries()) {
if (now > item.expires) {
this.cache.delete(key)
}
}
}
}
/**
* 图片懒加载管理器
*/
class LazyLoadManager {
constructor() {
this.observer = null
this.images = []
}
/**
* 初始化懒加载
*/
init(selector = '.lazy-image') {
if (!wx.createIntersectionObserver) {
console.warn('当前环境不支持IntersectionObserver')
return
}
this.observer = wx.createIntersectionObserver()
this.observer.relativeToViewport({ bottom: 100 })
this.observer.observe(selector, (res) => {
if (res.intersectionRatio > 0) {
this.loadImage(res.id)
}
})
}
/**
* 加载图片
*/
loadImage(id) {
const image = this.images.find(img => img.id === id)
if (image && !image.loaded) {
image.loaded = true
// 触发图片加载
if (image.callback) {
image.callback()
}
}
}
/**
* 添加图片
*/
addImage(id, callback) {
this.images.push({ id, loaded: false, callback })
}
/**
* 销毁
*/
destroy() {
if (this.observer) {
this.observer.disconnect()
}
}
}
/**
* 分页加载管理器
*/
class PaginationManager {
constructor(config = {}) {
this.pageSize = config.pageSize || 20
this.currentPage = 1
this.totalPages = 0
this.totalCount = 0
this.hasMore = true
this.loading = false
this.data = []
}
/**
* 加载下一页
*/
async loadMore(fetchFunction) {
if (this.loading || !this.hasMore) {
return null
}
this.loading = true
try {
const result = await fetchFunction(this.currentPage, this.pageSize)
this.data = [...this.data, ...result.data]
this.totalCount = result.total
this.totalPages = Math.ceil(result.total / this.pageSize)
this.hasMore = this.currentPage < this.totalPages
this.currentPage++
return result
} finally {
this.loading = false
}
}
/**
* 重置
*/
reset() {
this.currentPage = 1
this.totalPages = 0
this.totalCount = 0
this.hasMore = true
this.loading = false
this.data = []
}
/**
* 获取状态
*/
getState() {
return {
currentPage: this.currentPage,
totalPages: this.totalPages,
totalCount: this.totalCount,
hasMore: this.hasMore,
loading: this.loading,
dataLength: this.data.length
}
}
}
/**
* 性能监控器
*/
class PerformanceMonitor {
constructor() {
this.metrics = []
}
/**
* 开始监控
*/
start(name) {
return {
name,
startTime: Date.now(),
end: () => this.end(name, Date.now())
}
}
/**
* 结束监控
*/
end(name, startTime) {
const duration = Date.now() - startTime
this.metrics.push({
name,
duration,
timestamp: Date.now()
})
// 性能警告
if (duration > 1000) {
console.warn(`[性能警告] ${name} 耗时 ${duration}ms`)
}
return duration
}
/**
* 获取指标
*/
getMetrics(name) {
if (name) {
return this.metrics.filter(m => m.name === name)
}
return this.metrics
}
/**
* 获取平均时间
*/
getAverage(name) {
const metrics = this.getMetrics(name)
if (metrics.length === 0) return 0
const total = metrics.reduce((sum, m) => sum + m.duration, 0)
return total / metrics.length
}
/**
* 清空指标
*/
clear() {
this.metrics = []
}
}
/**
* 数据预加载管理器
*/
class PreloadManager {
constructor() {
this.preloadQueue = []
this.maxConcurrent = 3
this.running = 0
}
/**
* 添加预加载任务
*/
add(task, priority = 0) {
this.preloadQueue.push({ task, priority })
this.preloadQueue.sort((a, b) => b.priority - a.priority)
this.process()
}
/**
* 处理队列
*/
async process() {
while (this.running < this.maxConcurrent && this.preloadQueue.length > 0) {
const { task } = this.preloadQueue.shift()
this.running++
try {
await task()
} catch (e) {
console.error('预加载失败:', e)
} finally {
this.running--
this.process()
}
}
}
/**
* 清空队列
*/
clear() {
this.preloadQueue = []
}
}
// 创建全局实例
const cacheManager = new CacheManager()
const performanceMonitor = new PerformanceMonitor()
const preloadManager = new PreloadManager()
// 定期清理过期缓存
setInterval(() => {
cacheManager.cleanup()
}, 60 * 1000) // 每分钟清理一次
module.exports = {
debounce,
throttle,
CacheManager,
cacheManager,
LazyLoadManager,
PaginationManager,
PerformanceMonitor,
performanceMonitor,
PreloadManager,
preloadManager
}

312
utils/request.js Normal file
View File

@@ -0,0 +1,312 @@
/**
* API 请求管理器
* 统一处理网络请求、错误处理、请求拦截
*/
const { logger } = require('./logger.js')
const { cacheManager } = require('./performance.js')
class Request {
constructor(config = {}) {
this.baseURL = config.baseURL || ''
this.timeout = config.timeout || 10000
this.header = config.header || {
'Content-Type': 'application/json'
}
this.interceptors = {
request: [],
response: []
}
}
/**
* 请求拦截器
*/
useRequestInterceptor(handler) {
this.interceptors.request.push(handler)
}
/**
* 响应拦截器
*/
useResponseInterceptor(handler) {
this.interceptors.response.push(handler)
}
/**
* 发送请求
*/
async request(options) {
let config = {
url: this.baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: { ...this.header, ...options.header },
timeout: options.timeout || this.timeout
}
// 执行请求拦截器
for (const interceptor of this.interceptors.request) {
config = await interceptor(config)
}
// 检查缓存
if (config.method === 'GET' && options.cache) {
const cacheKey = this._getCacheKey(config)
const cached = cacheManager.get(cacheKey)
if (cached) {
logger.debug('使用缓存数据', { url: config.url })
return cached
}
}
const startTime = Date.now()
try {
const response = await this._wxRequest(config)
// 执行响应拦截器
let result = response
for (const interceptor of this.interceptors.response) {
result = await interceptor(result)
}
// 缓存GET请求结果
if (config.method === 'GET' && options.cache) {
const cacheKey = this._getCacheKey(config)
cacheManager.set(cacheKey, result, options.cacheTTL)
}
// 记录性能
const duration = Date.now() - startTime
logger.debug('请求成功', {
url: config.url,
duration: `${duration}ms`
})
return result
} catch (error) {
const duration = Date.now() - startTime
logger.error('请求失败', {
url: config.url,
duration: `${duration}ms`,
error
})
throw this._handleError(error)
}
}
/**
* 微信请求封装
*/
_wxRequest(config) {
return new Promise((resolve, reject) => {
wx.request({
...config,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject({
statusCode: res.statusCode,
message: res.data.message || '请求失败',
data: res.data
})
}
},
fail: (err) => {
reject({
statusCode: 0,
message: err.errMsg || '网络错误',
error: err
})
}
})
})
}
/**
* 错误处理
*/
_handleError(error) {
const errorMap = {
0: '网络连接失败',
400: '请求参数错误',
401: '未授权,请登录',
403: '拒绝访问',
404: '请求的资源不存在',
500: '服务器错误',
502: '网关错误',
503: '服务不可用',
504: '网关超时'
}
return {
code: error.statusCode,
message: errorMap[error.statusCode] || error.message || '未知错误',
data: error.data
}
}
/**
* 获取缓存键
*/
_getCacheKey(config) {
return `${config.method}:${config.url}:${JSON.stringify(config.data)}`
}
/**
* GET 请求
*/
get(url, params = {}, options = {}) {
return this.request({
url,
method: 'GET',
data: params,
...options
})
}
/**
* POST 请求
*/
post(url, data = {}, options = {}) {
return this.request({
url,
method: 'POST',
data,
...options
})
}
/**
* PUT 请求
*/
put(url, data = {}, options = {}) {
return this.request({
url,
method: 'PUT',
data,
...options
})
}
/**
* DELETE 请求
*/
delete(url, data = {}, options = {}) {
return this.request({
url,
method: 'DELETE',
data,
...options
})
}
/**
* 上传文件
*/
upload(url, filePath, formData = {}, options = {}) {
return new Promise((resolve, reject) => {
const uploadTask = wx.uploadFile({
url: this.baseURL + url,
filePath,
name: options.name || 'file',
formData,
header: { ...this.header, ...options.header },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(res.data))
} else {
reject({
statusCode: res.statusCode,
message: '上传失败'
})
}
},
fail: reject
})
// 进度回调
if (options.onProgress) {
uploadTask.onProgressUpdate(options.onProgress)
}
})
}
/**
* 下载文件
*/
download(url, options = {}) {
return new Promise((resolve, reject) => {
const downloadTask = wx.downloadFile({
url: this.baseURL + url,
header: { ...this.header, ...options.header },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.tempFilePath)
} else {
reject({
statusCode: res.statusCode,
message: '下载失败'
})
}
},
fail: reject
})
// 进度回调
if (options.onProgress) {
downloadTask.onProgressUpdate(options.onProgress)
}
})
}
}
// 创建全局实例
const request = new Request({
baseURL: 'https://api.example.com', // TODO: 替换为实际API地址
timeout: 10000
})
// 添加请求拦截器
request.useRequestInterceptor(async (config) => {
// 添加 token
const token = wx.getStorageSync('token')
if (token) {
config.header['Authorization'] = `Bearer ${token}`
}
// 显示加载提示
if (config.loading !== false) {
wx.showLoading({
title: '加载中...',
mask: true
})
}
return config
})
// 添加响应拦截器
request.useResponseInterceptor(async (response) => {
// 隐藏加载提示
wx.hideLoading()
// 统一处理业务错误码
if (response.code !== 0 && response.code !== 200) {
wx.showToast({
title: response.message || '操作失败',
icon: 'none'
})
throw response
}
return response.data || response
})
module.exports = {
Request,
request
}

165
utils/storage.js Normal file
View File

@@ -0,0 +1,165 @@
/**
* 本地存储管理器
* 提供统一的数据存储接口和错误处理
*/
class Storage {
/**
* 设置数据
*/
static set(key, data) {
try {
wx.setStorageSync(key, data)
return { success: true }
} catch (e) {
console.error(`存储失败 [${key}]:`, e)
return { success: false, error: e }
}
}
/**
* 获取数据
*/
static get(key, defaultValue = null) {
try {
const data = wx.getStorageSync(key)
return data || defaultValue
} catch (e) {
console.error(`读取失败 [${key}]:`, e)
return defaultValue
}
}
/**
* 删除数据
*/
static remove(key) {
try {
wx.removeStorageSync(key)
return { success: true }
} catch (e) {
console.error(`删除失败 [${key}]:`, e)
return { success: false, error: e }
}
}
/**
* 清空所有数据
*/
static clear() {
try {
wx.clearStorageSync()
return { success: true }
} catch (e) {
console.error('清空存储失败:', e)
return { success: false, error: e }
}
}
/**
* 获取存储信息
*/
static getInfo() {
try {
return wx.getStorageInfoSync()
} catch (e) {
console.error('获取存储信息失败:', e)
return null
}
}
/**
* 批量设置
*/
static setMultiple(items) {
const results = []
for (const [key, value] of Object.entries(items)) {
results.push(this.set(key, value))
}
return results
}
/**
* 批量获取
*/
static getMultiple(keys, defaultValue = null) {
const result = {}
keys.forEach(key => {
result[key] = this.get(key, defaultValue)
})
return result
}
}
/**
* 数据同步管理器
* 处理本地和云端数据同步
*/
class SyncManager {
constructor() {
this.syncQueue = []
this.isSyncing = false
}
/**
* 添加到同步队列
*/
addToQueue(type, data) {
this.syncQueue.push({
type,
data,
timestamp: Date.now()
})
this.processQueue()
}
/**
* 处理同步队列
*/
async processQueue() {
if (this.isSyncing || this.syncQueue.length === 0) {
return
}
this.isSyncing = true
while (this.syncQueue.length > 0) {
const item = this.syncQueue.shift()
try {
await this.syncItem(item)
} catch (e) {
console.error('同步失败:', e)
// 重新加入队列
this.syncQueue.push(item)
break
}
}
this.isSyncing = false
}
/**
* 同步单个项目
*/
async syncItem(item) {
// TODO: 实现云端同步逻辑
console.log('同步数据:', item)
return new Promise((resolve) => {
setTimeout(resolve, 100)
})
}
/**
* 获取待同步数量
*/
getPendingCount() {
return this.syncQueue.length
}
}
const syncManager = new SyncManager()
module.exports = {
Storage,
syncManager
}

128
utils/store.js Normal file
View File

@@ -0,0 +1,128 @@
/**
* 全局状态管理器
* 提供应用级数据管理和状态同步
*/
class Store {
constructor() {
this.state = {
userInfo: null,
isLogin: false,
favoriteCourses: [],
gpaCourses: [],
schedule: {},
countdowns: [],
settings: {
theme: 'light',
notifications: true,
language: 'zh-CN'
}
}
this.listeners = []
this.init()
}
/**
* 初始化:从本地存储恢复状态
*/
init() {
try {
const savedState = wx.getStorageSync('appState')
if (savedState) {
this.state = { ...this.state, ...savedState }
}
} catch (e) {
console.error('状态初始化失败:', e)
}
}
/**
* 获取状态
*/
getState(key) {
if (key) {
return this.state[key]
}
return this.state
}
/**
* 设置状态
*/
setState(key, value) {
if (typeof key === 'object') {
// 批量更新
this.state = { ...this.state, ...key }
} else {
this.state[key] = value
}
this.notify()
this.persist()
}
/**
* 订阅状态变化
*/
subscribe(listener) {
this.listeners.push(listener)
return () => {
const index = this.listeners.indexOf(listener)
if (index > -1) {
this.listeners.splice(index, 1)
}
}
}
/**
* 通知所有订阅者
*/
notify() {
this.listeners.forEach(listener => {
try {
listener(this.state)
} catch (e) {
console.error('状态通知失败:', e)
}
})
}
/**
* 持久化到本地存储
*/
persist() {
try {
wx.setStorageSync('appState', this.state)
} catch (e) {
console.error('状态持久化失败:', e)
}
}
/**
* 清空状态
*/
clear() {
this.state = {
userInfo: null,
isLogin: false,
favoriteCourses: [],
gpaCourses: [],
schedule: {},
countdowns: [],
settings: {
theme: 'light',
notifications: true,
language: 'zh-CN'
}
}
this.persist()
this.notify()
}
}
// 创建全局实例
const store = new Store()
module.exports = {
store
}

239
utils/userManager.js Normal file
View File

@@ -0,0 +1,239 @@
/**
* 用户信息管理工具
* 统一处理用户信息的存储和读取,确保数据一致性
*/
/**
* 保存用户信息
* @param {Object} userInfo - 用户信息对象
*/
function saveUserInfo(userInfo) {
if (!userInfo) {
console.error('保存用户信息失败userInfo为空')
return false
}
try {
// 标准化用户信息格式
const standardizedUserInfo = {
// 统一使用 nickname 和 avatar 作为主字段
nickname: userInfo.nickname || userInfo.nickName || '同学',
avatar: userInfo.avatar || userInfo.avatarUrl || '/images/avatar-default.png',
// 保留原始字段以兼容旧代码
nickName: userInfo.nickname || userInfo.nickName || '同学',
avatarUrl: userInfo.avatar || userInfo.avatarUrl || '/images/avatar-default.png',
// 其他字段
gender: userInfo.gender || 0,
country: userInfo.country || '',
province: userInfo.province || '',
city: userInfo.city || '',
isLogin: userInfo.isLogin || false,
loginTime: userInfo.loginTime || new Date().getTime(),
// 保留其他可能的自定义字段
...userInfo
}
// 保存到本地存储
wx.setStorageSync('userInfo', standardizedUserInfo)
console.log('用户信息保存成功:', standardizedUserInfo)
return true
} catch (error) {
console.error('保存用户信息失败:', error)
return false
}
}
/**
* 获取用户信息
* @returns {Object|null} 用户信息对象如果不存在则返回null
*/
function getUserInfo() {
try {
let userInfo = wx.getStorageSync('userInfo')
if (userInfo) {
// 标准化数据结构
const standardizedUserInfo = {
nickname: userInfo.nickname || userInfo.nickName || '同学',
avatar: userInfo.avatar || userInfo.avatarUrl || '/images/avatar-default.png',
nickName: userInfo.nickname || userInfo.nickName || '同学',
avatarUrl: userInfo.avatar || userInfo.avatarUrl || '/images/avatar-default.png',
gender: userInfo.gender || 0,
country: userInfo.country || '',
province: userInfo.province || '',
city: userInfo.city || '',
isLogin: userInfo.isLogin || false,
loginTime: userInfo.loginTime || 0,
...userInfo
}
// 如果发现数据结构不一致,自动更新
if (!userInfo.nickname || !userInfo.avatar) {
saveUserInfo(standardizedUserInfo)
}
return standardizedUserInfo
}
return null
} catch (error) {
console.error('获取用户信息失败:', error)
return null
}
}
/**
* 更新用户信息(部分更新)
* @param {Object} updates - 要更新的字段
*/
function updateUserInfo(updates) {
if (!updates) {
console.error('更新用户信息失败updates为空')
return false
}
try {
const currentUserInfo = getUserInfo() || {}
const updatedUserInfo = {
...currentUserInfo,
...updates
}
// 确保统一字段
if (updates.nickname) {
updatedUserInfo.nickName = updates.nickname
}
if (updates.avatar) {
updatedUserInfo.avatarUrl = updates.avatar
}
if (updates.nickName) {
updatedUserInfo.nickname = updates.nickName
}
if (updates.avatarUrl) {
updatedUserInfo.avatar = updates.avatarUrl
}
return saveUserInfo(updatedUserInfo)
} catch (error) {
console.error('更新用户信息失败:', error)
return false
}
}
/**
* 更新用户昵称
* @param {String} nickname - 新昵称
*/
function updateNickname(nickname) {
if (!nickname || !nickname.trim()) {
console.error('昵称不能为空')
return false
}
return updateUserInfo({
nickname: nickname.trim(),
nickName: nickname.trim()
})
}
/**
* 更新用户头像
* @param {String} avatar - 新头像路径
*/
function updateAvatar(avatar) {
if (!avatar) {
console.error('头像路径不能为空')
return false
}
return updateUserInfo({
avatar: avatar,
avatarUrl: avatar
})
}
/**
* 清除用户信息(退出登录)
*/
function clearUserInfo() {
try {
// 保留昵称和头像,仅标记为未登录
const currentUserInfo = getUserInfo()
if (currentUserInfo) {
const logoutUserInfo = {
nickname: currentUserInfo.nickname,
avatar: currentUserInfo.avatar,
isLogin: false
}
saveUserInfo(logoutUserInfo)
} else {
wx.removeStorageSync('userInfo')
}
console.log('用户信息已清除')
return true
} catch (error) {
console.error('清除用户信息失败:', error)
return false
}
}
/**
* 检查用户是否已登录
* @returns {Boolean} 是否已登录
*/
function isUserLogin() {
const userInfo = getUserInfo()
return userInfo && userInfo.isLogin === true
}
/**
* 获取用户昵称
* @returns {String} 用户昵称
*/
function getNickname() {
const userInfo = getUserInfo()
return userInfo ? userInfo.nickname : '同学'
}
/**
* 获取用户头像
* @returns {String} 用户头像路径
*/
function getAvatar() {
const userInfo = getUserInfo()
return userInfo ? userInfo.avatar : '/images/avatar-default.png'
}
/**
* 初始化默认用户信息
*/
function initDefaultUserInfo() {
const userInfo = getUserInfo()
if (!userInfo) {
const defaultUser = {
nickname: '同学',
avatar: '/images/avatar-default.png',
isLogin: false
}
saveUserInfo(defaultUser)
console.log('初始化默认用户信息')
}
}
module.exports = {
saveUserInfo,
getUserInfo,
updateUserInfo,
updateNickname,
updateAvatar,
clearUserInfo,
isUserLogin,
getNickname,
getAvatar,
initDefaultUserInfo
}

302
utils/util.js Normal file
View File

@@ -0,0 +1,302 @@
// 工具函数库
/**
* 格式化时间
*/
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : `0${n}`
}
/**
* 显示成功提示
*/
const showSuccess = (title = '操作成功') => {
wx.showToast({
title: title,
icon: 'success',
duration: 2000
})
}
/**
* 显示失败提示
*/
const showError = (title = '操作失败') => {
wx.showToast({
title: title,
icon: 'none',
duration: 2000
})
}
/**
* 显示加载中
*/
const showLoading = (title = '加载中...') => {
wx.showLoading({
title: title,
mask: true
})
}
/**
* 隐藏加载
*/
const hideLoading = () => {
wx.hideLoading()
}
/**
* 计算GPA
* @param {Array} courses 课程列表 [{score: 90, credit: 4}, ...]
*/
const calculateGPA = (courses) => {
if (!courses || courses.length === 0) return 0;
let totalPoints = 0;
let totalCredits = 0;
courses.forEach(course => {
const gradePoint = scoreToGradePoint(course.score);
totalPoints += gradePoint * course.credit;
totalCredits += course.credit;
});
return totalCredits > 0 ? (totalPoints / totalCredits).toFixed(2) : 0;
}
/**
* 分数转绩点
* 公式60分及以上 = 成绩/10-560分以下 = 0
*/
const scoreToGradePoint = (score) => {
if (score >= 60) {
return parseFloat((score / 10 - 5).toFixed(2));
}
return 0;
}
/**
* 计算倒计时
*/
const getCountdown = (targetDate) => {
const now = new Date().getTime();
const target = new Date(targetDate).getTime();
const diff = target - now;
if (diff <= 0) {
return { days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true };
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds, isExpired: false };
}
/**
* 日期格式化
*/
const formatDate = (date, format = 'YYYY-MM-DD') => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
const second = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hour)
.replace('mm', minute)
.replace('ss', second)
}
/**
* 相对时间
*/
const formatRelativeTime = (timestamp) => {
const now = Date.now()
const diff = now - new Date(timestamp).getTime()
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
const week = 7 * day
const month = 30 * day
if (diff < minute) return '刚刚'
if (diff < hour) return `${Math.floor(diff / minute)}分钟前`
if (diff < day) return `${Math.floor(diff / hour)}小时前`
if (diff < week) return `${Math.floor(diff / day)}天前`
if (diff < month) return `${Math.floor(diff / week)}周前`
return formatDate(timestamp, 'YYYY-MM-DD')
}
/**
* 文件大小格式化
*/
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}
/**
* 验证手机号
*/
const validatePhone = (phone) => {
return /^1[3-9]\d{9}$/.test(phone)
}
/**
* 验证邮箱
*/
const validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
/**
* 验证学号示例8位数字
*/
const validateStudentId = (id) => {
return /^\d{8,10}$/.test(id)
}
/**
* 深拷贝
*/
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof Array) return obj.map(item => deepClone(item))
const clonedObj = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}
/**
* 数组去重
*/
const unique = (arr, key) => {
if (!key) return [...new Set(arr)]
const seen = new Set()
return arr.filter(item => {
const val = item[key]
if (seen.has(val)) return false
seen.add(val)
return true
})
}
/**
* 数组分组
*/
const groupBy = (arr, key) => {
return arr.reduce((result, item) => {
const group = typeof key === 'function' ? key(item) : item[key]
if (!result[group]) result[group] = []
result[group].push(item)
return result
}, {})
}
/**
* 生成UUID
*/
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
/**
* 节流
*/
const throttle = (fn, delay = 300) => {
let timer = null
return function(...args) {
if (timer) return
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
/**
* 防抖
*/
const debounce = (fn, delay = 300) => {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
/**
* 分享配置
*/
const getShareConfig = (title, imageUrl = '') => {
return {
title: title || '知芽小筑 - 学习辅助小程序',
path: '/pages/index/index',
imageUrl: imageUrl || '/images/share.png'
}
}
module.exports = {
formatTime,
formatDate,
formatRelativeTime,
formatFileSize,
formatNumber,
showSuccess,
showError,
showLoading,
hideLoading,
calculateGPA,
scoreToGradePoint,
getCountdown,
validatePhone,
validateEmail,
validateStudentId,
deepClone,
unique,
groupBy,
generateUUID,
throttle,
debounce,
getShareConfig
}