1
This commit is contained in:
185
utils/aiService.js
Normal file
185
utils/aiService.js
Normal 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
350
utils/analytics.js
Normal 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
316
utils/auth.js
Normal 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
158
utils/data.js
Normal 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
265
utils/dataManager.js
Normal 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
276
utils/gpaPredictor.js
Normal 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
278
utils/learningTracker.js
Normal 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
310
utils/logger.js
Normal 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
390
utils/performance.js
Normal 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
312
utils/request.js
Normal 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
165
utils/storage.js
Normal 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
128
utils/store.js
Normal 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
239
utils/userManager.js
Normal 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
302
utils/util.js
Normal 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-5,60分以下 = 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
|
||||
}
|
||||
Reference in New Issue
Block a user