diff --git a/frontend/package.json b/frontend/package.json index d210bfb..075baaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,17 +9,18 @@ "serve": "vite preview --port 3000" }, "dependencies": { - "vue": "^3.3.8", - "vue-router": "^4.2.5", - "pinia": "^2.1.7", - "axios": "^1.6.2", - "element-plus": "^2.4.4", "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", "chart.js": "^4.4.0", - "vue-chartjs": "^5.2.0" + "echarts": "^6.0.0", + "element-plus": "^2.4.4", + "pinia": "^2.1.7", + "vue": "^3.3.8", + "vue-chartjs": "^5.2.0", + "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", "vite": "^5.0.2" } -} \ No newline at end of file +} diff --git a/frontend/src/views/MainDashboard.vue b/frontend/src/views/MainDashboard.vue index 2488c4b..a1f3c7e 100644 --- a/frontend/src/views/MainDashboard.vue +++ b/frontend/src/views/MainDashboard.vue @@ -1,39 +1,158 @@
- +
@@ -243,6 +408,100 @@ + + + +
+
+ +
+
+
🤖
+
+

AI学习助理

+

你的智能学习伙伴

+
+
+ ✕ +
+ + +
+

📚 今日学习推荐

+
+
+ 📐 +
+ 函数单元复习 +

3道练习题 · 预计20分钟

+
+ 开始 +
+
+ 📝 +
+ 英语单词记忆 +

Unit 5 · 15个新单词

+
+ 学习 +
+
+ 🔬 +
+ 物理错题回顾 +

牛顿定律 · 5道题

+
+ 复习 +
+
+
+ + +
+

📈 学习进度分析

+
+
+
{{ userProgress.weeklyMinutes }}分钟
+
本周学习
+
+
+
{{ userProgress.rankPercent }}%
+
超越同龄人
+
+
+
{{ todayStats.accuracy }}%
+
今日准确率
+
+
+
+ + +
+

💬 智能问答

+
+
+
{{ message.type === 'user' ? '👤' : '🤖' }}
+ +
+
+ +
+ + + +
+
+
+
+
@@ -252,6 +511,7 @@ import { useRouter } from 'vue-router' import { ElMessage, ElNotification } from 'element-plus' import { UploadFilled, Upload, Clock, List, TrendCharts, Reading } from '@element-plus/icons-vue' import { useAuthStore } from '../stores/auth' +import * as echarts from 'echarts' import api from '../services/api' const router = useRouter() @@ -266,6 +526,46 @@ const selectedFile = ref(null) const filesList = ref([]) const uploadFormRef = ref() +// AI Assistant +const showAiAssistant = ref(false) +const aiInput = ref('') +const aiLoading = ref(false) +const chatMessagesIdCounter = ref(2) +const chatMessages = ref([ + { + id: 1, + type: 'ai', + content: '你好!我是你的AI学习助理,可以帮你制定学习计划、解答问题、分析错题。有什么可以帮助你的吗?' + } +]) +const aiSuggestions = ref([ + '今日学习计划是什么?', + '如何提高数学成绩?', + '分析我的薄弱知识点', + '推荐一些学习资料' +]) + +// Today Statistics +const todayStats = ref({ + studyTime: 0, + completedExercises: 0, + accuracy: 0 +}) + +// User Progress +const userStreak = ref(7) // 连击天数 +const userLevel = ref(5) + +// Achievement Metrics +const learningStreak = ref(7) +const weeklyStudyTime = ref(366) +const masteredWords = ref(1248) +const todayAccuracy = ref(88) +const userProgress = ref({ + weeklyMinutes: 366, + rankPercent: 78 +}) + const dashboardStats = ref({ papers_count: 0, mistakes_count: 0, @@ -282,25 +582,59 @@ const statCards = computed(() => [ value: dashboardStats.value.due_reviews, icon: '⏰', color: 'due', + subtitle: '建议复习时段:14:00-16:00', clickHandler: () => goToTodayReview(), action: { text: '立即复习', handler: () => goToTodayReview() }, - trend: generateTrendData(dashboardStats.value.due_reviews) + trend: generateTrendData(dashboardStats.value.due_reviews), + progress: Math.min(dashboardStats.value.due_reviews / 10 * 100, 100) // 假设目标10个 }, { type: 'papers', title: '试卷总数', value: dashboardStats.value.papers_count, - icon: '📄', + icon: '📚', color: 'papers', + subtitle: `已完成 ${Math.floor(dashboardStats.value.papers_count * 0.7)} 份`, clickHandler: () => goToAllPapers(), action: { text: '查看试卷', handler: () => goToAllPapers() }, - trend: generateTrendData(dashboardStats.value.papers_count) + trend: generateTrendData(dashboardStats.value.papers_count), + progress: 70 // 模拟完成度 + }, + { + type: 'study_time', + title: '学习时间', + value: `${todayStats.value.studyTime}分钟`, + icon: '⏱️', + color: 'time', + subtitle: '今日累计学习时长', + clickHandler: () => showStudyStats(), + action: { + text: '查看详情', + handler: () => showStudyStats() + }, + trend: generateTrendData(todayStats.value.studyTime / 10), // 转换为适合图表的数值 + progress: Math.min(todayStats.value.studyTime / 60 * 100, 100) // 目标60分钟 + }, + { + type: 'accuracy', + title: '最近准确率', + value: `${todayStats.value.accuracy}%`, + icon: '🎯', + color: 'accuracy', + subtitle: todayStats.value.accuracy >= 80 ? '表现优秀' : todayStats.value.accuracy >= 60 ? '继续努力' : '需要加强', + clickHandler: () => goToAccuracyAnalysis(), + action: { + text: '查看分析', + handler: () => goToAccuracyAnalysis() + }, + trend: generateTrendData(todayStats.value.accuracy), + progress: todayStats.value.accuracy }, { type: 'mistakes', @@ -383,6 +717,41 @@ const subjectStatsMap = computed(() => { return dashboardStats.value.subject_stats || {} }) +const currentUser = computed(() => { + return authStore.user || { username: '学习者' } +}) + +// Greeting function +const getGreeting = () => { + const hour = new Date().getHours() + if (hour < 6) return '夜深了' + if (hour < 12) return '早上好' + if (hour < 14) return '中午好' + if (hour < 18) return '下午好' + return '晚上好' +} + +// Daily motivation +const getDailyMotivation = () => { + const motivations = [ + "📚 今天也是充满希望的一天!知识的海洋在等你探索!", + "🌟 相信自己,你可以的!每一点进步都值得庆祝!", + "🚀 每一步都在进步中!坚持就是最好的品质!", + "💪 坚持就是胜利!今天的努力是明天的收获!", + "🎯 学习让未来更美好!你正在为自己的梦想铺路!", + "🔥 热爱学习,享受成长的过程!", + "⭐ 每一个知识点都是通向成功的阶梯!", + "🌈 今天的学习是为了明天更精彩的自己!" + ] + const dayIndex = new Date().getDay() + return motivations[dayIndex] || motivations[0] +} + +// Today improvement +const getTodayImprovement = () => { + return Math.floor(Math.random() * 20) + 5 // 5-25分钟的随机进步 +} + // 生成趋势数据 function generateTrendData(baseValue) { if (baseValue === 0) return null; @@ -601,71 +970,332 @@ const drawStatTrends = () => { }) } -// 绘制主趋势图 +// 绘制主趋势图 - 使用ECharts渐变折线+动效 const drawTrendChart = () => { - const canvas = document.getElementById('trend-chart') - if (!canvas) return - - const ctx = canvas.getContext('2d') - ctx.clearRect(0, 0, canvas.width, canvas.height) - - // 模拟数据 - 最近7天的学习准确率 - const data = [75, 80, 85, 78, 90, 88, 92] - const width = canvas.width - const height = canvas.height - const margin = 40 - - // 绘制网格 - ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)' - ctx.lineWidth = 1 - - // 水平网格线 - for (let i = 0; i <= 5; i++) { - const y = margin + (i * (height - 2 * margin)) / 5 - ctx.beginPath() - ctx.moveTo(margin, y) - ctx.lineTo(width - margin, y) - ctx.stroke() + const chartDom = document.getElementById('trend-chart') + if (!chartDom) return + + // 销毁已存在的图表实例 + if (window.trendChartInstance) { + window.trendChartInstance.dispose() } - - // 垂直网格线 - for (let i = 0; i <= 7; i++) { - const x = margin + (i * (width - 2 * margin)) / 7 - ctx.beginPath() - ctx.moveTo(x, margin) - ctx.lineTo(x, height - margin) - ctx.stroke() + + // 初始化ECharts实例 + window.trendChartInstance = echarts.init(chartDom) + + // 生成最近7天的日期标签 + const dates = [] + const today = new Date() + for (let i = 6; i >= 0; i--) { + const date = new Date(today) + date.setDate(date.getDate() - i) + dates.push(`${date.getMonth() + 1}/${date.getDate()}`) } - - // 绘制趋势线 - ctx.strokeStyle = '#4facfe' - ctx.lineWidth = 3 - ctx.beginPath() - - data.forEach((value, index) => { - const x = margin + (index * (width - 2 * margin)) / (data.length - 1) - const y = height - margin - ((value - 60) / 40) * (height - 2 * margin) - - if (index === 0) { - ctx.moveTo(x, y) - } else { - ctx.lineTo(x, y) + + // 模拟数据 - 最近7天的学习数据 + const accuracyData = [75, 80, 85, 78, 90, 88, 92] + const studyTimeData = [120, 150, 180, 140, 200, 190, 210] + + const option = { + // 动画配置 + animation: true, + animationDuration: 2000, + animationEasing: 'cubicOut', + + // 提示框 - FogMint护眼配色 + tooltip: { + trigger: 'axis', + backgroundColor: 'rgba(46, 51, 64, 0.9)', + borderColor: 'rgba(88, 197, 165, 0.3)', + textStyle: { + color: '#ECEFF4' + }, + axisPointer: { + type: 'line', + lineStyle: { + color: 'rgba(88, 197, 165, 0.4)' + } + } + }, + + // 图例 + legend: { + data: ['学习准确率', '学习时长'], + textStyle: { + color: '#8892A0' + }, + top: 10 + }, + + // 网格 + grid: { + left: '3%', + right: '4%', + bottom: '3%', + top: '15%', + containLabel: true + }, + + // X轴 - FogMint护眼配色 + xAxis: { + type: 'category', + data: dates, + axisLine: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + axisLabel: { + color: '#8892A0' + }, + axisTick: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + splitLine: { + show: true, + lineStyle: { + color: 'rgba(136, 146, 160, 0.15)', + type: 'dashed' + } + } + }, + + // Y轴 - FogMint护眼配色 + yAxis: [ + { + type: 'value', + name: '准确率(%)', + min: 60, + max: 100, + nameTextStyle: { + color: '#8892A0' + }, + axisLine: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + axisLabel: { + color: '#8892A0', + formatter: '{value}%' + }, + axisTick: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + splitLine: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.15)', + type: 'dashed' + } + } + }, + { + type: 'value', + name: '时长(分钟)', + min: 0, + max: 250, + nameTextStyle: { + color: '#8892A0' + }, + axisLine: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + axisLabel: { + color: '#8892A0', + formatter: '{value}min' + }, + axisTick: { + lineStyle: { + color: 'rgba(136, 146, 160, 0.4)' + } + }, + splitLine: { + show: false + } + } + ], + + // 系列 + series: [ + { + name: '学习准确率', + type: 'line', + data: accuracyData, + smooth: true, + yAxisIndex: 0, + + // 渐变填充 - FogMint护眼配色 + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(88, 197, 165, 0.4)' + }, + { + offset: 1, + color: 'rgba(88, 197, 165, 0.05)' + } + ] + } + }, + + // 线条样式 + lineStyle: { + width: 4, + color: { + type: 'linear', + x: 0, + y: 0, + x2: 1, + y2: 0, + colorStops: [ + { + offset: 0, + color: '#58C5A5' + }, + { + offset: 1, + color: '#48B885' + } + ] + }, + shadowColor: 'rgba(88, 197, 165, 0.4)', + shadowBlur: 8 + }, + + // 数据点样式 + itemStyle: { + color: '#fff', + borderColor: '#58C5A5', + borderWidth: 3, + shadowColor: 'rgba(88, 197, 165, 0.6)', + shadowBlur: 8 + }, + + // 强调效果 + emphasis: { + itemStyle: { + color: '#fff', + borderColor: '#48B885', + borderWidth: 4, + shadowColor: 'rgba(88, 197, 165, 0.8)', + shadowBlur: 12 + }, + lineStyle: { + width: 6 + } + } + }, + { + name: '学习时长', + type: 'line', + data: studyTimeData, + smooth: true, + yAxisIndex: 1, + + // 渐变填充 - FogMint护眼配色 + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(96, 165, 250, 0.3)' + }, + { + offset: 1, + color: 'rgba(96, 165, 250, 0.05)' + } + ] + } + }, + + // 线条样式 + lineStyle: { + width: 3, + color: { + type: 'linear', + x: 0, + y: 0, + x2: 1, + y2: 0, + colorStops: [ + { + offset: 0, + color: '#60A5FA' + }, + { + offset: 1, + color: '#3B82F6' + } + ] + }, + shadowColor: 'rgba(96, 165, 250, 0.4)', + shadowBlur: 8 + }, + + // 数据点样式 - FogMint护眼配色 + itemStyle: { + color: '#fff', + borderColor: '#60A5FA', + borderWidth: 2, + shadowColor: 'rgba(96, 165, 250, 0.6)', + shadowBlur: 6 + }, + + // 强调效果 + emphasis: { + itemStyle: { + color: '#fff', + borderColor: '#3B82F6', + borderWidth: 3, + shadowColor: 'rgba(96, 165, 250, 0.8)', + shadowBlur: 10 + }, + lineStyle: { + width: 5 + } + } + } + ] + } + + // 设置图表选项 + window.trendChartInstance.setOption(option) + + // 响应式处理 + const resizeChart = () => { + if (window.trendChartInstance) { + window.trendChartInstance.resize() } - - // 绘制数据点 - ctx.fillStyle = '#ffffff' - ctx.beginPath() - ctx.arc(x, y, 4, 0, 2 * Math.PI) - ctx.fill() - }) - - ctx.stroke() - - // 添加标题 - ctx.fillStyle = '#ffffff' - ctx.font = 'bold 14px Arial' - ctx.textAlign = 'center' - ctx.fillText('最近7天学习趋势', width / 2, 20) + } + + window.addEventListener('resize', resizeChart) + + // 清理函数 + return () => { + window.removeEventListener('resize', resizeChart) + if (window.trendChartInstance) { + window.trendChartInstance.dispose() + window.trendChartInstance = null + } + } } const logout = () => { @@ -673,9 +1303,327 @@ const logout = () => { router.push('/login') } +// Enhanced Interactive Functions +const addClickAnimation = (element) => { + element.classList.add('bounce-animation') + setTimeout(() => { + element.classList.remove('bounce-animation') + }, 1000) +} + +const handleCardClick = (event, callback) => { + const card = event.currentTarget + addClickAnimation(card) + + // Add ripple effect + const ripple = document.createElement('div') + ripple.classList.add('ripple-effect') + + const rect = card.getBoundingClientRect() + const size = Math.max(rect.width, rect.height) + const x = event.clientX - rect.left - size / 2 + const y = event.clientY - rect.top - size / 2 + + ripple.style.width = ripple.style.height = size + 'px' + ripple.style.left = x + 'px' + ripple.style.top = y + 'px' + + card.appendChild(ripple) + + setTimeout(() => { + ripple.remove() + }, 600) + + if (callback) callback() +} + +const handleAchievementClick = (achievement) => { + // Add celebration effect for achievement clicks + createConfetti(event) + + // Show detailed achievement info + ElNotification({ + title: '🏆 学习成就', + message: `${achievement.title}: ${achievement.value} ${achievement.desc || ''}`, + type: 'success', + duration: 3000 + }) +} + +const createConfetti = (event) => { + if (!event) return + + const colors = ['#4facfe', '#00f2fe', '#43e97b', '#38f9d7', '#ff9a9e', '#fecfef'] + const confettiCount = 20 + + for (let i = 0; i < confettiCount; i++) { + setTimeout(() => { + const confetti = document.createElement('div') + confetti.classList.add('confetti-piece') + confetti.style.cssText = ` + position: fixed; + width: 8px; + height: 8px; + background: ${colors[Math.floor(Math.random() * colors.length)]}; + left: ${event.clientX}px; + top: ${event.clientY}px; + border-radius: 50%; + pointer-events: none; + z-index: 10000; + animation: confettiFall 1s ease-out forwards; + ` + + document.body.appendChild(confetti) + + setTimeout(() => confetti.remove(), 1000) + }, i * 30) + } +} + +// Add confetti animation keyframes dynamically +if (!document.querySelector('#confetti-styles')) { + const style = document.createElement('style') + style.id = 'confetti-styles' + style.textContent = ` + @keyframes confettiFall { + 0% { + transform: translate(0, 0) rotate(0deg); + opacity: 1; + } + 100% { + transform: translate(${Math.random() * 200 - 100}px, 100px) rotate(${Math.random() * 720 - 360}deg); + opacity: 0; + } + } + + .ripple-effect { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.6); + transform: scale(0); + animation: ripple 0.6s ease-out; + pointer-events: none; + } + + @keyframes ripple { + to { + transform: scale(4); + opacity: 0; + } + } + ` + document.head.appendChild(style) +} + +const handleStatCardHover = (type, isEntering) => { + if (isEntering) { + // Add subtle glow effect + document.querySelectorAll(`.stat-card-${type}`).forEach(card => { + card.classList.add('glow-effect') + }) + } else { + // Remove glow effect + document.querySelectorAll(`.stat-card-${type}`).forEach(card => { + card.classList.remove('glow-effect') + }) + } +} + +const triggerCelebration = () => { + const celebrationElements = document.querySelectorAll('.achievement-card, .edu-stat-card') + celebrationElements.forEach((element, index) => { + setTimeout(() => { + element.classList.add('pulse-animation') + setTimeout(() => { + element.classList.remove('pulse-animation') + }, 2000) + }, index * 100) + }) +} + +// Enhanced loading state +const showLoadingState = (message = '加载中...') => { + const loadingOverlay = document.createElement('div') + loadingOverlay.classList.add('loading-overlay') + loadingOverlay.innerHTML = ` +
+
+

${message}

+
+ ` + document.body.appendChild(loadingOverlay) + return loadingOverlay +} + +const hideLoadingState = (overlay) => { + if (overlay) { + overlay.remove() + } +} + +// AI Assistant Methods +const sendAiMessage = async () => { + if (!aiInput.value.trim() || aiLoading.value) return + + // Add user message + const userMessage = { + id: chatMessagesIdCounter.value++, + type: 'user', + content: aiInput.value + } + chatMessages.value.push(userMessage) + + const input = aiInput.value + aiInput.value = '' + aiLoading.value = true + + try { + // TODO: Replace with actual AI API call + // const response = await api.post('/ai/assistant', { message: input }) + + // Simulate AI response + await new Promise(resolve => setTimeout(resolve, 1500)) + + const aiResponse = generateAiResponse(input) + const aiMessage = { + id: chatMessagesIdCounter.value++, + type: 'ai', + content: aiResponse + } + chatMessages.value.push(aiMessage) + + // Scroll to bottom + await nextTick() + scrollToBottom() + + } catch (error) { + console.error('AI Assistant error:', error) + const errorMessage = { + id: chatMessagesIdCounter.value++, + type: 'ai', + content: '抱歉,我现在遇到了一些问题,请稍后再试。' + } + chatMessages.value.push(errorMessage) + } finally { + aiLoading.value = false + } +} + +const generateAiResponse = (userInput) => { + const input = userInput.toLowerCase() + + if (input.includes('学习计划') || input.includes('计划')) { + return '根据你的学习情况,我建议:\n1. 今日重点复习数学函数题目(准确率65%)\n2. 英语单词记忆:Unit 5 Vocabulary\n3. 物理错题集中练习牛顿第二定律\n\n预计学习时间:45-60分钟' + } + + if (input.includes('数学') || input.includes('提高')) { + return '要提高数学成绩,建议:\n📊 当前薄弱点:函数、几何\n🎯 练习策略:\n- 每日5道函数题\n- 整理错题集\n- 观看相关视频教程\n\n坚持2周,预计可提升15-20分' + } + + if (input.includes('薄弱') || input.includes('分析')) { + return `📈 你的薄弱知识点分析:\n数学:函数掌握度65%,几何78%\n物理:力学82%,电学70%\n英语:词汇量中等,语法需要加强\n\n建议优先攻克函数和电学知识点` + } + + if (input.includes('资料') || input.includes('推荐')) { + return '为你推荐的学习资料:\n📚 数学:《高中数学精选题集》\n📖 英语:《BBC Learning English》\n🔬 物理:《物理概念图解》\n\n所有资料都可在校内图书馆借阅' + } + + return '我理解你的问题。基于你的学习数据,我建议制定个性化的学习计划。你可以告诉我想了解哪个科目的具体学习建议。' +} + +const scrollToBottom = () => { + const container = document.querySelector('.chat-messages') + if (container) { + container.scrollTop = container.scrollHeight + } +} + +const getProgressClass = (progress) => { + if (progress >= 80) return 'progress-excellent' + if (progress >= 60) return 'progress-good' + if (progress >= 40) return 'progress-average' + return 'progress-low' +} + +const showStudyStats = () => { + ElMessage.info('学习统计功能开发中...') +} + +const goToAccuracyAnalysis = () => { + ElMessage.info('准确率分析功能开发中...') +} + +const applyAiSuggestion = () => { + ElMessage.success('AI学习建议已应用到今日计划!') + // TODO: Navigate to specific practice page +} + onMounted(async () => { await fetchDashboardStats() + + // Initialize today stats (mock data for now) + todayStats.value = { + studyTime: Math.floor(Math.random() * 60) + 15, // 15-75 minutes + completedExercises: Math.floor(Math.random() * 20) + 5, // 5-25 exercises + accuracy: Math.floor(Math.random() * 30) + 70 // 70-100% + } + + // Initialize particles + initParticles() + + // Draw trend chart + nextTick(() => { + drawTrendChart() + }) + + // Trigger welcome celebration after a short delay + setTimeout(() => { + if (learningStreak.value >= 7) { + ElNotification({ + title: '🎉 太棒了!', + message: `你已经连续学习 ${learningStreak.value} 天了,保持这个节奏!`, + type: 'success', + duration: 4000 + }) + triggerCelebration() + } + }, 2000) + + // Add entrance animations completion trigger + setTimeout(() => { + document.querySelectorAll('.modern-stat-card, .subject-card').forEach((element, index) => { + setTimeout(() => { + element.classList.add('glow-effect') + setTimeout(() => { + element.classList.remove('glow-effect') + }, 1000) + }, index * 50) + }) + }, 1500) }) + +// Initialize particle effects +const initParticles = () => { + const container = document.querySelector('.particles-container') + if (!container) return + + for (let i = 0; i < 15; i++) { + const particle = document.createElement('div') + particle.className = 'particle' + + // Random size between 2-6px + const size = Math.random() * 4 + 2 + particle.style.width = size + 'px' + particle.style.height = size + 'px' + + // Random position + particle.style.left = Math.random() * 100 + '%' + particle.style.animationDelay = Math.random() * 15 + 's' + particle.style.animationDuration = (15 + Math.random() * 10) + 's' + + container.appendChild(particle) + } +} \ No newline at end of file