|
|
@@ -0,0 +1,998 @@
|
|
|
+<template>
|
|
|
+ <div class="learning-behavior-analysis">
|
|
|
+ <!-- 页面头部 -->
|
|
|
+ <div class="header">
|
|
|
+ <h1>📊 学习行为分析</h1>
|
|
|
+ <p>全面分析学院教学情况和学员学习行为</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 导航标签 -->
|
|
|
+ <div class="nav-tabs">
|
|
|
+ <div class="nav-tab" :class="{ active: activeTab === 'college' }" @click="switchTab('college')">
|
|
|
+ 🏫 学院维度分析
|
|
|
+ </div>
|
|
|
+ <div class="nav-tab" :class="{ active: activeTab === 'student' }" @click="switchTab('student')">
|
|
|
+ 👤 学员维度分析
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学院维度分析 -->
|
|
|
+ <div v-show="activeTab === 'college'" class="tab-content">
|
|
|
+ <!-- 筛选条件 -->
|
|
|
+ <div class="filter-section">
|
|
|
+ <h3>🔍 数据筛选</h3>
|
|
|
+ <div class="filter-controls">
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>选择学院</label>
|
|
|
+ <a-select v-model:value="collegeFilters.collegeId" placeholder="全部学院" @change="updateCollegeData">
|
|
|
+ <a-select-option value="">全部学院</a-select-option>
|
|
|
+ <a-select-option value="computer">计算机学院</a-select-option>
|
|
|
+ <a-select-option value="business">商学院</a-select-option>
|
|
|
+ <a-select-option value="art">艺术学院</a-select-option>
|
|
|
+ <a-select-option value="science">理学院</a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </div>
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>时间范围</label>
|
|
|
+ <a-select v-model:value="collegeFilters.timeRange" @change="updateCollegeData">
|
|
|
+ <a-select-option :value="7">最近7天</a-select-option>
|
|
|
+ <a-select-option :value="30">最近30天</a-select-option>
|
|
|
+ <a-select-option :value="90">最近90天</a-select-option>
|
|
|
+ <a-select-option :value="365">最近一年</a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </div>
|
|
|
+ <div class="filter-group">
|
|
|
+ <a-button type="primary" @click="updateCollegeData">查询</a-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学院整体统计 -->
|
|
|
+ <div class="stats-grid">
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>📚 课程访问统计</h3>
|
|
|
+ <div class="stat-number">{{ collegeStats.totalCourses }}</div>
|
|
|
+ <div class="stat-label">总课程数</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.activeCourses }}</div>
|
|
|
+ <div class="stat-label">活跃课程数</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.courseAccessRate }}%</div>
|
|
|
+ <div class="stat-label">课程访问率</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>👥 用户登录统计</h3>
|
|
|
+ <div class="stat-number">{{ collegeStats.totalLogins.toLocaleString() }}</div>
|
|
|
+ <div class="stat-label">总登录人次</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.uniqueUsers.toLocaleString() }}</div>
|
|
|
+ <div class="stat-label">独立用户数</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.avgLoginPerUser }}</div>
|
|
|
+ <div class="stat-label">人均登录次数</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>⏰ 观看时长统计</h3>
|
|
|
+ <div class="stat-number">{{ collegeStats.totalOnlineTime }}h</div>
|
|
|
+ <div class="stat-label">总观看时长</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.avgSessionTime }}min</div>
|
|
|
+ <div class="stat-label">平均时长</div>
|
|
|
+ <div class="stat-number">{{ collegeStats.peakOnlineUsers }}</div>
|
|
|
+ <div class="stat-label">峰值观看人数</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 登录时段分析图表 -->
|
|
|
+ <div class="chart-container">
|
|
|
+ <h3>⏰ 用户登录时段分布</h3>
|
|
|
+ <div ref="loginTimeChart" class="chart"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 课程访问热度图表 -->
|
|
|
+ <div class="chart-container">
|
|
|
+ <h3>🔥 课程访问热度排行</h3>
|
|
|
+ <div ref="courseHeatChart" class="chart"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学院课程详细统计 -->
|
|
|
+ <div class="data-table">
|
|
|
+ <h3>📊 学院课程详细统计</h3>
|
|
|
+ <a-table
|
|
|
+ :columns="collegeColumns"
|
|
|
+ :data-source="collegeTableData"
|
|
|
+ :pagination="{ pageSize: 10 }"
|
|
|
+ row-key="id"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学员维度分析 -->
|
|
|
+ <div v-show="activeTab === 'student'" class="tab-content">
|
|
|
+ <!-- 学员搜索 -->
|
|
|
+ <div class="filter-section">
|
|
|
+ <h3>🔍 学员搜索</h3>
|
|
|
+ <div class="filter-controls">
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>学员姓名/学号</label>
|
|
|
+ <a-input v-model:value="studentSearch" placeholder="输入学员姓名或学号" @pressEnter="searchStudent" />
|
|
|
+ </div>
|
|
|
+ <div class="filter-group">
|
|
|
+ <a-button type="primary" @click="searchStudent">搜索</a-button>
|
|
|
+ </div>
|
|
|
+ <div class="filter-group">
|
|
|
+ <a-button @click="clearSearch">清除</a-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学员学习行为卡片 -->
|
|
|
+ <div class="student-cards">
|
|
|
+ <div
|
|
|
+ v-for="student in filteredStudents"
|
|
|
+ :key="student.id"
|
|
|
+ class="student-card"
|
|
|
+ :class="{ highlighted: student.highlighted }"
|
|
|
+ >
|
|
|
+ <div class="student-header">
|
|
|
+ <div class="student-info">
|
|
|
+ <h4>{{ student.name }} ({{ student.studentId }})</h4>
|
|
|
+ <div class="student-meta">{{ student.college }} | {{ student.major }} | {{ student.grade }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="student-stats">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ student.overallProgress }}%</div>
|
|
|
+ <div class="stat-label">总体进度</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="course-progress">
|
|
|
+ <div v-for="course in student.courses" :key="course.id" class="course-item">
|
|
|
+ <div class="course-name">{{ course.name }}</div>
|
|
|
+ <div class="course-stats">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ course.progress }}%</div>
|
|
|
+ <div class="stat-label">学习进度</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ course.completedAssignments }}/{{ course.totalAssignments }}</div>
|
|
|
+ <div class="stat-label">作业完成</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ course.discussionParticipation }}</div>
|
|
|
+ <div class="stat-label">讨论参与</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ course.questionCount }}</div>
|
|
|
+ <div class="stat-label">提问次数</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
|
|
+ import * as echarts from 'echarts'
|
|
|
+
|
|
|
+ // 当前活动标签
|
|
|
+ const activeTab = ref('college')
|
|
|
+
|
|
|
+ // 学院维度筛选条件
|
|
|
+ const collegeFilters = reactive({
|
|
|
+ collegeId: '',
|
|
|
+ timeRange: 30
|
|
|
+ })
|
|
|
+
|
|
|
+ // 学院统计数据
|
|
|
+ const collegeStats = reactive({
|
|
|
+ totalCourses: 156,
|
|
|
+ activeCourses: 142,
|
|
|
+ courseAccessRate: 91.0,
|
|
|
+ totalLogins: 12456,
|
|
|
+ uniqueUsers: 3234,
|
|
|
+ avgLoginPerUser: 3.85,
|
|
|
+ totalOnlineTime: 2456,
|
|
|
+ avgSessionTime: 45.2,
|
|
|
+ peakOnlineUsers: 234
|
|
|
+ })
|
|
|
+
|
|
|
+ // 图表引用
|
|
|
+ const loginTimeChart = ref(null)
|
|
|
+ const courseHeatChart = ref(null)
|
|
|
+ let loginTimeChartInstance = null
|
|
|
+ let courseHeatChartInstance = null
|
|
|
+
|
|
|
+ // 学院表格列定义
|
|
|
+ const collegeColumns = [
|
|
|
+ {
|
|
|
+ title: '学院名称',
|
|
|
+ dataIndex: 'name',
|
|
|
+ key: 'name'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '课程数量',
|
|
|
+ dataIndex: 'courseCount',
|
|
|
+ key: 'courseCount'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '总访问量',
|
|
|
+ dataIndex: 'totalVisits',
|
|
|
+ key: 'totalVisits'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '平均完成率',
|
|
|
+ dataIndex: 'avgCompletionRate',
|
|
|
+ key: 'avgCompletionRate',
|
|
|
+ customRender: ({ text }) => `<span class="completion-rate">${text}%</span>`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '作业提交率',
|
|
|
+ dataIndex: 'assignmentSubmissionRate',
|
|
|
+ key: 'assignmentSubmissionRate',
|
|
|
+ customRender: ({ text }) => `<span class="completion-rate">${text}%</span>`
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 学院表格数据
|
|
|
+ const collegeTableData = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '计算机学院',
|
|
|
+ courseCount: 45,
|
|
|
+ totalVisits: 5234,
|
|
|
+ avgCompletionRate: 78.5,
|
|
|
+ assignmentSubmissionRate: 85.2
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '商学院',
|
|
|
+ courseCount: 38,
|
|
|
+ totalVisits: 4567,
|
|
|
+ avgCompletionRate: 72.3,
|
|
|
+ assignmentSubmissionRate: 79.8
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '艺术学院',
|
|
|
+ courseCount: 28,
|
|
|
+ totalVisits: 3123,
|
|
|
+ avgCompletionRate: 68.9,
|
|
|
+ assignmentSubmissionRate: 76.4
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: '理学院',
|
|
|
+ courseCount: 35,
|
|
|
+ totalVisits: 3890,
|
|
|
+ avgCompletionRate: 75.2,
|
|
|
+ assignmentSubmissionRate: 82.1
|
|
|
+ }
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 学员搜索
|
|
|
+ const studentSearch = ref('')
|
|
|
+
|
|
|
+ // 学员数据
|
|
|
+ const studentsData = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '张三',
|
|
|
+ studentId: '2021001',
|
|
|
+ college: '计算机学院',
|
|
|
+ major: '计算机科学与技术',
|
|
|
+ grade: '大三',
|
|
|
+ overallProgress: 85.6,
|
|
|
+ highlighted: false,
|
|
|
+ courses: [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: 'JavaScript程序设计',
|
|
|
+ progress: 92,
|
|
|
+ completedAssignments: 8,
|
|
|
+ totalAssignments: 10,
|
|
|
+ discussionParticipation: 15,
|
|
|
+ questionCount: 5
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '数据结构与算法',
|
|
|
+ progress: 78,
|
|
|
+ completedAssignments: 6,
|
|
|
+ totalAssignments: 8,
|
|
|
+ discussionParticipation: 12,
|
|
|
+ questionCount: 3
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '李四',
|
|
|
+ studentId: '2021002',
|
|
|
+ college: '商学院',
|
|
|
+ major: '工商管理',
|
|
|
+ grade: '大二',
|
|
|
+ overallProgress: 72.3,
|
|
|
+ highlighted: false,
|
|
|
+ courses: [
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '管理学原理',
|
|
|
+ progress: 85,
|
|
|
+ completedAssignments: 7,
|
|
|
+ totalAssignments: 9,
|
|
|
+ discussionParticipation: 8,
|
|
|
+ questionCount: 2
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: '市场营销学',
|
|
|
+ progress: 65,
|
|
|
+ completedAssignments: 4,
|
|
|
+ totalAssignments: 7,
|
|
|
+ discussionParticipation: 6,
|
|
|
+ questionCount: 1
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '王五',
|
|
|
+ studentId: '2021003',
|
|
|
+ college: '艺术学院',
|
|
|
+ major: '视觉传达设计',
|
|
|
+ grade: '大一',
|
|
|
+ overallProgress: 91.2,
|
|
|
+ highlighted: false,
|
|
|
+ courses: [
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ name: '设计基础',
|
|
|
+ progress: 95,
|
|
|
+ completedAssignments: 10,
|
|
|
+ totalAssignments: 10,
|
|
|
+ discussionParticipation: 20,
|
|
|
+ questionCount: 8
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ name: '色彩构成',
|
|
|
+ progress: 88,
|
|
|
+ completedAssignments: 8,
|
|
|
+ totalAssignments: 9,
|
|
|
+ discussionParticipation: 15,
|
|
|
+ questionCount: 6
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 过滤后的学员数据
|
|
|
+ const filteredStudents = computed(() => {
|
|
|
+ if (!studentSearch.value.trim()) {
|
|
|
+ return studentsData.value.map((student) => ({ ...student, highlighted: false }))
|
|
|
+ }
|
|
|
+
|
|
|
+ const searchTerm = studentSearch.value.trim().toLowerCase()
|
|
|
+ return studentsData.value
|
|
|
+ .filter((student) => {
|
|
|
+ const matchName = student.name.toLowerCase().includes(searchTerm)
|
|
|
+ const matchId = student.studentId.includes(searchTerm)
|
|
|
+ return matchName || matchId
|
|
|
+ })
|
|
|
+ .map((student) => ({ ...student, highlighted: true }))
|
|
|
+ }) // 切换标签
|
|
|
+ const switchTab = (tabName) => {
|
|
|
+ activeTab.value = tabName
|
|
|
+
|
|
|
+ if (tabName === 'college') {
|
|
|
+ nextTick(() => {
|
|
|
+ initCollegeCharts()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新学院数据
|
|
|
+ const updateCollegeData = () => {
|
|
|
+ console.log('更新学院数据:', collegeFilters)
|
|
|
+
|
|
|
+ // 根据筛选条件更新统计数据
|
|
|
+ if (collegeFilters.collegeId === 'computer') {
|
|
|
+ Object.assign(collegeStats, {
|
|
|
+ totalCourses: 45,
|
|
|
+ activeCourses: 42,
|
|
|
+ totalLogins: 5234,
|
|
|
+ uniqueUsers: 1234,
|
|
|
+ avgLoginPerUser: 4.24,
|
|
|
+ totalOnlineTime: 1234,
|
|
|
+ avgSessionTime: 52.3,
|
|
|
+ peakOnlineUsers: 156
|
|
|
+ })
|
|
|
+ } else if (collegeFilters.collegeId === 'business') {
|
|
|
+ Object.assign(collegeStats, {
|
|
|
+ totalCourses: 38,
|
|
|
+ activeCourses: 35,
|
|
|
+ totalLogins: 4567,
|
|
|
+ uniqueUsers: 987,
|
|
|
+ avgLoginPerUser: 4.63,
|
|
|
+ totalOnlineTime: 987,
|
|
|
+ avgSessionTime: 48.7,
|
|
|
+ peakOnlineUsers: 123
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 全部学院
|
|
|
+ Object.assign(collegeStats, {
|
|
|
+ totalCourses: 156,
|
|
|
+ activeCourses: 142,
|
|
|
+ totalLogins: 12456,
|
|
|
+ uniqueUsers: 3234,
|
|
|
+ avgLoginPerUser: 3.85,
|
|
|
+ totalOnlineTime: 2456,
|
|
|
+ avgSessionTime: 45.2,
|
|
|
+ peakOnlineUsers: 234
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新初始化图表
|
|
|
+ initCollegeCharts()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 搜索学员
|
|
|
+ const searchStudent = () => {
|
|
|
+ if (!studentSearch.value.trim()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const found = filteredStudents.value.length > 0
|
|
|
+ if (!found) {
|
|
|
+ alert(`未找到包含"${studentSearch.value}"的学员`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除搜索
|
|
|
+ const clearSearch = () => {
|
|
|
+ studentSearch.value = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化学院维度图表
|
|
|
+ const initCollegeCharts = () => {
|
|
|
+ initLoginTimeChart()
|
|
|
+ initCourseHeatChart()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 登录时段分布图表
|
|
|
+ const initLoginTimeChart = () => {
|
|
|
+ if (!loginTimeChart.value) return
|
|
|
+
|
|
|
+ if (loginTimeChartInstance) {
|
|
|
+ loginTimeChartInstance.dispose()
|
|
|
+ }
|
|
|
+
|
|
|
+ loginTimeChartInstance = echarts.init(loginTimeChart.value)
|
|
|
+
|
|
|
+ const hours = [
|
|
|
+ '00:00',
|
|
|
+ '02:00',
|
|
|
+ '04:00',
|
|
|
+ '06:00',
|
|
|
+ '08:00',
|
|
|
+ '10:00',
|
|
|
+ '12:00',
|
|
|
+ '14:00',
|
|
|
+ '16:00',
|
|
|
+ '18:00',
|
|
|
+ '20:00',
|
|
|
+ '22:00'
|
|
|
+ ]
|
|
|
+ const loginCounts = [45, 23, 12, 8, 156, 234, 189, 267, 312, 298, 345, 289]
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ title: {
|
|
|
+ text: '24小时登录时段分布',
|
|
|
+ left: 'center',
|
|
|
+ textStyle: {
|
|
|
+ color: '#2c3e50',
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#3498db',
|
|
|
+ borderWidth: 1,
|
|
|
+ textStyle: {
|
|
|
+ color: '#333'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: hours,
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#bdc3c7'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '登录人次',
|
|
|
+ nameTextStyle: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#bdc3c7'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#ecf0f1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '登录人次',
|
|
|
+ type: 'bar',
|
|
|
+ data: loginCounts,
|
|
|
+ itemStyle: {
|
|
|
+ color: {
|
|
|
+ type: 'linear',
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ x2: 0,
|
|
|
+ y2: 1,
|
|
|
+ colorStops: [
|
|
|
+ {
|
|
|
+ offset: 0,
|
|
|
+ color: '#3498db'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ offset: 1,
|
|
|
+ color: '#2980b9'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ color: '#e74c3c'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ loginTimeChartInstance.setOption(option)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 课程访问热度图表
|
|
|
+ const initCourseHeatChart = () => {
|
|
|
+ if (!courseHeatChart.value) return
|
|
|
+
|
|
|
+ if (courseHeatChartInstance) {
|
|
|
+ courseHeatChartInstance.dispose()
|
|
|
+ }
|
|
|
+
|
|
|
+ courseHeatChartInstance = echarts.init(courseHeatChart.value)
|
|
|
+
|
|
|
+ const courses = [
|
|
|
+ 'JavaScript程序设计',
|
|
|
+ '数据结构与算法',
|
|
|
+ '管理学原理',
|
|
|
+ '设计基础',
|
|
|
+ '市场营销学',
|
|
|
+ '色彩构成',
|
|
|
+ 'Python数据分析',
|
|
|
+ 'React前端开发'
|
|
|
+ ]
|
|
|
+ const visits = [1234, 1156, 987, 856, 789, 756, 678, 645]
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ title: {
|
|
|
+ text: '课程访问热度TOP8',
|
|
|
+ left: 'center',
|
|
|
+ textStyle: {
|
|
|
+ color: '#2c3e50',
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#3498db',
|
|
|
+ borderWidth: 1,
|
|
|
+ textStyle: {
|
|
|
+ color: '#333'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '访问量',
|
|
|
+ nameTextStyle: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#bdc3c7'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#ecf0f1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: courses,
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#bdc3c7'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#2c3e50'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '访问量',
|
|
|
+ type: 'bar',
|
|
|
+ data: visits,
|
|
|
+ itemStyle: {
|
|
|
+ color: {
|
|
|
+ type: 'linear',
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ x2: 1,
|
|
|
+ y2: 0,
|
|
|
+ colorStops: [
|
|
|
+ {
|
|
|
+ offset: 0,
|
|
|
+ color: '#e74c3c'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ offset: 1,
|
|
|
+ color: '#f39c12'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ color: '#3498db'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ courseHeatChartInstance.setOption(option)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 响应式处理
|
|
|
+ const handleResize = () => {
|
|
|
+ loginTimeChartInstance?.resize()
|
|
|
+ courseHeatChartInstance?.resize()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 组件挂载后初始化
|
|
|
+ onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ initCollegeCharts()
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // 组件卸载时清理
|
|
|
+ onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+ loginTimeChartInstance?.dispose()
|
|
|
+ courseHeatChartInstance?.dispose()
|
|
|
+ })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .learning-behavior-analysis {
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 30px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 2.5em;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header p {
|
|
|
+ color: #7f8c8d;
|
|
|
+ font-size: 1.1em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-tabs {
|
|
|
+ display: flex;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 5px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-tab {
|
|
|
+ flex: 1;
|
|
|
+ padding: 15px 20px;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 10px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #7f8c8d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-tab.active {
|
|
|
+ background: linear-gradient(135deg, #3498db, #2980b9);
|
|
|
+ color: white;
|
|
|
+ box-shadow: 0 4px 15px rgba(52, 152, 219, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-tab:hover:not(.active) {
|
|
|
+ background: rgba(52, 152, 219, 0.1);
|
|
|
+ color: #3498db;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-content {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-section {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 25px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-section h3 {
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 1.5em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 15px;
|
|
|
+ align-items: end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-group label {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ color: #2c3e50;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
+ gap: 25px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card h3 {
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ font-size: 1.2em;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-number {
|
|
|
+ font-size: 2.2em;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #3498db;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ color: #7f8c8d;
|
|
|
+ font-size: 0.9em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 25px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container h3 {
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 1.5em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart {
|
|
|
+ height: 400px;
|
|
|
+ border-radius: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-table {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 25px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-table h3 {
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 1.5em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-cards {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-card {
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ transition: transform 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-card.highlighted {
|
|
|
+ border: 2px solid #3498db;
|
|
|
+ box-shadow: 0 8px 32px rgba(52, 152, 219, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 2px solid #ecf0f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-info h4 {
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 1.3em;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-meta {
|
|
|
+ color: #7f8c8d;
|
|
|
+ font-size: 0.9em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-progress {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 0;
|
|
|
+ border-bottom: 1px solid #ecf0f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2c3e50;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-stats {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ font-size: 0.9em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-item {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #3498db;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ color: #7f8c8d;
|
|
|
+ font-size: 0.8em;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.completion-rate) {
|
|
|
+ color: #27ae60;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.low-engagement) {
|
|
|
+ color: #e74c3c;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .stats-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-controls {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ font-size: 2em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .nav-tabs {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-stats {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|