Kaynağa Gözat

feat: 新增统计分析模块和历史开课统计功能

refactor: 优化试卷管理表单和考试管理表单的班级选择功能

fix: 修复学生考试记录页面的搜索功能和表格样式

style: 调整试卷查看和编辑页面的布局和样式

chore: 添加mockjs依赖并配置统计分析相关接口的mock数据

docs: 更新统计分析模块的接口文档和注释

test: 添加统计分析模块的测试用例和数据

build: 更新package.json依赖版本

perf: 优化平台状态总览页面的数据加载和图表渲染性能
tanshanming 6 ay önce
ebeveyn
işleme
7d23c6820c

+ 1 - 0
package.json

@@ -88,6 +88,7 @@
 		"eslint-plugin-prettier": "4.2.1",
 		"eslint-plugin-vue": "9.7.0",
 		"less": "4.1.3",
+		"mockjs": "^1.1.0",
 		"postcss": "8.4.21",
 		"prettier": "2.8.7",
 		"rollup-plugin-visualizer": "5.8.3",

+ 507 - 0
src/api/statisticalAnalysis/analysisLearningBehaviors.js

@@ -0,0 +1,507 @@
+// 学习行为分析相关接口
+import { moduleRequest } from '@/utils/request'
+import Mock from 'mockjs'
+
+const request = moduleRequest(`/api/webapp/`)
+
+// Mock 数据配置
+Mock.setup({
+	timeout: '200-600'
+})
+
+// 学院统计数据映射
+const collegeStatsMap = {
+	'': {
+		totalCourses: 156,
+		activeCourses: 142,
+		courseAccessRate: 91.0,
+		totalLogins: 12456,
+		uniqueUsers: 3234,
+		avgLoginPerUser: 3.85,
+		totalOnlineTime: 2456,
+		avgSessionTime: 45.2,
+		peakOnlineUsers: 234
+	},
+	computer: {
+		totalCourses: 45,
+		activeCourses: 42,
+		courseAccessRate: 93.3,
+		totalLogins: 5234,
+		uniqueUsers: 1234,
+		avgLoginPerUser: 4.24,
+		totalOnlineTime: 1234,
+		avgSessionTime: 52.3,
+		peakOnlineUsers: 156
+	},
+	business: {
+		totalCourses: 38,
+		activeCourses: 35,
+		courseAccessRate: 92.1,
+		totalLogins: 4567,
+		uniqueUsers: 987,
+		avgLoginPerUser: 4.63,
+		totalOnlineTime: 987,
+		avgSessionTime: 48.7,
+		peakOnlineUsers: 123
+	},
+	art: {
+		totalCourses: 28,
+		activeCourses: 25,
+		courseAccessRate: 89.3,
+		totalLogins: 3123,
+		uniqueUsers: 756,
+		avgLoginPerUser: 4.13,
+		totalOnlineTime: 756,
+		avgSessionTime: 42.1,
+		peakOnlineUsers: 89
+	},
+	science: {
+		totalCourses: 35,
+		activeCourses: 32,
+		courseAccessRate: 91.4,
+		totalLogins: 3890,
+		uniqueUsers: 892,
+		avgLoginPerUser: 4.36,
+		totalOnlineTime: 892,
+		avgSessionTime: 46.8,
+		peakOnlineUsers: 112
+	},
+	engineering: {
+		totalCourses: 42,
+		activeCourses: 39,
+		courseAccessRate: 92.9,
+		totalLogins: 4123,
+		uniqueUsers: 1045,
+		avgLoginPerUser: 3.95,
+		totalOnlineTime: 1045,
+		avgSessionTime: 47.5,
+		peakOnlineUsers: 134
+	}
+}
+
+// 学员数据
+const studentsData = [
+	{
+		id: 1,
+		name: '张三',
+		studentId: '2021001',
+		college: '计算机学院',
+		major: '计算机科学与技术',
+		grade: '大三',
+		overallProgress: 85.6,
+		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,
+		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,
+		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
+			}
+		]
+	},
+	{
+		id: 4,
+		name: '赵六',
+		studentId: '2021004',
+		college: '理学院',
+		major: '数学与应用数学',
+		grade: '大二',
+		overallProgress: 79.4,
+		courses: [
+			{
+				id: 7,
+				name: '高等数学',
+				progress: 82,
+				completedAssignments: 9,
+				totalAssignments: 12,
+				discussionParticipation: 10,
+				questionCount: 4
+			},
+			{
+				id: 8,
+				name: '线性代数',
+				progress: 76,
+				completedAssignments: 6,
+				totalAssignments: 8,
+				discussionParticipation: 8,
+				questionCount: 2
+			}
+		]
+	},
+	{
+		id: 5,
+		name: '孙七',
+		studentId: '2021005',
+		college: '工程学院',
+		major: '机械工程',
+		grade: '大三',
+		overallProgress: 88.1,
+		courses: [
+			{
+				id: 9,
+				name: '机械设计',
+				progress: 90,
+				completedAssignments: 11,
+				totalAssignments: 12,
+				discussionParticipation: 18,
+				questionCount: 7
+			},
+			{
+				id: 10,
+				name: '工程力学',
+				progress: 86,
+				completedAssignments: 8,
+				totalAssignments: 10,
+				discussionParticipation: 14,
+				questionCount: 5
+			}
+		]
+	}
+]
+
+// Mock 接口定义
+Mock.mock(/\/api\/webapp\/analysis\/colleges/, 'get', {
+	code: 200,
+	message: '获取成功',
+	data: [
+		{ id: '', name: '全部学院' },
+		{ id: 'computer', name: '计算机学院' },
+		{ id: 'business', name: '商学院' },
+		{ id: 'art', name: '艺术学院' },
+		{ id: 'science', name: '理学院' },
+		{ id: 'engineering', name: '工程学院' }
+	]
+})
+
+Mock.mock(/\/api\/webapp\/analysis\/college-stats/, 'get', (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const collegeId = params.get('collegeId') || ''
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: collegeStatsMap[collegeId] || collegeStatsMap['']
+	}
+})
+
+Mock.mock(/\/api\/webapp\/analysis\/login-time-distribution/, 'get', (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const timeRange = parseInt(params.get('timeRange')) || 30
+	const collegeId = params.get('collegeId') || ''
+
+	// 基础时间段
+	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'
+	]
+
+	// 根据时间范围生成不同的数据模式
+	let loginCounts = []
+
+	switch (timeRange) {
+		case 7: // 最近7天 - 数据相对较少,波动较大
+			loginCounts = [12, 8, 3, 2, 45, 89, 67, 123, 156, 134, 178, 98]
+			break
+		case 30: // 最近30天 - 中等数据量
+			loginCounts = [45, 23, 12, 8, 156, 234, 189, 267, 312, 298, 345, 289]
+			break
+		case 90: // 最近90天 - 数据量较大,趋势更明显
+			loginCounts = [134, 89, 45, 23, 456, 678, 567, 789, 923, 867, 1023, 756]
+			break
+		case 365: // 最近一年 - 数据量最大,趋势稳定
+			loginCounts = [567, 345, 234, 123, 1234, 1789, 1567, 2134, 2456, 2298, 2678, 1987]
+			break
+		default:
+			loginCounts = [45, 23, 12, 8, 156, 234, 189, 267, 312, 298, 345, 289]
+	}
+
+	// 根据学院调整数据(不同学院有不同的活跃时段特点)
+	if (collegeId) {
+		const collegeMultipliers = {
+			computer: [0.8, 0.7, 0.5, 0.4, 1.2, 1.3, 1.1, 1.4, 1.5, 1.3, 1.6, 1.2], // 计算机学院晚上更活跃
+			business: [0.6, 0.5, 0.3, 0.2, 1.1, 1.4, 1.3, 1.2, 1.1, 1.0, 0.9, 0.8], // 商学院工作时间更活跃
+			art: [1.2, 1.0, 0.8, 0.6, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.3], // 艺术学院夜猫子较多
+			science: [0.7, 0.6, 0.4, 0.3, 1.0, 1.2, 1.1, 1.3, 1.2, 1.1, 1.0, 0.9], // 理学院相对规律
+			engineering: [0.8, 0.7, 0.5, 0.4, 1.1, 1.3, 1.2, 1.4, 1.3, 1.2, 1.1, 1.0] // 工程学院
+		}
+
+		const multiplier = collegeMultipliers[collegeId]
+		if (multiplier) {
+			loginCounts = loginCounts.map((count, index) => Math.round(count * multiplier[index]))
+		}
+	}
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			hours,
+			loginCounts
+		}
+	}
+})
+
+Mock.mock(/\/api\/webapp\/analysis\/course-heat/, 'get', (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const timeRange = parseInt(params.get('timeRange')) || 30
+	const collegeId = params.get('collegeId') || ''
+
+	// 不同学院的课程数据
+	const collegeCoursesMap = {
+		'': {
+			courses: [
+				'JavaScript程序设计',
+				'数据结构与算法',
+				'管理学原理',
+				'设计基础',
+				'市场营销学',
+				'色彩构成',
+				'Python数据分析',
+				'React前端开发'
+			],
+			baseVisits: [1234, 1156, 987, 856, 789, 756, 678, 645]
+		},
+		computer: {
+			courses: [
+				'JavaScript程序设计',
+				'数据结构与算法',
+				'Python数据分析',
+				'React前端开发',
+				'Java程序设计',
+				'数据库原理',
+				'计算机网络',
+				'操作系统'
+			],
+			baseVisits: [1456, 1234, 1123, 987, 856, 789, 678, 567]
+		},
+		business: {
+			courses: [
+				'管理学原理',
+				'市场营销学',
+				'财务管理',
+				'人力资源管理',
+				'战略管理',
+				'组织行为学',
+				'商务谈判',
+				'创业管理'
+			],
+			baseVisits: [1345, 1234, 1098, 987, 876, 765, 654, 543]
+		},
+		art: {
+			courses: ['设计基础', '色彩构成', '平面设计', 'UI设计', '插画设计', '品牌设计', '包装设计', '网页设计'],
+			baseVisits: [1234, 1123, 1012, 901, 789, 678, 567, 456]
+		},
+		science: {
+			courses: ['高等数学', '线性代数', '概率论与数理统计', '数学分析', '离散数学', '数值分析', '运筹学', '数学建模'],
+			baseVisits: [1567, 1345, 1234, 1123, 1012, 901, 789, 678]
+		},
+		engineering: {
+			courses: ['机械设计', '工程力学', '材料力学', '流体力学', '热力学', '控制工程', '机械制图', '工程材料'],
+			baseVisits: [1456, 1345, 1234, 1123, 1012, 901, 789, 678]
+		}
+	}
+
+	const courseData = collegeCoursesMap[collegeId] || collegeCoursesMap['']
+
+	// 根据时间范围调整访问量
+	let timeMultiplier = 1
+	switch (timeRange) {
+		case 7:
+			timeMultiplier = 0.3
+			break
+		case 30:
+			timeMultiplier = 1
+			break
+		case 90:
+			timeMultiplier = 2.5
+			break
+		case 365:
+			timeMultiplier = 8
+			break
+	}
+
+	const visits = courseData.baseVisits.map((visit) => Math.round(visit * timeMultiplier * (0.8 + Math.random() * 0.4)))
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			courses: courseData.courses,
+			visits
+		}
+	}
+})
+
+Mock.mock(/\/api\/webapp\/analysis\/college-course-details/, 'get', {
+	code: 200,
+	message: '获取成功',
+	data: [
+		{
+			id: 1,
+			name: '计算机学院',
+			courseCount: 45,
+			totalVisits: 5234,
+			avgCompletionRate: 78.5,
+			assignmentSubmissionRate: 85.2,
+			dropoutRate: 10.2
+		},
+		{
+			id: 2,
+			name: '商学院',
+			courseCount: 38,
+			totalVisits: 4567,
+			avgCompletionRate: 72.3,
+			assignmentSubmissionRate: 79.8,
+			dropoutRate: 12.5
+		},
+		{
+			id: 3,
+			name: '艺术学院',
+			courseCount: 28,
+			totalVisits: 3123,
+			avgCompletionRate: 68.9,
+			assignmentSubmissionRate: 76.4,
+			dropoutRate: 15.2
+		},
+		{
+			id: 4,
+			name: '理学院',
+			courseCount: 35,
+			totalVisits: 3890,
+			avgCompletionRate: 75.2,
+			assignmentSubmissionRate: 82.1,
+			dropoutRate: 13.8
+		},
+		{
+			id: 5,
+			name: '工程学院',
+			courseCount: 42,
+			totalVisits: 4123,
+			avgCompletionRate: 76.8,
+			assignmentSubmissionRate: 83.5,
+			dropoutRate: 11.7
+		}
+	]
+})
+
+Mock.mock(/\/api\/webapp\/analysis\/search-students/, 'get', (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const keyword = params.get('keyword') || ''
+
+	let filteredStudents = studentsData
+	if (keyword) {
+		filteredStudents = studentsData.filter(
+			(student) => student.name.includes(keyword) || student.studentId.includes(keyword)
+		)
+	}
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: filteredStudents
+	}
+})
+
+// API 函数导出
+export const getCollegeList = () => {
+	return request('analysis/colleges', '', 'get')
+}
+
+export const getCollegeStats = (params) => {
+	return request('analysis/college-stats', params, 'get')
+}
+
+export const getLoginTimeDistribution = (params) => {
+	return request('analysis/login-time-distribution', params, 'get')
+}
+
+export const getCourseHeat = (params) => {
+	return request('analysis/course-heat', params, 'get')
+}
+
+export const getCollegeCourseDetails = (params) => {
+	return request('analysis/college-course-details', params, 'get')
+}
+
+export const searchStudents = (params) => {
+	return request('analysis/search-students', params, 'get')
+}

+ 533 - 0
src/api/statisticalAnalysis/analysisTeachingActivities.js

@@ -0,0 +1,533 @@
+// 教学活动分析相关接口
+import { moduleRequest } from '@/utils/request'
+import Mock from 'mockjs'
+
+const request = moduleRequest(`/api/webapp/`)
+
+// Mock 数据配置
+Mock.setup({
+	timeout: '200-600'
+})
+
+// 课程选项数据
+const courseOptions = [
+	{ value: '', label: '全部课程' },
+	{ value: 'course1', label: 'JavaScript基础教程' },
+	{ value: 'course2', label: 'Python数据分析' },
+	{ value: 'course3', label: 'React前端开发' },
+	{ value: 'course4', label: '机器学习入门' },
+	{ value: 'course5', label: 'Vue.js实战开发' },
+	{ value: 'course6', label: 'Node.js后端开发' }
+]
+
+// 生成日期数据
+const generateDateData = (days) => {
+	const dates = []
+	const today = new Date()
+
+	for (let i = days - 1; i >= 0; i--) {
+		const date = new Date(today)
+		date.setDate(date.getDate() - i)
+		dates.push(date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }))
+	}
+
+	return dates
+}
+
+// 生成访问数据
+const generateVisitData = (days, courseId = '') => {
+	const data = []
+	// 全部课程的访问量应该比单个课程高
+	const courseMultiplier = courseId ? 0.15 + Math.random() * 0.25 : 1.0
+
+	for (let i = 0; i < days; i++) {
+		const dayOfWeek = (new Date().getDay() - days + i + 7) % 7
+		const isWeekend = dayOfWeek === 0 || dayOfWeek === 6
+		// 全部课程基础访问量更高
+		const baseValue = isWeekend ? 450 : 680
+		const randomFactor = 0.7 + Math.random() * 0.6
+		const finalValue = Math.round(baseValue * randomFactor * courseMultiplier)
+		data.push(finalValue)
+	}
+	return data
+}
+
+// 讨论数据 - 这是全部课程的汇总讨论数据
+const discussionData = [
+	{
+		id: 1,
+		title: 'JavaScript闭包概念理解',
+		author: '张三',
+		createTime: '2024-01-15 10:30',
+		replyCount: 25,
+		lastReplyTime: '2024-01-16 14:20'
+	},
+	{
+		id: 2,
+		title: 'Python数据分析库选择',
+		author: '李四',
+		createTime: '2024-01-14 16:45',
+		replyCount: 18,
+		lastReplyTime: '2024-01-15 09:15'
+	},
+	{
+		id: 3,
+		title: 'React组件生命周期',
+		author: '王五',
+		createTime: '2024-01-13 11:20',
+		replyCount: 32,
+		lastReplyTime: '2024-01-14 17:30'
+	},
+	{
+		id: 4,
+		title: '机器学习算法选择',
+		author: '赵六',
+		createTime: '2024-01-12 14:15',
+		replyCount: 16,
+		lastReplyTime: '2024-01-13 10:45'
+	},
+	{
+		id: 5,
+		title: '前端性能优化技巧',
+		author: '钱七',
+		createTime: '2024-01-11 09:30',
+		replyCount: 45,
+		lastReplyTime: '2024-01-15 16:20'
+	},
+	{
+		id: 6,
+		title: 'Vue3 Composition API使用心得',
+		author: '孙八',
+		createTime: '2024-01-10 15:20',
+		replyCount: 38,
+		lastReplyTime: '2024-01-14 11:30'
+	},
+	{
+		id: 7,
+		title: 'Node.js异步编程最佳实践',
+		author: '周九',
+		createTime: '2024-01-09 13:45',
+		replyCount: 29,
+		lastReplyTime: '2024-01-13 16:15'
+	},
+	{
+		id: 8,
+		title: '数据库索引优化策略',
+		author: '陈十',
+		createTime: '2024-01-08 11:20',
+		replyCount: 22,
+		lastReplyTime: '2024-01-12 15:40'
+	},
+	{
+		id: 9,
+		title: 'Web安全防护实践',
+		author: '刘十一',
+		createTime: '2024-01-07 14:30',
+		replyCount: 19,
+		lastReplyTime: '2024-01-11 09:25'
+	},
+	{
+		id: 10,
+		title: '移动端适配解决方案',
+		author: '杨十二',
+		createTime: '2024-01-06 16:15',
+		replyCount: 27,
+		lastReplyTime: '2024-01-10 13:50'
+	},
+	{
+		id: 11,
+		title: 'TypeScript类型系统深入',
+		author: '黄十三',
+		createTime: '2024-01-05 10:45',
+		replyCount: 34,
+		lastReplyTime: '2024-01-09 17:20'
+	},
+	{
+		id: 12,
+		title: 'Docker容器化部署',
+		author: '吴十四',
+		createTime: '2024-01-04 13:20',
+		replyCount: 21,
+		lastReplyTime: '2024-01-08 11:15'
+	},
+	{
+		id: 13,
+		title: 'GraphQL API设计',
+		author: '郑十五',
+		createTime: '2024-01-03 15:40',
+		replyCount: 15,
+		lastReplyTime: '2024-01-07 14:30'
+	},
+	{
+		id: 14,
+		title: '微服务架构实践',
+		author: '王十六',
+		createTime: '2024-01-02 09:15',
+		replyCount: 28,
+		lastReplyTime: '2024-01-06 16:45'
+	},
+	{
+		id: 15,
+		title: 'Redis缓存策略',
+		author: '李十七',
+		createTime: '2024-01-01 11:30',
+		replyCount: 23,
+		lastReplyTime: '2024-01-05 12:20'
+	}
+]
+
+// 文档数据 - 这是全部课程的汇总数据,数量较大
+const documentData = [
+	{
+		id: 1,
+		name: 'JavaScript基础语法',
+		type: 'PDF文档',
+		viewCount: 4856,
+		completedCount: 3234,
+		completionRate: 66.5,
+		avgReadTime: '25:30',
+		exitRate: 24.6,
+		downloadCount: 1456
+	},
+	{
+		id: 2,
+		name: 'Python数据分析入门',
+		type: '在线文档',
+		viewCount: 3234,
+		completedCount: 2587,
+		completionRate: 80.0,
+		avgReadTime: '32:15',
+		exitRate: 18.5,
+		downloadCount: 1234
+	},
+	{
+		id: 3,
+		name: 'React组件开发指南',
+		type: 'PDF文档',
+		viewCount: 2987,
+		completedCount: 2289,
+		completionRate: 76.6,
+		avgReadTime: '28:45',
+		exitRate: 22.3,
+		downloadCount: 989
+	},
+	{
+		id: 4,
+		name: '机器学习算法详解',
+		type: '在线文档',
+		viewCount: 2756,
+		completedCount: 1908,
+		completionRate: 69.2,
+		avgReadTime: '45:20',
+		exitRate: 30.8,
+		downloadCount: 823
+	},
+	{
+		id: 5,
+		name: '前端工程化实践',
+		type: 'PDF文档',
+		viewCount: 2523,
+		completedCount: 1665,
+		completionRate: 66.0,
+		avgReadTime: '38:15',
+		exitRate: 34.0,
+		downloadCount: 689
+	},
+	{
+		id: 6,
+		name: 'Vue.js进阶开发',
+		type: '在线文档',
+		viewCount: 2678,
+		completedCount: 1802,
+		completionRate: 67.3,
+		avgReadTime: '35:40',
+		exitRate: 32.7,
+		downloadCount: 756
+	},
+	{
+		id: 7,
+		name: 'Node.js后端开发',
+		type: 'PDF文档',
+		viewCount: 2345,
+		completedCount: 1567,
+		completionRate: 66.8,
+		avgReadTime: '42:10',
+		exitRate: 28.9,
+		downloadCount: 634
+	},
+	{
+		id: 8,
+		name: '数据库设计原理',
+		type: '在线文档',
+		viewCount: 2123,
+		completedCount: 1489,
+		completionRate: 70.1,
+		avgReadTime: '38:25',
+		exitRate: 26.4,
+		downloadCount: 567
+	},
+	{
+		id: 9,
+		name: 'Web安全防护',
+		type: 'PDF文档',
+		viewCount: 1987,
+		completedCount: 1345,
+		completionRate: 67.7,
+		avgReadTime: '33:50',
+		exitRate: 29.8,
+		downloadCount: 498
+	},
+	{
+		id: 10,
+		name: '移动端开发实战',
+		type: '在线文档',
+		viewCount: 1876,
+		completedCount: 1234,
+		completionRate: 65.8,
+		avgReadTime: '40:15',
+		exitRate: 31.2,
+		downloadCount: 445
+	}
+]
+
+// 生成课程选项数据
+const generateCourseOptions = () => {
+	return {
+		code: 200,
+		message: '获取成功',
+		data: courseOptions
+	}
+}
+
+// 生成核心统计数据
+const generateStatsData = (options) => {
+	const urlParts = options.url.split('?')
+	const params = urlParts.length > 1 ? new URLSearchParams(urlParts[1]) : new URLSearchParams()
+	const courseId = params.get('courseId') || ''
+	const timeRange = parseInt(params.get('timeRange')) || 30
+
+	// 全部课程的数据应该是所有课程的总和,单个课程数据相对较少
+	const baseMultiplier = courseId ? 0.15 + Math.random() * 0.25 : 1.0 // 单个课程占总数的15%-40%
+	const timeMultiplier = timeRange / 30
+
+	// 基础数据 - 全部课程的总数据
+	const baseStats = {
+		totalDocViewers: 8560,
+		completedDocViewers: 6234,
+		totalDocExits: 1890,
+		totalDiscussions: 1456,
+		totalReplies: 8934
+	}
+
+	const calculatedStats = {
+		totalDocViewers: Math.round(baseStats.totalDocViewers * baseMultiplier * timeMultiplier),
+		completedDocViewers: Math.round(baseStats.completedDocViewers * baseMultiplier * timeMultiplier),
+		totalDocExits: Math.round(baseStats.totalDocExits * baseMultiplier * timeMultiplier),
+		totalDiscussions: Math.round(baseStats.totalDiscussions * baseMultiplier * timeMultiplier),
+		totalReplies: Math.round(baseStats.totalReplies * baseMultiplier * timeMultiplier)
+	}
+
+	// 确保完成人数不超过总观看人数
+	if (calculatedStats.completedDocViewers > calculatedStats.totalDocViewers) {
+		calculatedStats.completedDocViewers = Math.round(calculatedStats.totalDocViewers * 0.7)
+	}
+
+	// 计算完成率
+	const docCompletionRate =
+		calculatedStats.totalDocViewers > 0
+			? parseFloat(((calculatedStats.completedDocViewers / calculatedStats.totalDocViewers) * 100).toFixed(1))
+			: 0
+
+	// 计算跳出率 - 单个课程跳出率可能更高
+	const baseExitRate = courseId ? 25 + Math.random() * 15 : 18 + Math.random() * 12
+	const docExitRate = parseFloat(baseExitRate.toFixed(1))
+
+	// 计算平均回帖数
+	const avgRepliesPerDiscussion =
+		calculatedStats.totalDiscussions > 0
+			? parseFloat((calculatedStats.totalReplies / calculatedStats.totalDiscussions).toFixed(1))
+			: 0
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			totalDocViewers: calculatedStats.totalDocViewers,
+			completedDocViewers: calculatedStats.completedDocViewers,
+			docCompletionRate,
+			totalDocExits: calculatedStats.totalDocExits,
+			docExitRate,
+			avgDocExitTime: `${String(Math.floor(Math.random() * 15) + 8).padStart(2, '0')}:${String(
+				Math.floor(Math.random() * 60)
+			).padStart(2, '0')}`,
+			totalDiscussions: calculatedStats.totalDiscussions,
+			totalReplies: calculatedStats.totalReplies,
+			avgRepliesPerDiscussion
+		}
+	}
+}
+
+// 生成每周统计数据
+const generateWeeklyStatsData = (options) => {
+	const urlParts = options.url.split('?')
+	const params = urlParts.length > 1 ? new URLSearchParams(urlParts[1]) : new URLSearchParams()
+	const courseId = params.get('courseId') || ''
+
+	// 全部课程数据应该是所有课程的总和
+	const baseMultiplier = courseId ? 0.12 + Math.random() * 0.28 : 1.0 // 单个课程占总数的12%-40%
+
+	// 基础数据 - 全部课程的总数据
+	const baseWeeklyStats = {
+		studentWeeklyPosts: 456,
+		studentWeeklyReplies: 1678,
+		teacherWeeklyPosts: 123,
+		teacherWeeklyReplies: 567
+	}
+
+	const calculatedStats = {
+		studentWeeklyPosts: Math.round(baseWeeklyStats.studentWeeklyPosts * baseMultiplier),
+		studentWeeklyReplies: Math.round(baseWeeklyStats.studentWeeklyReplies * baseMultiplier),
+		teacherWeeklyPosts: Math.round(baseWeeklyStats.teacherWeeklyPosts * baseMultiplier),
+		teacherWeeklyReplies: Math.round(baseWeeklyStats.teacherWeeklyReplies * baseMultiplier)
+	}
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			studentWeeklyPosts: calculatedStats.studentWeeklyPosts,
+			studentWeeklyReplies: calculatedStats.studentWeeklyReplies,
+			studentAvgPostsPerDay: parseFloat((calculatedStats.studentWeeklyPosts / 7).toFixed(1)),
+			teacherWeeklyPosts: calculatedStats.teacherWeeklyPosts,
+			teacherWeeklyReplies: calculatedStats.teacherWeeklyReplies,
+			teacherAvgPostsPerDay: parseFloat((calculatedStats.teacherWeeklyPosts / 7).toFixed(1))
+		}
+	}
+}
+
+// 生成每日访问数据
+const generateDailyVisitsData = (options) => {
+	const urlParts = options.url.split('?')
+	const params = urlParts.length > 1 ? new URLSearchParams(urlParts[1]) : new URLSearchParams()
+	const courseId = params.get('courseId') || ''
+	const timeRange = parseInt(params.get('timeRange')) || 30
+
+	const dates = generateDateData(timeRange)
+	const visits = generateVisitData(timeRange, courseId)
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			dates,
+			visits
+		}
+	}
+}
+
+// 生成讨论数据
+const generateDiscussionData = (options) => {
+	const urlParts = options.url.split('?')
+	const params = urlParts.length > 1 ? new URLSearchParams(urlParts[1]) : new URLSearchParams()
+	const courseId = params.get('courseId') || ''
+	const page = parseInt(params.get('page')) || 1
+	const pageSize = parseInt(params.get('pageSize')) || 10
+
+	let filteredData = discussionData
+	if (courseId) {
+		// 单个课程只显示部分讨论数据,模拟该课程相关的讨论
+		const courseDiscussionCount = Math.floor(discussionData.length * (0.2 + Math.random() * 0.3)) // 20%-50%
+		filteredData = discussionData.slice(0, Math.max(courseDiscussionCount, 2)) // 至少显示2条
+	}
+
+	const startIndex = (page - 1) * pageSize
+	const endIndex = startIndex + pageSize
+	const pageData = filteredData.slice(startIndex, endIndex)
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			list: pageData,
+			total: filteredData.length,
+			page,
+			pageSize
+		}
+	}
+}
+
+// 生成文档数据
+const generateDocumentData = (options) => {
+	const urlParts = options.url.split('?')
+	const params = urlParts.length > 1 ? new URLSearchParams(urlParts[1]) : new URLSearchParams()
+	const courseId = params.get('courseId') || ''
+	const page = parseInt(params.get('page')) || 1
+	const pageSize = parseInt(params.get('pageSize')) || 10
+
+	let filteredData = [...documentData]
+
+	if (courseId) {
+		// 单个课程只显示该课程相关的文档,数量相对较少
+		const courseDocumentCount = Math.floor(documentData.length * (0.25 + Math.random() * 0.35)) // 25%-60%
+		filteredData = documentData.slice(0, Math.max(courseDocumentCount, 3)) // 至少显示3条
+
+		// 调整单个课程文档的数据,使其相对较小但合理
+		filteredData = filteredData.map((doc) => ({
+			...doc,
+			viewCount: Math.round(doc.viewCount * (0.15 + Math.random() * 0.25)), // 15%-40%的观看量
+			completedCount: Math.round(doc.completedCount * (0.15 + Math.random() * 0.25)),
+			downloadCount: Math.round(doc.downloadCount * (0.15 + Math.random() * 0.25)),
+			// 完成率和跳出率保持相对合理的范围
+			completionRate: parseFloat((doc.completionRate + (Math.random() - 0.5) * 10).toFixed(1)),
+			exitRate: parseFloat((doc.exitRate + (Math.random() - 0.5) * 8).toFixed(1))
+		}))
+	}
+
+	const startIndex = (page - 1) * pageSize
+	const endIndex = startIndex + pageSize
+	const pageData = filteredData.slice(startIndex, endIndex)
+
+	return {
+		code: 200,
+		message: '获取成功',
+		data: {
+			list: pageData,
+			total: filteredData.length,
+			page,
+			pageSize
+		}
+	}
+}
+
+// Mock 接口定义
+Mock.mock(/\/api\/webapp\/teaching-analysis\/course-options/, 'get', generateCourseOptions)
+Mock.mock(/\/api\/webapp\/teaching-analysis\/stats/, 'get', generateStatsData)
+Mock.mock(/\/api\/webapp\/teaching-analysis\/weekly-stats/, 'get', generateWeeklyStatsData)
+Mock.mock(/\/api\/webapp\/teaching-analysis\/daily-visits/, 'get', generateDailyVisitsData)
+Mock.mock(/\/api\/webapp\/teaching-analysis\/discussions/, 'get', generateDiscussionData)
+Mock.mock(/\/api\/webapp\/teaching-analysis\/documents/, 'get', generateDocumentData)
+
+// 导出的API函数
+export const getCourseOptions = (params = {}) => {
+	return request('teaching-analysis/course-options', params, 'get')
+}
+
+export const getTeachingStats = (params = {}) => {
+	return request('teaching-analysis/stats', params, 'get')
+}
+
+export const getWeeklyStats = (params = {}) => {
+	return request('teaching-analysis/weekly-stats', params, 'get')
+}
+
+export const getDailyVisits = (params = {}) => {
+	return request('teaching-analysis/daily-visits', params, 'get')
+}
+
+export const getDiscussionData = (params = {}) => {
+	return request('teaching-analysis/discussions', params, 'get')
+}
+
+export const getDocumentStats = (params = {}) => {
+	return request('teaching-analysis/documents', params, 'get')
+}

+ 272 - 0
src/api/statisticalAnalysis/overviewLearningProgress.js

@@ -0,0 +1,272 @@
+// 文件模块相关接口
+import { moduleRequest } from '@/utils/request'
+import Mock from 'mockjs'
+
+const request = moduleRequest(`/api/webapp`)
+
+// Mock 数据配置
+Mock.setup({
+	timeout: '200-600'
+})
+
+// 课程名称列表
+const courseNames = [
+	'Python编程入门',
+	'数据结构与算法',
+	'机器学习基础',
+	'Web前端开发',
+	'Java后端开发',
+	'数据库设计',
+	'算法分析',
+	'人工智能导论',
+	'计算机网络',
+	'操作系统原理',
+	'软件工程',
+	'计算机图形学',
+	'编译原理',
+	'分布式系统',
+	'云计算技术'
+]
+
+// 生成完整的课程数据集(用于分页)
+const generateFullCourseData = () => {
+	return courseNames.map((courseName, index) => ({
+		id: index + 1,
+		courseName,
+		studentCount: Mock.Random.integer(80, 150),
+		visitCount: Mock.Random.integer(800, 2000),
+		videoDuration: Mock.Random.pick(['3h25m', '4h12m', '5h30m', '6h45m', '2h58m', '7h20m', '8h15m', '9h30m']),
+		documentVisit: Mock.Random.integer(300, 800),
+		submissionCount: Mock.Random.integer(70, 95),
+		numberStudentsDroppingCourses: Mock.Random.integer(5, 25)
+	}))
+}
+
+// 全局存储完整数据集
+let fullCourseData = generateFullCourseData()
+
+// 根据筛选条件调整数据
+const adjustDataByFilters = (data, courseName, contentType) => {
+	return data.map((item) => {
+		let adjustedItem = { ...item }
+
+		// 根据课程名称筛选调整数据
+		if (courseName && courseName !== '' && item.courseName !== courseName) {
+			// 如果不是选中的课程,数据为0或很小
+			adjustedItem.studentCount = 0
+			adjustedItem.visitCount = 0
+			adjustedItem.documentVisit = 0
+			adjustedItem.submissionCount = 0
+			adjustedItem.numberStudentsDroppingCourses = 0
+			adjustedItem.videoDuration = '0h0m'
+		} else if (courseName && courseName !== '' && item.courseName === courseName) {
+			// 如果是选中的课程,保持原数据或稍微增加
+			adjustedItem.studentCount = item.studentCount
+			adjustedItem.visitCount = item.visitCount
+			adjustedItem.documentVisit = item.documentVisit
+			adjustedItem.submissionCount = item.submissionCount
+		}
+
+		// 根据内容类型调整数据
+		if (contentType && contentType !== 'all') {
+			const reductionFactor = 0.6 // 具体类型的数据是全部的60%
+			switch (contentType) {
+				case 'video':
+					adjustedItem.visitCount = Math.floor(adjustedItem.visitCount * reductionFactor)
+					adjustedItem.documentVisit = Math.floor(adjustedItem.documentVisit * 0.3)
+					break
+				case 'document':
+					adjustedItem.documentVisit = Math.floor(adjustedItem.documentVisit * reductionFactor)
+					adjustedItem.visitCount = Math.floor(adjustedItem.visitCount * 0.4)
+					break
+				case 'exercise':
+					adjustedItem.submissionCount = Math.floor(adjustedItem.submissionCount * reductionFactor)
+					adjustedItem.visitCount = Math.floor(adjustedItem.visitCount * 0.3)
+					adjustedItem.documentVisit = Math.floor(adjustedItem.documentVisit * 0.2)
+					break
+			}
+		}
+
+		return adjustedItem
+	})
+}
+
+// 生成分页的课程列表数据
+const generateCourseList = (options) => {
+	// 解析查询参数
+	const url = new URL('http://localhost' + options.url)
+	const current = parseInt(url.searchParams.get('current')) || 1
+	const pageSize = parseInt(url.searchParams.get('pageSize')) || 6
+	const courseName = url.searchParams.get('courseName') || ''
+	const contentType = url.searchParams.get('contentType') || 'all'
+
+	// 根据筛选条件调整数据
+	let filteredData = adjustDataByFilters(fullCourseData, courseName, contentType)
+
+	// 如果选择了特定课程,只显示该课程
+	if (courseName && courseName !== '') {
+		filteredData = filteredData.filter((item) => item.courseName === courseName)
+	}
+
+	// 计算分页
+	const startIndex = (current - 1) * pageSize
+	const endIndex = startIndex + pageSize
+	const pageData = filteredData.slice(startIndex, endIndex)
+
+	return {
+		code: 200,
+		data: {
+			data: pageData,
+			total: filteredData.length,
+			current: current,
+			pageSize: pageSize
+		}
+	}
+}
+
+// 生成统计概览数据
+const generateOverviewStats = (options) => {
+	// 解析查询参数
+	const url = new URL('http://localhost' + options.url)
+	const courseName = url.searchParams.get('courseName') || ''
+	const contentType = url.searchParams.get('contentType') || 'all'
+
+	// 基础数据
+	let baseStats = {
+		totalStudents: 2846,
+		totalVisits: 12587,
+		totalSubmissions: 1924,
+		totalInteractions: 5362
+	}
+
+	// 根据课程名称调整数据
+	if (courseName && courseName !== '') {
+		// 选择特定课程时,数据减少到20-30%
+		baseStats.totalStudents = Math.floor(baseStats.totalStudents * Mock.Random.float(0.2, 0.3))
+		baseStats.totalVisits = Math.floor(baseStats.totalVisits * Mock.Random.float(0.2, 0.3))
+		baseStats.totalSubmissions = Math.floor(baseStats.totalSubmissions * Mock.Random.float(0.2, 0.3))
+		baseStats.totalInteractions = Math.floor(baseStats.totalInteractions * Mock.Random.float(0.2, 0.3))
+	}
+
+	// 根据内容类型调整数据
+	if (contentType && contentType !== 'all') {
+		const reductionFactor = 0.65 // 具体类型是全部的65%
+		switch (contentType) {
+			case 'video':
+				baseStats.totalVisits = Math.floor(baseStats.totalVisits * reductionFactor)
+				baseStats.totalSubmissions = Math.floor(baseStats.totalSubmissions * 0.3)
+				break
+			case 'document':
+				baseStats.totalVisits = Math.floor(baseStats.totalVisits * 0.4)
+				baseStats.totalSubmissions = Math.floor(baseStats.totalSubmissions * 0.2)
+				break
+			case 'exercise':
+				baseStats.totalSubmissions = Math.floor(baseStats.totalSubmissions * reductionFactor)
+				baseStats.totalVisits = Math.floor(baseStats.totalVisits * 0.3)
+				break
+		}
+	}
+
+	return {
+		code: 200,
+		data: baseStats
+	}
+}
+
+// 生成趋势图数据
+const generateTrendData = (options) => {
+	// 解析查询参数
+	const url = new URL('http://localhost' + options.url)
+	const courseName = url.searchParams.get('courseName') || ''
+	const contentType = url.searchParams.get('contentType') || 'all'
+
+	// 基础趋势数据
+	let baseVisitTrend = [2400, 2100, 2600, 2300, 2800, 2200, 2500]
+	let baseSubmissionTrend = [85, 78, 92, 88, 95, 82, 90]
+
+	// 根据课程名称调整数据
+	if (courseName && courseName !== '') {
+		baseVisitTrend = baseVisitTrend.map((val) => Math.floor(val * Mock.Random.float(0.25, 0.35)))
+		baseSubmissionTrend = baseSubmissionTrend.map((val) => Math.floor(val * Mock.Random.float(0.25, 0.35)))
+	}
+
+	// 根据内容类型调整数据
+	if (contentType && contentType !== 'all') {
+		const reductionFactor = 0.6
+		switch (contentType) {
+			case 'video':
+				baseVisitTrend = baseVisitTrend.map((val) => Math.floor(val * reductionFactor))
+				baseSubmissionTrend = baseSubmissionTrend.map((val) => Math.floor(val * 0.3))
+				break
+			case 'document':
+				baseVisitTrend = baseVisitTrend.map((val) => Math.floor(val * 0.4))
+				baseSubmissionTrend = baseSubmissionTrend.map((val) => Math.floor(val * 0.2))
+				break
+			case 'exercise':
+				baseSubmissionTrend = baseSubmissionTrend.map((val) => Math.floor(val * reductionFactor))
+				baseVisitTrend = baseVisitTrend.map((val) => Math.floor(val * 0.3))
+				break
+		}
+	}
+
+	return {
+		code: 200,
+		data: {
+			dates: ['4日', '5日', '6日', '7日', '8日', '9日', '10日'],
+			visitTrend: baseVisitTrend,
+			submissionTrend: baseSubmissionTrend
+		}
+	}
+}
+
+// 生成课程选项数据
+const generateCourseOptions = () => {
+	const options = courseNames.slice(0, 10).map((courseName, index) => ({
+		id: index + 1,
+		courseName
+	}))
+
+	return {
+		code: 200,
+		data: options
+	}
+}
+// Mock 接口定义 - 使用更宽松的正则表达式匹配
+Mock.mock(/\/api\/webapp\/?\/overview\/stats/, 'get', (options) => {
+	return generateOverviewStats(options)
+})
+
+Mock.mock(/\/api\/webapp\/?\/overview\/trend/, 'get', (options) => {
+	return generateTrendData(options)
+})
+
+Mock.mock(/\/api\/webapp\/?\/overview\/courses/, 'get', (options) => {
+	return generateCourseList(options)
+})
+
+Mock.mock(/\/api\/webapp\/?\/overview\/course-options/, 'get', () => {
+	return generateCourseOptions()
+})
+
+// 导出的API函数
+export const overviewLearningProgressApi = {
+	// 获取统计概览数据
+	getOverviewStats(params = {}) {
+		return request('/overview/stats', params, 'get')
+	},
+
+	// 获取趋势数据
+	getTrendData(params = {}) {
+		return request('/overview/trend', params, 'get')
+	},
+
+	// 获取课程列表数据
+	getCourseList(params = {}) {
+		return request('/overview/courses', params, 'get')
+	},
+
+	// 获取课程选项
+	getCourseOptions() {
+		return request('/overview/course-options', '', 'get')
+	}
+}

+ 206 - 0
src/api/statisticalAnalysis/platformStatusOverview.js

@@ -0,0 +1,206 @@
+// 平台状态总览相关接口
+import { moduleRequest } from '@/utils/request'
+
+const request = moduleRequest(`/api/webapp/`)
+
+// 模拟数据
+const mockData = {
+	// 平台统计数据
+	platformStats: {
+		code: 200,
+		message: '获取成功',
+		data: {
+			totalCourses: 128,
+			openedCourses: 97,
+			totalTeachers: 34,
+			totalStudents: 1256
+		}
+	},
+
+	// 热门课程数据
+	hotCourses: {
+		code: 200,
+		message: '获取成功',
+		data: [
+			{
+				id: 1,
+				rank: 1,
+				name: 'Python数据分析',
+				teacher: '王老师',
+				visits: '2,345',
+				references: 56
+			},
+			{
+				id: 2,
+				rank: 2,
+				name: 'JavaScript前端开发',
+				teacher: '李老师',
+				visits: '2,123',
+				references: 48
+			},
+			{
+				id: 3,
+				rank: 3,
+				name: '机器学习基础',
+				teacher: '张老师',
+				visits: '1,987',
+				references: 41
+			},
+			{
+				id: 4,
+				rank: 4,
+				name: '管理学原理',
+				teacher: '赵老师',
+				visits: '1,876',
+				references: 39
+			},
+			{
+				id: 5,
+				rank: 5,
+				name: '大学英语',
+				teacher: '钱老师',
+				visits: '1,765',
+				references: 35
+			}
+		]
+	},
+
+	// 开课信息数据
+	courseInfo: {
+		code: 200,
+		message: '获取成功',
+		data: {
+			records: [
+				{
+					id: 1,
+					name: 'Python数据分析',
+					teacher: '王老师',
+					startDate: '2024-03-01',
+					studentCount: 320,
+					visits: '2,345'
+				},
+				{
+					id: 2,
+					name: 'JavaScript前端开发',
+					teacher: '李老师',
+					startDate: '2024-03-10',
+					studentCount: 280,
+					visits: '2,123'
+				},
+				{
+					id: 3,
+					name: '机器学习基础',
+					teacher: '张老师',
+					startDate: '2024-03-15',
+					studentCount: 210,
+					visits: '1,987'
+				},
+				{
+					id: 4,
+					name: '管理学原理',
+					teacher: '赵老师',
+					startDate: '2024-03-20',
+					studentCount: 180,
+					visits: '1,876'
+				},
+				{
+					id: 5,
+					name: '大学英语',
+					teacher: '钱老师',
+					startDate: '2024-03-25',
+					studentCount: 266,
+					visits: '1,765'
+				}
+			],
+			total: 5,
+			current: 1,
+			size: 10
+		}
+	},
+
+	// 课程访问统计数据
+	courseVisitStats: {
+		code: 200,
+		message: '获取成功',
+		data: {
+			courseName: '',
+			visitData: [120, 156, 178, 210, 234, 245, 267],
+			dateRange: []
+		}
+	}
+}
+
+// 获取平台统计数据
+export const getPlatformStats = () => {
+	// 模拟异步请求
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			resolve(mockData.platformStats)
+		}, 300)
+	})
+}
+
+// 获取热门课程列表
+export const getHotCourses = () => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			resolve(mockData.hotCourses)
+		}, 300)
+	})
+}
+
+// 获取开课信息列表
+export const getCourseInfo = (params = {}) => {
+	const { current = 1, size = 10 } = params
+
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const response = {
+				...mockData.courseInfo,
+				data: {
+					...mockData.courseInfo.data,
+					current,
+					size
+				}
+			}
+			resolve(response)
+		}, 300)
+	})
+}
+
+// 获取课程访问统计
+export const getCourseVisitStats = (params) => {
+	const { courseName } = params
+
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			// 生成近7天的日期
+			const days = []
+			const today = new Date()
+			for (let i = 6; i >= 0; i--) {
+				const date = new Date(today)
+				date.setDate(date.getDate() - i)
+				const month = (date.getMonth() + 1).toString().padStart(2, '0')
+				const day = date.getDate().toString().padStart(2, '0')
+				days.push(`${month}-${day}`)
+			}
+
+			// 生成模拟访问数据
+			const baseVisits = [120, 156, 178, 210, 234, 245, 267]
+			const visitData = baseVisits.map((base) => {
+				const randomFactor = 0.8 + Math.random() * 0.4
+				return Math.round(base * randomFactor)
+			})
+
+			const response = {
+				...mockData.courseVisitStats,
+				data: {
+					courseName,
+					visitData,
+					dateRange: days
+				}
+			}
+			resolve(response)
+		}, 300)
+	})
+}

+ 305 - 0
src/api/statisticalAnalysis/statisticalAnalysisResourceLibrary.js

@@ -0,0 +1,305 @@
+import { moduleRequest } from '@/utils/request'
+import Mock from 'mockjs'
+
+const request = moduleRequest(`/api/webapp/`)
+
+// 使用 Mock.js 生成动态数据
+const generateMockData = (filters = {}) => {
+	const Random = Mock.Random
+
+	// 基础数据模板
+	const types = ['航空教学', '部队管理', '政治工作', '地面维修', '其他']
+	const departments = ['航空学院', '军事管理系', '政治工作部', '地面维修中心', '其他部门']
+	const formats = ['mp4', 'pdf', 'docx', 'pptx', 'xlsx', 'jpg', 'avi', 'wmv', 'mkv', 'other']
+	const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月']
+
+	// 根据筛选条件调整数据规模
+	const getScaleFactor = () => {
+		if (filters.department === 'all') return 1.0
+		return 0.15 + Math.random() * 0.25 // 单个院系占总数的15%-40%
+	}
+
+	const scaleFactor = getScaleFactor()
+
+	// 固定总容量为10TB,已使用空间根据筛选条件调整
+	const TOTAL_CAPACITY = 10.0 // 固定总容量10TB
+
+	// 生成合理的基础总数
+	const baseTotalResources = Mock.mock('@integer(1500, 2500)')
+	const baseUsedStorage = Mock.mock('@float(4.0, 8.0, 1, 1)') // 已使用4-8TB
+	const baseViews = Mock.mock('@integer(80000, 200000)')
+	const baseFavorites = Mock.mock('@integer(15000, 40000)')
+
+	// 根据筛选条件调整摘要数据
+	const adjustedTotalResources = Math.floor(baseTotalResources * scaleFactor)
+	const adjustedUsedStorage = (baseUsedStorage * scaleFactor).toFixed(1)
+	const adjustedViews = Math.floor(baseViews * scaleFactor)
+	const adjustedFavorites = Math.floor(baseFavorites * scaleFactor)
+
+	// 生成资源类型分布 - 确保航空教学占主导地位
+	const generateTypeCounts = () => {
+		const total = adjustedTotalResources
+		const counts = []
+
+		// 航空教学占40-50%
+		counts[0] = Math.floor(total * (0.4 + Math.random() * 0.1))
+
+		// 其他类型按比例分配剩余资源
+		const remaining = total - counts[0]
+		const ratios = [0.25, 0.2, 0.15, 0.1] // 部队管理、政治工作、地面维修、其他
+
+		for (let i = 1; i < types.length - 1; i++) {
+			counts[i] = Math.floor(remaining * ratios[i - 1] * (0.8 + Math.random() * 0.4))
+		}
+
+		// 最后一项为剩余数量
+		counts[types.length - 1] = total - counts.slice(0, -1).reduce((sum, count) => sum + count, 0)
+
+		return counts
+	}
+
+	// 生成院系分布 - 确保航空学院占主导地位
+	const generateDepartmentCounts = () => {
+		const total = adjustedTotalResources
+		const counts = []
+
+		// 航空学院占45-55%
+		counts[0] = Math.floor(total * (0.45 + Math.random() * 0.1))
+
+		// 其他院系按比例分配
+		const remaining = total - counts[0]
+		const ratios = [0.2, 0.18, 0.15, 0.12] // 军事管理系、政治工作部、地面维修中心、其他部门
+
+		for (let i = 1; i < departments.length - 1; i++) {
+			counts[i] = Math.floor(remaining * ratios[i - 1] * (0.8 + Math.random() * 0.4))
+		}
+
+		counts[departments.length - 1] = total - counts.slice(0, -1).reduce((sum, count) => sum + count, 0)
+
+		return counts
+	}
+
+	// 生成文件格式分布 - 视频和文档类型占主导
+	const generateFormatCounts = () => {
+		const total = adjustedTotalResources
+		const counts = new Array(formats.length).fill(0)
+
+		// 主要格式占比
+		const mainFormats = {
+			mp4: 0.25, // 视频最多
+			pdf: 0.2, // PDF文档
+			docx: 0.15, // Word文档
+			pptx: 0.12, // PPT
+			jpg: 0.08, // 图片
+			avi: 0.06, // 其他视频格式
+			xlsx: 0.05, // Excel
+			wmv: 0.04, // 视频格式
+			mkv: 0.03, // 视频格式
+			other: 0.02 // 其他格式
+		}
+
+		formats.forEach((format, index) => {
+			const ratio = mainFormats[format] || 0.01
+			counts[index] = Math.floor(total * ratio * (0.8 + Math.random() * 0.4))
+		})
+
+		// 确保总数匹配
+		const currentTotal = counts.reduce((sum, count) => sum + count, 0)
+		const diff = total - currentTotal
+		counts[0] += diff // 将差值加到mp4上
+
+		return counts
+	}
+
+	const typeCounts = generateTypeCounts()
+	const departmentCounts = generateDepartmentCounts()
+	const formatCounts = generateFormatCounts()
+
+	return {
+		// 摘要数据
+		summaryData: {
+			code: 200,
+			data: {
+				totalResources: adjustedTotalResources.toLocaleString(),
+				totalStorage: `${adjustedUsedStorage} TB`,
+				totalCapacity: `${TOTAL_CAPACITY} TB`,
+				usageRate: `${((parseFloat(adjustedUsedStorage) / TOTAL_CAPACITY) * 100).toFixed(0)}%`,
+				totalViews: adjustedViews.toLocaleString(),
+				totalFavorites: adjustedFavorites.toLocaleString()
+			}
+		},
+
+		// 资源类型分布数据
+		resourceTypeData: {
+			code: 200,
+			data: {
+				types: types,
+				typeCounts: typeCounts,
+				typeStorage: typeCounts.map((count) =>
+					((count / adjustedTotalResources) * parseFloat(adjustedUsedStorage)).toFixed(1)
+				)
+			}
+		},
+
+		// 院系分布数据
+		departmentData: {
+			code: 200,
+			data: {
+				departments: departments,
+				departmentCounts: departmentCounts,
+				departmentStorage: departmentCounts.map((count) =>
+					((count / adjustedTotalResources) * parseFloat(adjustedUsedStorage)).toFixed(1)
+				)
+			}
+		},
+
+		// 可见性和热度数据
+		visibilityData: {
+			code: 200,
+			data: {
+				visibility: ['公开', '非公开'],
+				visibilityCounts: [
+					Math.floor(adjustedTotalResources * 0.75), // 75%公开
+					Math.floor(adjustedTotalResources * 0.25) // 25%非公开
+				],
+				hotness: ['热门', '非热门'],
+				hotnessCounts: [
+					Math.floor(adjustedTotalResources * 0.2), // 20%热门
+					Math.floor(adjustedTotalResources * 0.8) // 80%非热门
+				],
+				recommended: ['已推荐', '未推荐'],
+				recommendedCounts: [
+					Math.floor(adjustedTotalResources * 0.15), // 15%已推荐
+					Math.floor(adjustedTotalResources * 0.85) // 85%未推荐
+				]
+			}
+		},
+
+		// 文件格式数据
+		formatData: {
+			code: 200,
+			data: {
+				formats: formats,
+				formatCounts: formatCounts,
+				formatStorage: formatCounts.map((count) =>
+					((count / adjustedTotalResources) * parseFloat(adjustedUsedStorage)).toFixed(2)
+				),
+				formatAvgSize: formats.map((format, index) => {
+					// 根据文件类型设置合理的平均大小
+					const sizeMap = {
+						mp4: Mock.mock('@integer(200, 800)'),
+						avi: Mock.mock('@integer(300, 1000)'),
+						wmv: Mock.mock('@integer(150, 600)'),
+						mkv: Mock.mock('@integer(400, 1200)'),
+						pdf: Mock.mock('@integer(5, 50)'),
+						docx: Mock.mock('@integer(2, 20)'),
+						pptx: Mock.mock('@integer(10, 100)'),
+						xlsx: Mock.mock('@integer(1, 15)'),
+						jpg: Mock.mock('@integer(1, 10)'),
+						other: Mock.mock('@integer(5, 200)')
+					}
+
+					const size = sizeMap[format] || Mock.mock('@integer(1, 100)')
+					if (size > 1000) return `${(size / 1000).toFixed(1)}GB`
+					return `${size}MB`
+				})
+			}
+		},
+
+		// 用户参与度数据
+		engagementData: {
+			code: 200,
+			data: {
+				engagement: ['观看', '收藏', '分享'],
+				engagementCounts: [
+					adjustedViews,
+					adjustedFavorites,
+					Math.floor(adjustedFavorites * 0.3) // 分享数约为收藏数的30%
+				],
+				typeViewCounts: typeCounts.map((count) => Math.floor(count * (2 + Math.random() * 3))) // 每个资源平均2-5次观看
+			}
+		},
+
+		// 时间趋势数据
+		trendData: {
+			code: 200,
+			data: {
+				trendLabels: months,
+				uploadTrend: months.map(() => Math.floor((adjustedTotalResources / 12) * (0.6 + Math.random() * 0.8))), // 月均上传量有波动
+				viewTrend: months.map(() => Math.floor((adjustedViews / 12) * (0.7 + Math.random() * 0.6))) // 月均观看量有波动
+			}
+		}
+	}
+}
+
+// API函数 - 每次调用都生成新的动态数据,传入筛选参数
+export const getSummaryData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.summaryData)
+		}, Mock.mock('@integer(200, 500)')) // 随机延迟
+	})
+	// 真实接口调用:
+	// return request('/resource-library/summary', params, 'get')
+}
+
+export const getResourceTypeData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.resourceTypeData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/type-distribution', params, 'get')
+}
+
+export const getDepartmentData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.departmentData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/department-distribution', params, 'get')
+}
+
+export const getVisibilityData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.visibilityData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/visibility-analysis', params, 'get')
+}
+
+export const getFormatData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.formatData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/format-distribution', params, 'get')
+}
+
+export const getEngagementData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.engagementData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/engagement-analysis', params, 'get')
+}
+
+export const getTrendData = (params = {}) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			const mockData = generateMockData(params)
+			resolve(mockData.trendData)
+		}, Mock.mock('@integer(200, 500)'))
+	})
+	// return request('/resource-library/trend-analysis', params, 'get')
+}

+ 7 - 0
src/api/statisticalAnalysis/statisticsHistoryCourseOfferings.js

@@ -0,0 +1,7 @@
+// 文件模块相关接口
+import { moduleRequest } from '@/utils/request'
+
+const request = moduleRequest(`/api/webapp/`)
+
+// 历史开课统计
+export const historyProgressPage = (params) => request('/disk/courseopen/historyProgressPage', params, 'get')

+ 292 - 0
src/api/statisticalAnalysis/videoAnalysis.js

@@ -0,0 +1,292 @@
+// 文件模块相关接口
+import { moduleRequest } from '@/utils/request'
+import Mock from 'mockjs'
+
+const request = moduleRequest(`/api/webapp`)
+
+// Mock 数据配置
+Mock.setup({
+	timeout: '200-600'
+})
+
+// 生成课程选项数据
+const generateCourseOptions = () => {
+	return Mock.mock({
+		code: 200,
+		'data|8-12': [
+			{
+				'id|+1': 1,
+				courseName:
+					'@pick(["JavaScript基础教程", "Python数据分析", "React前端开发", "机器学习入门", "Vue3实战开发", "Node.js后端开发", "数据结构与算法", "计算机网络基础"])',
+				courseId: 'course@integer(1, 100)'
+			}
+		]
+	})
+}
+
+// 生成视频分析统计数据
+const generateVideoStats = (options) => {
+	console.log('options-', options)
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const courseId = params.get('courseId') || ''
+	const timeRange = params.get('timeRange') || '30'
+
+	// 根据时间范围调整数据规模
+	const timeMultiplier =
+		{
+			7: 0.3,
+			30: 1,
+			90: 2.5,
+			365: 8
+		}[timeRange] || 1
+	console.log('courseId', courseId)
+	// 根据课程选择调整数据规模 - 全部课程数据更多
+	const courseMultiplier = courseId === '' ? 3.5 : 1 // 全部课程是单个课程的3.5倍
+	console.log('courseMultiplier', courseMultiplier)
+	// 基础数据
+	const baseViewers = Math.floor(800 * timeMultiplier * courseMultiplier)
+	const baseCompleted = Math.floor(baseViewers * (0.65 + Math.random() * 0.2)) // 65%-85%完成率
+	const completionRate = Math.round((baseCompleted / baseViewers) * 100)
+
+	const baseDownloads = Math.floor(baseViewers * (0.25 + Math.random() * 0.15)) // 25%-40%下载率
+	const downloadRate = Math.round((baseDownloads / baseViewers) * 100)
+	const avgDownloads = Math.round((baseDownloads / baseViewers) * 100) / 100
+
+	const baseExits = Math.floor(baseViewers * (0.15 + Math.random() * 0.1)) // 15%-25%跳出率
+	const exitRate = Math.round((baseExits / baseViewers) * 100)
+
+	const baseNotes = Math.floor(baseViewers * (0.6 + Math.random() * 0.4)) // 60%-100%笔记率
+	const baseDiscussions = Math.floor(baseViewers * (0.3 + Math.random() * 0.2)) // 30%-50%讨论率
+	const baseReplies = Math.floor(baseDiscussions * (2 + Math.random() * 2)) // 每个讨论2-4个回复
+
+	return Mock.mock({
+		code: 200,
+		data: {
+			totalViewers: baseViewers + Math.floor(Math.random() * 200),
+			completedViewers: baseCompleted,
+			completionRate: completionRate,
+			totalDownloads: baseDownloads,
+			downloadRate: downloadRate,
+			avgDownloads: avgDownloads,
+			totalExits: baseExits,
+			exitRate: exitRate,
+			avgExitTime: '@pick(["08:45", "12:34", "15:23", "18:56", "22:15", "06:30", "14:20", "19:45"])',
+			totalNotes: baseNotes,
+			totalDiscussions: baseDiscussions,
+			totalReplies: baseReplies
+		}
+	})
+}
+
+// 生成学员详细数据
+const generateStudentData = (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const current = parseInt(params.get('current')) || 1
+	const pageSize = parseInt(params.get('pageSize')) || 10
+	const courseId = params.get('courseId') || ''
+	const timeRange = params.get('timeRange') || '30'
+
+	// 根据课程和时间范围调整总数
+	const timeMultiplier = { 7: 0.3, 30: 1, 90: 2.5, 365: 8 }[timeRange] || 1
+	const courseMultiplier = courseId === '' ? 3.5 : 1
+	const baseTotal = Math.floor(120 * timeMultiplier * courseMultiplier)
+
+	return Mock.mock({
+		code: 200,
+		[`data|${pageSize}`]: [
+			{
+				'key|+1': (current - 1) * pageSize + 1,
+				id: () => String((current - 1) * pageSize + Mock.Random.increment()).padStart(3, '0'),
+				name: '@cname',
+				totalTime: () => {
+					const minutes = Math.floor(Math.random() * 180) + 30 // 30-210分钟
+					const hours = Math.floor(minutes / 60)
+					const mins = minutes % 60
+					return hours > 0 ? `${hours}小时${mins}分钟` : `${mins}分钟`
+				},
+				'progress|20-100': () => Math.floor(Math.random() * 80) + 20,
+				'viewCount|1-12': () => Math.floor(Math.random() * 12) + 1,
+				'exitPoints|1-5': () => {
+					const count = Math.floor(Math.random() * 5) + 1
+					const points = []
+					for (let i = 0; i < count; i++) {
+						const minutes = Math.floor(Math.random() * 45)
+						const seconds = Math.floor(Math.random() * 60)
+						points.push(`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`)
+					}
+					return points
+				},
+				'seekCount|2-25': () => Math.floor(Math.random() * 24) + 2,
+				'noteCount|0-15': () => Math.floor(Math.random() * 16),
+				'discussionCount|0-12': () => Math.floor(Math.random() * 13),
+				'replyCount|0-20': () => Math.floor(Math.random() * 21),
+				lastAccess: '@datetime("yyyy-MM-dd HH:mm")'
+			}
+		],
+		total: baseTotal + Math.floor(Math.random() * 50),
+		current,
+		pageSize
+	})
+}
+
+// 生成章节数据
+const generateChapterData = (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const courseId = params.get('courseId') || ''
+	const timeRange = params.get('timeRange') || '30'
+
+	// 根据课程和时间范围调整数据规模
+	const timeMultiplier = { 7: 0.3, 30: 1, 90: 2.5, 365: 8 }[timeRange] || 1
+	const courseMultiplier = courseId === '' ? 3.5 : 1
+
+	const chapterCount = Math.floor(Math.random() * 4) + 5 // 5-8章
+	const chapters = []
+
+	for (let i = 1; i <= chapterCount; i++) {
+		const baseViewers = Math.floor((800 - i * 50) * timeMultiplier * courseMultiplier) // 越后面章节观看人数越少
+		const completionRate = Math.max(95 - i * 5 - Math.random() * 10, 60) // 越后面完成率越低
+		const completed = Math.floor(baseViewers * (completionRate / 100))
+
+		// 生成视频时长
+		const totalMinutes = Math.floor(Math.random() * 30) + 15 // 15-45分钟
+		const minutes = totalMinutes % 60
+		const seconds = Math.floor(Math.random() * 60)
+		const duration = `${minutes}:${seconds.toString().padStart(2, '0')}`
+
+		// 平均观看时长应该小于等于视频时长
+		const avgWatchMinutes = Math.floor(totalMinutes * (0.7 + Math.random() * 0.3))
+		const avgWatchSeconds = Math.floor(Math.random() * 60)
+		const avgWatchTime = `${avgWatchMinutes}:${avgWatchSeconds.toString().padStart(2, '0')}`
+
+		chapters.push({
+			key: i,
+			chapter: `第${i}章:${
+				['课程介绍', '基础知识', '核心概念', '实战应用', '高级技巧', '项目实战', '总结回顾', '拓展学习'][i - 1] ||
+				'课程内容'
+			}`,
+			duration: duration,
+			viewers: baseViewers + Math.floor(Math.random() * 100),
+			completed: completed,
+			completionRate: Math.round(completionRate),
+			avgWatchTime: avgWatchTime,
+			exitRate: Math.min(Math.floor(5 + i * 3 + Math.random() * 10), 35), // 越后面跳出率越高
+			downloads: Math.floor(baseViewers * (0.1 + Math.random() * 0.15)), // 10%-25%下载率
+			notes: Math.floor(baseViewers * (0.4 + Math.random() * 0.3)), // 40%-70%笔记率
+			discussions: Math.floor(baseViewers * (0.15 + Math.random() * 0.15)) // 15%-30%讨论率
+		})
+	}
+
+	return {
+		code: 200,
+		data: chapters
+	}
+}
+
+// 生成图表数据
+const generateChartData = (options) => {
+	const params = new URLSearchParams(options.url.split('?')[1])
+	const courseId = params.get('courseId') || ''
+	const timeRange = params.get('timeRange') || '30'
+
+	// 根据课程和时间范围调整数据规模
+	const timeMultiplier = { 7: 0.3, 30: 1, 90: 2.5, 365: 8 }[timeRange] || 1
+	const courseMultiplier = courseId === '' ? 3.5 : 1
+
+	const chapterCount = Math.floor(Math.random() * 4) + 5 // 5-8章
+	const chapters = []
+	const watchTimes = []
+	const completedUsers = []
+	const completionRates = []
+	const notes = []
+	const discussions = []
+	const replies = []
+
+	// 生成章节相关数据
+	for (let i = 1; i <= chapterCount; i++) {
+		chapters.push(`第${i}章`)
+
+		// 观看时长:越后面的章节观看时长可能越短
+		const baseWatchTime = 35 - i * 2 + Math.random() * 10
+		watchTimes.push(Math.round(baseWatchTime * 10) / 10)
+
+		// 完成用户数:递减趋势
+		const baseUsers = Math.floor((1000 - i * 80) * timeMultiplier * courseMultiplier)
+		completedUsers.push(baseUsers + Math.floor(Math.random() * 100))
+
+		// 完成率:递减趋势
+		const rate = Math.max(95 - i * 5 - Math.random() * 8, 60)
+		completionRates.push(Math.round(rate * 10) / 10)
+
+		// 互动数据
+		const baseInteraction = Math.floor(baseUsers * 0.6)
+		notes.push(Math.floor(baseInteraction * (0.6 + Math.random() * 0.4)))
+		discussions.push(Math.floor(baseInteraction * (0.2 + Math.random() * 0.2)))
+		replies.push(Math.floor(discussions[i - 1] * (2 + Math.random() * 2)))
+	}
+
+	// 学习进度分布 - 根据总体数据规模调整
+	const totalUsers = Math.floor(800 * timeMultiplier * courseMultiplier)
+	const progressDistribution = [
+		{ value: Math.floor(totalUsers * 0.15), name: '0-25%' },
+		{ value: Math.floor(totalUsers * 0.2), name: '26-50%' },
+		{ value: Math.floor(totalUsers * 0.25), name: '51-75%' },
+		{ value: Math.floor(totalUsers * 0.4), name: '76-100%' }
+	]
+
+	return {
+		code: 200,
+		data: {
+			progressDistribution,
+			timeTrend: {
+				chapters,
+				watchTime: watchTimes,
+				completedUsers
+			},
+			chapterCompletion: {
+				chapters,
+				completionRates
+			},
+			interactionData: {
+				chapters,
+				notes,
+				discussions,
+				replies
+			}
+		}
+	}
+}
+// Mock 接口定义
+Mock.mock(/\/api\/webapp\/video-analysis\/course-options/, 'get', generateCourseOptions)
+Mock.mock(/\/api\/webapp\/video-analysis\/stats/, 'get', generateVideoStats)
+Mock.mock(/\/api\/webapp\/video-analysis\/students/, 'get', generateStudentData)
+Mock.mock(/\/api\/webapp\/video-analysis\/chapters/, 'get', generateChapterData)
+Mock.mock(/\/api\/webapp\/video-analysis\/charts/, 'get', generateChartData)
+
+// 导出的API函数
+export const videoAnalysisApi = {
+	// 获取课程选项
+	getCourseOptions() {
+		return request('/video-analysis/course-options', '', 'get')
+	},
+
+	// 获取视频分析统计数据
+	getVideoStats(params = {}) {
+		console.log('params=', params)
+		return request('/video-analysis/stats', params, 'get')
+	},
+
+	// 获取学员详细数据
+	getStudentData(params = {}) {
+		return request('/video-analysis/students', params, 'get')
+	},
+
+	// 获取章节数据
+	getChapterData(params = {}) {
+		return request('/video-analysis/chapters', params, 'get')
+	},
+
+	// 获取图表数据
+	getChartData(params = {}) {
+		return request('/video-analysis/charts', params, 'get')
+	}
+}

+ 36 - 7
src/views/exm/examinationManagement/form.vue

@@ -30,7 +30,7 @@
 					<a-button type="primary" @click="addPaper" style="width: 100px">选择试卷</a-button>
 				</a-input-group>
 			</a-form-item>
-			<a-form-item label="学期" name="semesterId" :rules="rules.semesterId">
+			<a-form-item v-if="form.gradesIdsList.length === 0" label="学期" name="semesterId" :rules="rules.semesterId">
 				<a-select v-model:value="form.semesterId" placeholder="请选择学期" allowClear @change="handleSemesterChange">
 					<a-select-option v-for="item in semesterList" :key="item.id" :value="item.id">
 						{{ item.name }}
@@ -44,13 +44,25 @@
 					</a-select-option>
 				</a-select>
 			</a-form-item> -->
-			<a-form-item label="课程" name="courseId" :rules="rules.courseId">
+			<a-form-item label="课程" v-if="form.gradesIdsList.length === 0" name="courseId" :rules="rules.courseId">
 				<a-select v-model:value="form.courseId" placeholder="请选择课程" allowClear :disabled="!form.semesterId">
 					<a-select-option v-for="item in courseList" :key="item.courseId" :value="item.courseId">
 						{{ item.courseName }}
 					</a-select-option>
 				</a-select>
 			</a-form-item>
+			<a-form-item v-if="!form.semesterId" label="班级" name="gradesIdsList" :rules="rules.gradesIdsList">
+				<a-select
+					v-model:value="form.gradesIdsList"
+					mode="multiple"
+					:fieldNames="{ label: 'gradesName', value: 'gradesId' }"
+					:options="gradesQueryListOptions"
+					placeholder="请选择班级"
+					:show-search="true"
+					:filter-option="(input, option) => option.gradesName.toLowerCase().indexOf(input.toLowerCase()) >= 0"
+					allowClear
+				/>
+			</a-form-item>
 			<a-form-item label="开始时间" name="startTime" :rules="rules.startTime">
 				<a-date-picker
 					v-model:value="form.startTime"
@@ -142,6 +154,7 @@
 	import examManagerApi from '@/api/exam/paper/examManager.js'
 	import examPaperApi from '@/api/exam/paper/examPaperApi.js'
 	import resourceAuditApi from '@/api/resourceAudit.js'
+	import { gradesQueryList } from '@/api/semester/index.js'
 	import tool from '@/utils/tool'
 	import dayjs from 'dayjs'
 	const emit = defineEmits(['success'])
@@ -159,6 +172,7 @@
 	const semesterList = ref([])
 	const majorList = ref([])
 	const courseList = ref([])
+	const gradesQueryListOptions = ref([]) //班级
 	const form = reactive({
 		id: null,
 		examName: '',
@@ -167,7 +181,8 @@
 		startTime: null,
 		endTime: null,
 		examStatus: 0,
-		semesterId: null
+		semesterId: null,
+		gradesIdsList: []
 		// majorId: null
 	})
 
@@ -177,7 +192,8 @@
 		startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
 		endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
 		semesterId: [{ required: true, message: '请选择学期', trigger: 'change' }],
-		courseId: [{ required: true, message: '请选择课程', trigger: 'change' }]
+		courseId: [{ required: true, message: '请选择课程', trigger: 'change' }],
+		gradesIdsList: [{ required: true, message: '请选择班级', trigger: 'change' }]
 	}
 
 	const selectedPaperName = ref('')
@@ -266,8 +282,8 @@
 	}
 	// 禁用开始时间大于今天的日期
 	const disabledStartDate = (current) => {
-		// 禁用于今天的日期
-		return current && current < dayjs().endOf('day')
+		// 禁用于今天的日期
+		return current && current < dayjs().startOf('day')
 	}
 
 	// 试卷类型变更
@@ -316,6 +332,16 @@
 		paperPage.queryParam.pageIndex = re.current
 		paperPage.listLoading = false
 	}
+	// 班级下拉
+	const loadGradesQueryList = () => {
+		gradesQueryList()
+			.then((res) => {
+				gradesQueryListOptions.value = res.data
+			})
+			.catch((err) => {
+				console.log(err)
+			})
+	}
 
 	// 确认选择试卷
 	const confirmPaperSelect = () => {
@@ -401,7 +427,9 @@
 					Object.assign(form, {
 						...re,
 						startTime: re.startTime ? dayjs(re.startTime) : null,
-						endTime: re.endTime ? dayjs(re.endTime) : null
+						endTime: re.endTime ? dayjs(re.endTime) : null,
+						// 解析 gradesIds 字符串为数组
+						gradesIdsList: re.gradesids ? re.gradesids.split(',').map((id) => parseInt(id.trim())) : []
 					})
 					// 如果有试卷ID,需要获取试卷名称显示
 					if (re.paperId) {
@@ -442,6 +470,7 @@
 			.finally(() => {
 				semesterLoading()
 			})
+		loadGradesQueryList()
 		// 加载专业
 		// const majorLoading = message.loading('正在加载专业列表...', 0)
 		// resourceAuditApi

+ 2 - 2
src/views/exm/examinationManagement/index.vue

@@ -55,11 +55,11 @@
 					{{ formatDateTime(record.createTime) }}
 				</template>
 			</a-table-column>
-			<a-table-column title="操作" key="action" align="center" :width="220">
+			<a-table-column title="操作" key="action" align="center" :width="180">
 				<template #default="{ record }">
 					<a-button size="small" @click="editTask(record)">编辑</a-button>
 					<a-button size="small" danger style="margin-left: 8px" @click="deleteTask(record)">删除</a-button>
-					<a-button size="small" style="margin-left: 8px" @click="statistic(record)">统计分析</a-button>
+					<!-- <a-button size="small" style="margin-left: 8px" @click="statistic(record)">统计分析</a-button> -->
 				</template>
 			</a-table-column>
 		</a-table>

+ 36 - 7
src/views/exm/questionnaireManagement/form.vue

@@ -23,7 +23,7 @@
 					<a-button type="primary" @click="addPaper" style="width: 100px">选择问卷</a-button>
 				</a-input-group>
 			</a-form-item>
-			<a-form-item label="学期" name="semesterId" :rules="rules.semesterId">
+			<a-form-item v-if="form.gradesIdsList.length === 0" label="学期" name="semesterId" :rules="rules.semesterId">
 				<a-select v-model:value="form.semesterId" placeholder="请选择学期" allowClear @change="handleSemesterChange">
 					<a-select-option v-for="item in semesterList" :key="item.id" :value="item.id">
 						{{ item.name }}
@@ -37,13 +37,25 @@
 					</a-select-option>
 				</a-select>
 			</a-form-item> -->
-			<a-form-item label="课程" name="courseId" :rules="rules.courseId">
+			<a-form-item v-if="form.gradesIdsList.length === 0" label="课程" name="courseId" :rules="rules.courseId">
 				<a-select v-model:value="form.courseId" placeholder="请选择课程" allowClear :disabled="!form.semesterId">
 					<a-select-option v-for="item in courseList" :key="item.courseId" :value="item.courseId">
 						{{ item.courseName }}
 					</a-select-option>
 				</a-select>
 			</a-form-item>
+			<a-form-item v-if="!form.semesterId" label="班级" name="gradesIdsList" :rules="rules.gradesIdsList">
+				<a-select
+					v-model:value="form.gradesIdsList"
+					mode="multiple"
+					:fieldNames="{ label: 'gradesName', value: 'gradesId' }"
+					:options="gradesQueryListOptions"
+					placeholder="请选择班级"
+					:show-search="true"
+					:filter-option="(input, option) => option.gradesName.toLowerCase().indexOf(input.toLowerCase()) >= 0"
+					allowClear
+				/>
+			</a-form-item>
 			<a-form-item label="开始时间" name="startTime" :rules="rules.startTime">
 				<a-date-picker
 					v-model:value="form.startTime"
@@ -140,6 +152,7 @@
 	import examManagerApi from '@/api/exam/paper/examManager.js'
 	import examPaperApi from '@/api/exam/paper/examPaperApi.js'
 	import resourceAuditApi from '@/api/resourceAudit.js'
+	import { gradesQueryList } from '@/api/semester/index.js'
 
 	import dayjs from 'dayjs'
 	const emit = defineEmits(['success'])
@@ -157,6 +170,7 @@
 	const semesterList = ref([])
 	const majorList = ref([])
 	const courseList = ref([])
+	const gradesQueryListOptions = ref([]) //班级
 	const form = reactive({
 		id: null,
 		examName: '',
@@ -166,7 +180,8 @@
 		endTime: null,
 		examStatus: 0,
 		semesterId: null,
-		examType: '3'
+		examType: '3',
+		gradesIdsList: []
 		// majorId: null
 	})
 
@@ -176,7 +191,8 @@
 		startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
 		endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
 		semesterId: [{ required: true, message: '请选择学期', trigger: 'change' }],
-		courseId: [{ required: true, message: '请选择课程', trigger: 'change' }]
+		courseId: [{ required: true, message: '请选择课程', trigger: 'change' }],
+		gradesIdsList: [{ required: true, message: '请选择班级', trigger: 'change' }]
 	}
 
 	const selectedPaperName = ref('')
@@ -265,8 +281,8 @@
 	}
 	// 禁用开始时间大于今天的日期
 	const disabledStartDate = (current) => {
-		// 禁用大于今天的日期
-		return current && current < dayjs().endOf('day')
+		/// 禁用小于今天的日期
+		return current && current < dayjs().startOf('day')
 	}
 
 	// 问卷类型变更
@@ -301,6 +317,16 @@
 		paperPage.queryParam.pageIndex = re.current
 		paperPage.listLoading = false
 	}
+	// 班级下拉
+	const loadGradesQueryList = () => {
+		gradesQueryList()
+			.then((res) => {
+				gradesQueryListOptions.value = res.data
+			})
+			.catch((err) => {
+				console.log(err)
+			})
+	}
 
 	// 确认选择问卷
 	const confirmPaperSelect = () => {
@@ -387,7 +413,9 @@
 					Object.assign(form, {
 						...re,
 						startTime: re.startTime ? dayjs(re.startTime) : null,
-						endTime: re.endTime ? dayjs(re.endTime) : null
+						endTime: re.endTime ? dayjs(re.endTime) : null,
+						// 解析 gradesIds 字符串为数组
+						gradesIdsList: re.gradesids ? re.gradesids.split(',').map((id) => parseInt(id.trim())) : []
 					})
 					// 如果有问卷ID,需要获取问卷名称显示
 					if (re.paperId) {
@@ -425,6 +453,7 @@
 			.finally(() => {
 				semesterLoading()
 			})
+		loadGradesQueryList()
 		// 加载专业
 		// const majorLoading = message.loading('正在加载专业列表...', 0)
 		// resourceAuditApi

+ 264 - 245
src/views/statisticalAnalysis/analysisLearningBehaviors/index.vue

@@ -25,11 +25,9 @@
 					<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-option v-for="college in collegeList" :key="college.id" :value="college.id">
+								{{ college.name }}
+							</a-select-option>
 						</a-select>
 					</div>
 					<div class="filter-group">
@@ -174,8 +172,16 @@
 </template>
 
 <script setup>
-	import { ref, reactive, computed, onMounted, nextTick } from 'vue'
+	import { ref, reactive, computed, onMounted, nextTick, onUnmounted } from 'vue'
 	import * as echarts from 'echarts'
+	import {
+		getCollegeList,
+		getCollegeStats,
+		getLoginTimeDistribution,
+		getCourseHeat,
+		getCollegeCourseDetails,
+		searchStudents
+	} from '@/api/statisticalAnalysis/analysisLearningBehaviors'
 
 	// 当前活动标签
 	const activeTab = ref('college')
@@ -186,6 +192,9 @@
 		timeRange: 30
 	})
 
+	// 学院列表
+	const collegeList = ref([])
+
 	// 学院统计数据
 	const collegeStats = reactive({
 		totalCourses: 156,
@@ -226,13 +235,19 @@
 			title: '平均完成率',
 			dataIndex: 'avgCompletionRate',
 			key: 'avgCompletionRate',
-			customRender: ({ text }) => `<span class="completion-rate">${text}%</span>`
+			customRender: ({ text }) => `${text}%`
 		},
 		{
 			title: '作业提交率',
 			dataIndex: 'assignmentSubmissionRate',
 			key: 'assignmentSubmissionRate',
-			customRender: ({ text }) => `<span class="completion-rate">${text}%</span>`
+			customRender: ({ text }) => `${text}%`
+		},
+		{
+			title: '退课率',
+			dataIndex: 'dropoutRate',
+			key: 'dropoutRate',
+			customRender: ({ text }) => `${text}%`
 		}
 	]
 
@@ -244,7 +259,8 @@
 			courseCount: 45,
 			totalVisits: 5234,
 			avgCompletionRate: 78.5,
-			assignmentSubmissionRate: 85.2
+			assignmentSubmissionRate: 85.2,
+			dropoutRate: 10.2
 		},
 		{
 			id: 2,
@@ -252,7 +268,8 @@
 			courseCount: 38,
 			totalVisits: 4567,
 			avgCompletionRate: 72.3,
-			assignmentSubmissionRate: 79.8
+			assignmentSubmissionRate: 79.8,
+			dropoutRate: 12.5
 		},
 		{
 			id: 3,
@@ -260,7 +277,8 @@
 			courseCount: 28,
 			totalVisits: 3123,
 			avgCompletionRate: 68.9,
-			assignmentSubmissionRate: 76.4
+			assignmentSubmissionRate: 76.4,
+			dropoutRate: 15.2
 		},
 		{
 			id: 4,
@@ -268,7 +286,8 @@
 			courseCount: 35,
 			totalVisits: 3890,
 			avgCompletionRate: 75.2,
-			assignmentSubmissionRate: 82.1
+			assignmentSubmissionRate: 82.1,
+			dropoutRate: 13.8
 		}
 	])
 
@@ -383,87 +402,76 @@
 				return matchName || matchId
 			})
 			.map((student) => ({ ...student, highlighted: true }))
-	}) // 切换标签
+	})
+
+	// 切换标签
 	const switchTab = (tabName) => {
 		activeTab.value = tabName
 
 		if (tabName === 'college') {
-			nextTick(() => {
-				initCollegeCharts()
+			nextTick(async () => {
+				await 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
+	const updateCollegeData = async () => {
+		try {
+			// 获取学院统计数据
+			const statsResponse = await getCollegeStats({
+				collegeId: collegeFilters.collegeId,
+				timeRange: collegeFilters.timeRange
 			})
-		} else {
-			// 全部学院
-			Object.assign(collegeStats, {
-				totalCourses: 156,
-				activeCourses: 142,
-				totalLogins: 12456,
-				uniqueUsers: 3234,
-				avgLoginPerUser: 3.85,
-				totalOnlineTime: 2456,
-				avgSessionTime: 45.2,
-				peakOnlineUsers: 234
+			Object.assign(collegeStats, statsResponse)
+
+			// 获取课程详细统计
+			const detailsResponse = await getCollegeCourseDetails({
+				collegeId: collegeFilters.collegeId,
+				timeRange: collegeFilters.timeRange
 			})
-		}
+			collegeTableData.value = detailsResponse
 
-		// 重新初始化图表
-		initCollegeCharts()
+			// 重新初始化图表
+			await initCollegeCharts()
+		} catch (error) {
+			console.error('获取学院数据失败:', error)
+		}
 	}
 
 	// 搜索学员
-	const searchStudent = () => {
-		if (!studentSearch.value.trim()) {
-			return
-		}
-
-		const found = filteredStudents.value.length > 0
-		if (!found) {
-			alert(`未找到包含"${studentSearch.value}"的学员`)
+	const searchStudent = async () => {
+		try {
+			const response = await searchStudents({
+				keyword: studentSearch.value.trim()
+			})
+			studentsData.value = response.map((student) => ({
+				...student,
+				highlighted: !!studentSearch.value.trim()
+			}))
+
+			if (response.length === 0 && studentSearch.value.trim()) {
+				alert(`未找到包含"${studentSearch.value}"的学员`)
+			}
+		} catch (error) {
+			console.error('搜索学员失败:', error)
 		}
 	}
 
 	// 清除搜索
-	const clearSearch = () => {
+	const clearSearch = async () => {
 		studentSearch.value = ''
+		await searchStudent()
 	}
 
 	// 初始化学院维度图表
-	const initCollegeCharts = () => {
-		initLoginTimeChart()
-		initCourseHeatChart()
+	const initCollegeCharts = async () => {
+		await initLoginTimeChart()
+		await initCourseHeatChart()
 	}
 
 	// 登录时段分布图表
-	const initLoginTimeChart = () => {
+	const initLoginTimeChart = async () => {
 		if (!loginTimeChart.value) return
 
 		if (loginTimeChartInstance) {
@@ -472,111 +480,106 @@
 
 		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'
+		try {
+			const response = await getLoginTimeDistribution({
+				collegeId: collegeFilters.collegeId,
+				timeRange: collegeFilters.timeRange
+			})
+
+			const { hours, loginCounts } = response
+
+			const option = {
+				title: {
+					text: '24小时登录时段分布',
+					left: 'center',
+					textStyle: {
+						color: '#2c3e50',
+						fontSize: 16,
+						fontWeight: 'bold'
 					}
 				},
-				axisLabel: {
-					color: '#2c3e50'
-				}
-			},
-			yAxis: {
-				type: 'value',
-				name: '登录人次',
-				nameTextStyle: {
-					color: '#2c3e50'
-				},
-				axisLine: {
-					lineStyle: {
-						color: '#bdc3c7'
+				tooltip: {
+					trigger: 'axis',
+					backgroundColor: 'rgba(255, 255, 255, 0.95)',
+					borderColor: '#3498db',
+					borderWidth: 1,
+					textStyle: {
+						color: '#333'
 					}
 				},
-				axisLabel: {
-					color: '#2c3e50'
-				},
-				splitLine: {
-					lineStyle: {
-						color: '#ecf0f1'
+				xAxis: {
+					type: 'category',
+					data: hours,
+					axisLine: {
+						lineStyle: {
+							color: '#bdc3c7'
+						}
+					},
+					axisLabel: {
+						color: '#2c3e50'
 					}
-				}
-			},
-			series: [
-				{
+				},
+				yAxis: {
+					type: 'value',
 					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'
-								}
-							]
+					nameTextStyle: {
+						color: '#2c3e50'
+					},
+					axisLine: {
+						lineStyle: {
+							color: '#bdc3c7'
 						}
 					},
-					emphasis: {
+					axisLabel: {
+						color: '#2c3e50'
+					},
+					splitLine: {
+						lineStyle: {
+							color: '#ecf0f1'
+						}
+					}
+				},
+				series: [
+					{
+						name: '登录人次',
+						type: 'bar',
+						data: loginCounts,
 						itemStyle: {
-							color: '#e74c3c'
+							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)
+			loginTimeChartInstance.setOption(option)
+		} catch (error) {
+			console.error('获取登录时段数据失败:', error)
+		}
 	}
 
 	// 课程访问热度图表
-	const initCourseHeatChart = () => {
+	const initCourseHeatChart = async () => {
 		if (!courseHeatChart.value) return
 
 		if (courseHeatChartInstance) {
@@ -585,103 +588,102 @@
 
 		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'
+		try {
+			const response = await getCourseHeat({
+				collegeId: collegeFilters.collegeId,
+				timeRange: collegeFilters.timeRange
+			})
+
+			const { courses, visits } = response
+
+			const option = {
+				title: {
+					text: '课程访问热度TOP8',
+					left: 'center',
+					textStyle: {
+						color: '#2c3e50',
+						fontSize: 16,
+						fontWeight: 'bold'
 					}
 				},
-				axisLabel: {
-					color: '#2c3e50'
-				},
-				splitLine: {
-					lineStyle: {
-						color: '#ecf0f1'
-					}
-				}
-			},
-			yAxis: {
-				type: 'category',
-				data: courses,
-				axisLine: {
-					lineStyle: {
-						color: '#bdc3c7'
+				tooltip: {
+					trigger: 'axis',
+					backgroundColor: 'rgba(255, 255, 255, 0.95)',
+					borderColor: '#3498db',
+					borderWidth: 1,
+					textStyle: {
+						color: '#333'
 					}
 				},
-				axisLabel: {
-					color: '#2c3e50'
-				}
-			},
-			series: [
-				{
+				xAxis: {
+					type: 'value',
 					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'
-								}
-							]
+					nameTextStyle: {
+						color: '#2c3e50'
+					},
+					axisLine: {
+						lineStyle: {
+							color: '#bdc3c7'
+						}
+					},
+					axisLabel: {
+						color: '#2c3e50'
+					},
+					splitLine: {
+						lineStyle: {
+							color: '#ecf0f1'
+						}
+					}
+				},
+				yAxis: {
+					type: 'category',
+					data: courses,
+					axisLine: {
+						lineStyle: {
+							color: '#bdc3c7'
 						}
 					},
-					emphasis: {
+					axisLabel: {
+						color: '#2c3e50'
+					}
+				},
+				series: [
+					{
+						name: '访问量',
+						type: 'bar',
+						data: visits,
 						itemStyle: {
-							color: '#3498db'
+							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)
+			courseHeatChartInstance.setOption(option)
+		} catch (error) {
+			console.error('获取课程热度数据失败:', error)
+		}
 	}
 
 	// 响应式处理
@@ -690,10 +692,27 @@
 		courseHeatChartInstance?.resize()
 	}
 
+	// 初始化数据
+	const initData = async () => {
+		try {
+			// 获取学院列表
+			const colleges = await getCollegeList()
+			collegeList.value = colleges
+
+			// 获取初始学员数据
+			await searchStudent()
+
+			// 获取学院数据
+			await updateCollegeData()
+		} catch (error) {
+			console.error('初始化数据失败:', error)
+		}
+	}
+
 	// 组件挂载后初始化
 	onMounted(() => {
-		nextTick(() => {
-			initCollegeCharts()
+		nextTick(async () => {
+			await initData()
 			window.addEventListener('resize', handleResize)
 		})
 	})

+ 194 - 158
src/views/statisticalAnalysis/analysisTeachingActivities/index.vue

@@ -12,12 +12,10 @@
 			<div class="filter-controls">
 				<div class="filter-group">
 					<label>选择课程</label>
-					<a-select v-model:value="filters.courseId" placeholder="全部课程" @change="updateStats">
-						<a-select-option value="">全部课程</a-select-option>
-						<a-select-option value="course1">JavaScript基础教程</a-select-option>
-						<a-select-option value="course2">Python数据分析</a-select-option>
-						<a-select-option value="course3">React前端开发</a-select-option>
-						<a-select-option value="course4">机器学习入门</a-select-option>
+					<a-select v-model:value="filters.courseId" placeholder="全部课程" @change="updateStats" :loading="loading">
+						<a-select-option v-for="option in courseOptions" :key="option.value" :value="option.value">
+							{{ option.label }}
+						</a-select-option>
 					</a-select>
 				</div>
 				<div class="filter-group">
@@ -30,7 +28,7 @@
 					</a-select>
 				</div>
 				<div class="filter-group">
-					<a-button type="primary" @click="updateStats">查询</a-button>
+					<a-button type="primary" @click="updateStats" :loading="loading">查询</a-button>
 				</div>
 			</div>
 		</div>
@@ -100,20 +98,42 @@
 		<!-- 讨论详情表格 -->
 		<div class="data-table">
 			<h3>💬 课程讨论详情</h3>
-			<a-table :columns="discussionColumns" :data-source="discussionData" :pagination="{ pageSize: 10 }" row-key="id" />
+			<a-table
+				:columns="discussionColumns"
+				:data-source="discussionData"
+				:pagination="discussionPagination"
+				:loading="loading"
+				row-key="id"
+				@change="handleDiscussionTableChange"
+			/>
 		</div>
 
 		<!-- 文档观看详细统计 -->
 		<div class="data-table">
 			<h3>📚 文档观看详细统计</h3>
-			<a-table :columns="documentColumns" :data-source="documentData" :pagination="{ pageSize: 10 }" row-key="id" />
+			<a-table
+				:columns="documentColumns"
+				:data-source="documentData"
+				:pagination="documentPagination"
+				:loading="loading"
+				row-key="id"
+				@change="handleDocumentTableChange"
+			/>
 		</div>
 	</div>
 </template>
 
 <script setup>
-	import { ref, reactive, onMounted, nextTick } from 'vue'
+	import { ref, reactive, onMounted, nextTick, h } from 'vue'
 	import * as echarts from 'echarts'
+	import {
+		getCourseOptions,
+		getTeachingStats,
+		getWeeklyStats,
+		getDailyVisits,
+		getDiscussionData,
+		getDocumentStats
+	} from '@/api/statisticalAnalysis/analysisTeachingActivities'
 
 	// 响应式数据
 	const filters = reactive({
@@ -122,26 +142,29 @@
 	})
 
 	const stats = reactive({
-		totalDocViewers: 1856,
-		completedDocViewers: 1234,
-		docCompletionRate: 66.5,
-		totalDocExits: 456,
-		docExitRate: 24.6,
-		avgDocExitTime: '08:45',
-		totalDiscussions: 234,
-		totalReplies: 1567,
-		avgRepliesPerDiscussion: 6.7
+		totalDocViewers: 0,
+		completedDocViewers: 0,
+		docCompletionRate: 0,
+		totalDocExits: 0,
+		docExitRate: 0,
+		avgDocExitTime: '00:00',
+		totalDiscussions: 0,
+		totalReplies: 0,
+		avgRepliesPerDiscussion: 0
 	})
 
 	const weeklyStats = reactive({
-		studentWeeklyPosts: 67,
-		studentWeeklyReplies: 234,
-		studentAvgPostsPerDay: 9.6,
-		teacherWeeklyPosts: 23,
-		teacherWeeklyReplies: 89,
-		teacherAvgPostsPerDay: 3.3
+		studentWeeklyPosts: 0,
+		studentWeeklyReplies: 0,
+		studentAvgPostsPerDay: 0,
+		teacherWeeklyPosts: 0,
+		teacherWeeklyReplies: 0,
+		teacherAvgPostsPerDay: 0
 	})
 
+	// 课程选项
+	const courseOptions = ref([])
+
 	// 图表引用
 	const dailyVisitsChart = ref(null)
 	let chartInstance = null
@@ -176,48 +199,12 @@
 	]
 
 	// 讨论数据
-	const discussionData = ref([
-		{
-			id: 1,
-			title: 'JavaScript闭包概念理解',
-			author: '张三',
-			createTime: '2024-01-15 10:30',
-			replyCount: 15,
-			lastReplyTime: '2024-01-16 14:20'
-		},
-		{
-			id: 2,
-			title: 'Python数据分析库选择',
-			author: '李四',
-			createTime: '2024-01-14 16:45',
-			replyCount: 8,
-			lastReplyTime: '2024-01-15 09:15'
-		},
-		{
-			id: 3,
-			title: 'React组件生命周期',
-			author: '王五',
-			createTime: '2024-01-13 11:20',
-			replyCount: 12,
-			lastReplyTime: '2024-01-14 17:30'
-		},
-		{
-			id: 4,
-			title: '机器学习算法选择',
-			author: '赵六',
-			createTime: '2024-01-12 14:15',
-			replyCount: 6,
-			lastReplyTime: '2024-01-13 10:45'
-		},
-		{
-			id: 5,
-			title: '前端性能优化技巧',
-			author: '钱七',
-			createTime: '2024-01-11 09:30',
-			replyCount: 20,
-			lastReplyTime: '2024-01-15 16:20'
-		}
-	])
+	const discussionData = ref([])
+	const discussionPagination = reactive({
+		current: 1,
+		pageSize: 10,
+		total: 0
+	})
 
 	// 文档表格列定义
 	const documentColumns = [
@@ -267,7 +254,7 @@
 			customRender: ({ text }) => {
 				const rate = parseFloat(text)
 				const className = rate > 30 ? 'low-engagement' : ''
-				return h('span', { class: className }, `${text}$`)
+				return h('span', { class: className }, `${text}%`)
 			}
 		},
 		{
@@ -279,91 +266,20 @@
 	]
 
 	// 文档数据
-	const documentData = ref([
-		{
-			id: 1,
-			name: 'JavaScript基础语法',
-			type: 'PDF文档',
-			viewCount: 1856,
-			completedCount: 1234,
-			completionRate: 66.5,
-			avgReadTime: '25:30',
-			exitRate: 24.6,
-			downloadCount: 456
-		},
-		{
-			id: 2,
-			name: 'Python数据分析入门',
-			type: '在线文档',
-			viewCount: 1234,
-			completedCount: 987,
-			completionRate: 80.0,
-			avgReadTime: '32:15',
-			exitRate: 18.5,
-			downloadCount: 234
-		},
-		{
-			id: 3,
-			name: 'React组件开发指南',
-			type: 'PDF文档',
-			viewCount: 987,
-			completedCount: 756,
-			completionRate: 76.6,
-			avgReadTime: '28:45',
-			exitRate: 22.3,
-			downloadCount: 189
-		},
-		{
-			id: 4,
-			name: '机器学习算法详解',
-			type: '在线文档',
-			viewCount: 756,
-			completedCount: 523,
-			completionRate: 69.2,
-			avgReadTime: '45:20',
-			exitRate: 30.8,
-			downloadCount: 123
-		},
-		{
-			id: 5,
-			name: '前端工程化实践',
-			type: 'PDF文档',
-			viewCount: 523,
-			completedCount: 345,
-			completionRate: 66.0,
-			avgReadTime: '38:15',
-			exitRate: 34.0,
-			downloadCount: 89
-		}
-	])
-	// 生成日期数据
-
-	const generateDateData = (days) => {
-		const dates = []
-		const today = new Date()
-
-		for (let i = days - 1; i >= 0; i--) {
-			const date = new Date(today)
-			date.setDate(date.getDate() - i)
-			dates.push(date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }))
-		}
+	const documentData = ref([])
+	const documentPagination = reactive({
+		current: 1,
+		pageSize: 10,
+		total: 0
+	})
+	// 图表数据
+	const chartData = reactive({
+		dates: [],
+		visits: []
+	})
 
-		return dates
-	}
-
-	// 生成访问数据
-	const generateVisitData = (days) => {
-		const data = []
-		for (let i = 0; i < days; i++) {
-			// 模拟真实数据:工作日访问量较高,周末较低
-			const dayOfWeek = (new Date().getDay() - days + i + 7) % 7
-			const isWeekend = dayOfWeek === 0 || dayOfWeek === 6
-			const baseValue = isWeekend ? 120 : 200
-			const randomFactor = 0.7 + Math.random() * 0.6 // 0.7-1.3的随机因子
-			data.push(Math.round(baseValue * randomFactor))
-		}
-		return data
-	}
+	// 加载状态
+	const loading = ref(false)
 
 	// 初始化图表
 	const initChart = () => {
@@ -383,8 +299,7 @@
 		if (!chartInstance) return
 
 		const days = filters.timeRange
-		const dates = generateDateData(days)
-		const visits = generateVisitData(days)
+		const { dates, visits } = chartData
 
 		const option = {
 			title: {
@@ -499,16 +414,137 @@
 		chartInstance.setOption(option, true)
 	}
 
-	// 更新统计数据
-	const updateStats = () => {
-		// 模拟数据更新
-		updateChartData()
+	// 获取课程选项
+	const loadCourseOptions = async () => {
+		try {
+			const response = await getCourseOptions()
+			courseOptions.value = response
+		} catch (error) {
+			console.error('获取课程选项失败:', error)
+		}
+	}
+
+	// 获取核心统计数据
+	const loadTeachingStats = async () => {
+		try {
+			const response = await getTeachingStats({
+				courseId: filters.courseId,
+				timeRange: filters.timeRange
+			})
+			Object.assign(stats, response)
+		} catch (error) {
+			console.error('获取统计数据失败:', error)
+		}
+	}
+
+	// 获取每周统计数据
+	const loadWeeklyStats = async () => {
+		try {
+			const response = await getWeeklyStats({
+				courseId: filters.courseId
+			})
+			Object.assign(weeklyStats, response)
+		} catch (error) {
+			console.error('获取每周统计数据失败:', error)
+		}
+	}
+
+	// 获取每日访问数据
+	const loadDailyVisits = async () => {
+		try {
+			const response = await getDailyVisits({
+				timeRange: filters.timeRange
+			})
+			chartData.dates = response.dates
+			chartData.visits = response.visits
+			updateChartData()
+		} catch (error) {
+			console.error('获取每日访问数据失败:', error)
+		}
+	}
+
+	// 获取讨论数据
+	const loadDiscussionData = async () => {
+		try {
+			const response = await getDiscussionData({
+				courseId: filters.courseId,
+				page: discussionPagination.current,
+				pageSize: discussionPagination.pageSize
+			})
+			discussionData.value = response.list
+			discussionPagination.total = response.total
+		} catch (error) {
+			console.error('获取讨论数据失败:', error)
+		}
+	}
+
+	// 获取文档数据
+	const loadDocumentData = async () => {
+		try {
+			const response = await getDocumentStats({
+				courseId: filters.courseId,
+				page: documentPagination.current,
+				pageSize: documentPagination.pageSize
+			})
+			documentData.value = response.list
+			documentPagination.total = response.total
+		} catch (error) {
+			console.error('获取文档数据失败:', error)
+		}
+	}
+
+	// 更新所有数据
+	const updateStats = async () => {
+		loading.value = true
+		try {
+			await Promise.all([
+				loadTeachingStats(),
+				loadWeeklyStats(),
+				loadDailyVisits(),
+				loadDiscussionData(),
+				loadDocumentData()
+			])
+		} finally {
+			loading.value = false
+		}
+	}
+
+	// 初始化所有数据
+	const initData = async () => {
+		loading.value = true
+		try {
+			await Promise.all([
+				loadCourseOptions(),
+				loadTeachingStats(),
+				loadWeeklyStats(),
+				loadDailyVisits(),
+				loadDiscussionData(),
+				loadDocumentData()
+			])
+		} finally {
+			loading.value = false
+		}
+	}
+
+	// 处理讨论表格分页变化
+	const handleDiscussionTableChange = (pagination) => {
+		discussionPagination.current = pagination.current
+		discussionPagination.pageSize = pagination.pageSize
+		loadDiscussionData()
+	}
+
+	// 处理文档表格分页变化
+	const handleDocumentTableChange = (pagination) => {
+		documentPagination.current = pagination.current
+		documentPagination.pageSize = pagination.pageSize
+		loadDocumentData()
 	}
 
 	// 组件挂载后初始化
 	onMounted(() => {
 		nextTick(() => {
 			initChart()
+			initData()
 		})
 	})
 </script>

+ 156 - 72
src/views/statisticalAnalysis/overviewLearningProgress/index.vue

@@ -9,10 +9,9 @@
 					<div class="relative">
 						<a-select v-model:value="filters.courseName" class="w-full" placeholder="全部课程">
 							<a-select-option value="">全部课程</a-select-option>
-							<a-select-option value="python">Python编程入门</a-select-option>
-							<a-select-option value="datastructure">数据结构与算法</a-select-option>
-							<a-select-option value="ml">机器学习基础</a-select-option>
-							<a-select-option value="frontend">Web前端开发</a-select-option>
+							<a-select-option v-for="course in courseOptions" :key="course.id" :value="course.courseName">
+								{{ course.courseName }}
+							</a-select-option>
 						</a-select>
 					</div>
 				</div>
@@ -123,10 +122,12 @@
 </template>
 
 <script setup>
-	import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+	import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue'
 	import { ReloadOutlined, UserOutlined, EyeOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons-vue'
 	import * as echarts from 'echarts'
 	import dayjs from 'dayjs'
+	import { overviewLearningProgressApi } from '@/api/statisticalAnalysis/overviewLearningProgress'
+	import { message } from 'ant-design-vue'
 
 	// 响应式数据
 	const loading = ref(false)
@@ -222,75 +223,46 @@
 			dataIndex: 'submissionCount',
 			key: 'submissionCount',
 			sorter: true
+		},
+		{
+			title: '退课人数',
+			dataIndex: 'numberStudentsDroppingCourses',
+			key: 'numberStudentsDroppingCourses',
+			sorter: true
 		}
 	]
 
 	// 表格数据
-	const tableData = ref([
-		{
-			key: '1',
-			courseName: 'Python编程基础',
-			studentCount: 60,
-			visitCount: 1245,
-			videoDuration: '5h12m30s',
-			documentVisit: 453,
-			submissionCount: 94
-		},
-		{
-			key: '2',
-			courseName: '数据结构 - 栈与队列',
-			studentCount: 70,
-			visitCount: 1087,
-			videoDuration: '5h12m30s',
-			documentVisit: 389,
-			submissionCount: 88
-		},
-		{
-			key: '3',
-			courseName: 'Web前端框架实战',
-			studentCount: 80,
-			visitCount: 987,
-			videoDuration: '5h12m30s',
-			documentVisit: 326,
-			submissionCount: 79
-		},
-		{
-			key: '4',
-			courseName: '机器学习入门',
-			studentCount: 90,
-			visitCount: 856,
-			videoDuration: '5h12m30s',
-			documentVisit: 278,
-			submissionCount: 75
-		},
-		{
-			key: '5',
-			courseName: '数据库设计原理',
-			studentCount: 100,
-			visitCount: 723,
-			videoDuration: '5h12m30s',
-			documentVisit: 214,
-			submissionCount: 72
-		},
-		{
-			key: '6',
-			courseName: '算法复杂度分析',
-			studentCount: 110,
-			visitCount: 634,
-			videoDuration: '5h12m30s',
-			documentVisit: 196,
-			submissionCount: 68
-		}
-	])
+	const tableData = ref([])
+
+	// 图表数据
+	const chartData = reactive({
+		dates: [],
+		visitTrend: [],
+		submissionTrend: []
+	})
+
+	// 课程选项
+	const courseOptions = ref([])
 
 	// 分页配置
 	const pagination = reactive({
 		current: 1,
 		pageSize: 6,
-		total: 32,
+		total: 0,
 		showSizeChanger: true,
 		showQuickJumper: true,
-		showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`
+		showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`,
+		onChange: (page, pageSize) => {
+			pagination.current = page
+			pagination.pageSize = pageSize
+			fetchCourseList()
+		},
+		onShowSizeChange: (current, size) => {
+			pagination.current = 1
+			pagination.pageSize = size
+			fetchCourseList()
+		}
 	})
 
 	// 初始化折线图
@@ -298,12 +270,19 @@
 		if (!lineChartRef.value) return
 
 		lineChart = echarts.init(lineChartRef.value)
+		updateLineChart()
+	}
+
+	// 更新折线图数据
+	const updateLineChart = () => {
+		if (!lineChart) return
+
 		const option = {
 			grid: { top: 30, right: 20, bottom: 20, left: 40 },
 			tooltip: { trigger: 'axis' },
 			xAxis: {
 				type: 'category',
-				data: ['4日', '5日', '6日', '7日', '8日', '9日', '10日']
+				data: chartData.dates
 			},
 			yAxis: { type: 'value', name: '访问人数' },
 			series: [
@@ -313,7 +292,7 @@
 					smooth: true,
 					symbol: 'circle',
 					symbolSize: 8,
-					data: [2150, 2380, 1920, 2650, 2210, 2490, 2000],
+					data: chartData.visitTrend,
 					lineStyle: { color: '#3A7BFF', width: 3 },
 					itemStyle: { color: '#3A7BFF' },
 					areaStyle: {
@@ -333,12 +312,19 @@
 		if (!barChartRef.value) return
 
 		barChart = echarts.init(barChartRef.value)
+		updateBarChart()
+	}
+
+	// 更新柱状图数据
+	const updateBarChart = () => {
+		if (!barChart) return
+
 		const option = {
 			grid: { top: 30, right: 20, bottom: 20, left: 40 },
 			tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
 			xAxis: {
 				type: 'category',
-				data: ['4日', '5日', '6日', '7日', '8日', '9日', '10日']
+				data: chartData.dates
 			},
 			yAxis: { type: 'value', name: '平均提交数' },
 			series: [
@@ -346,7 +332,7 @@
 					name: '练习平均提交数',
 					type: 'bar',
 					barWidth: 28,
-					data: [82, 85, 78, 89, 76, 91, 83],
+					data: chartData.submissionTrend,
 					itemStyle: {
 						color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
 							{ offset: 0, color: '#5B8EFF' },
@@ -370,14 +356,90 @@
 		return `${start}至${end}`
 	}
 
-	// 刷新数据
+	// 获取统计概览数据
+	const fetchOverviewStats = async () => {
+		try {
+			const params = {
+				courseName: filters.courseName,
+				contentType: filters.contentType,
+				dateRange: filters.dateRange
+			}
+			const data = await overviewLearningProgressApi.getOverviewStats(params)
+			// 更新统计卡片数据
+			statsCards[0].value = data.totalStudents
+			statsCards[1].value = data.totalVisits
+			statsCards[2].value = data.totalSubmissions
+			statsCards[3].value = data.totalInteractions
+		} catch (error) {
+			console.error('获取统计数据失败:', error)
+			message.error('获取统计数据失败')
+		}
+	}
+
+	// 获取趋势数据
+	const fetchTrendData = async () => {
+		try {
+			const params = {
+				courseName: filters.courseName,
+				contentType: filters.contentType,
+				dateRange: filters.dateRange
+			}
+			const data = await overviewLearningProgressApi.getTrendData(params)
+
+			// 更新图表数据
+			chartData.dates = data.dates
+			chartData.visitTrend = data.visitTrend
+			chartData.submissionTrend = data.submissionTrend
+
+			// 更新图表
+			updateLineChart()
+			updateBarChart()
+		} catch (error) {
+			console.error('获取趋势数据失败:', error)
+			message.error('获取趋势数据失败')
+		}
+	}
+
+	// 获取课程选项
+	const fetchCourseOptions = async () => {
+		try {
+			const data = await overviewLearningProgressApi.getCourseOptions()
+			courseOptions.value = data
+		} catch (error) {
+			console.error('获取课程选项失败:', error)
+		}
+	}
+
+	// 获取课程列表数据
+	const fetchCourseList = async () => {
+		try {
+			const params = {
+				courseName: filters.courseName,
+				contentType: filters.contentType,
+				dateRange: filters.dateRange,
+				current: pagination.current,
+				pageSize: pagination.pageSize
+			}
+			const response = await overviewLearningProgressApi.getCourseList(params)
+			// 更新表格数据
+			tableData.value = response.data.map((item, index) => ({
+				...item,
+				key: item.id || index + 1
+			}))
+
+			// 更新分页信息
+			pagination.total = response.total
+		} catch (error) {
+			console.error('获取课程列表失败:', error)
+			message.error('获取课程列表失败')
+		}
+	}
+
+	// 刷新所有数据
 	const refreshData = async () => {
 		loading.value = true
 		try {
-			// 模拟API调用
-			await new Promise((resolve) => setTimeout(resolve, 1000))
-			// 这里可以调用实际的API来获取数据
-			console.log('刷新数据', filters)
+			await Promise.all([fetchOverviewStats(), fetchTrendData(), fetchCourseList()])
 		} catch (error) {
 			console.error('刷新数据失败:', error)
 		} finally {
@@ -385,18 +447,40 @@
 		}
 	}
 
+	// 初始化数据
+	const initData = async () => {
+		// 先获取课程选项
+		await fetchCourseOptions()
+		// 再获取其他数据
+		await refreshData()
+	}
+
 	// 窗口大小变化处理
 	const handleResize = () => {
 		if (lineChart) lineChart.resize()
 		if (barChart) barChart.resize()
 	}
 
+	// 监听筛选条件变化
+	watch(
+		() => [filters.courseName, filters.contentType, filters.dateRange],
+		() => {
+			// 筛选条件变化时重置到第一页
+			pagination.current = 1
+			refreshData()
+		},
+		{ deep: true }
+	)
+
 	// 生命周期
 	onMounted(async () => {
 		await nextTick()
 		initLineChart()
 		initBarChart()
 		window.addEventListener('resize', handleResize)
+
+		// 初始化数据
+		await initData()
 	})
 
 	onUnmounted(() => {

+ 89 - 130
src/views/statisticalAnalysis/platformStatusOverview/index.vue

@@ -34,7 +34,21 @@
 		<!-- 开课信息列表 -->
 		<div class="section">
 			<h2>全院开课信息</h2>
-			<a-table :columns="courseInfoColumns" :data-source="courseInfoData" :pagination="{ pageSize: 10 }" row-key="id" />
+			<a-table
+				:columns="courseInfoColumns"
+				:data-source="courseInfoData"
+				:pagination="{
+					current: courseInfoPagination.current,
+					pageSize: courseInfoPagination.pageSize,
+					total: courseInfoPagination.total,
+					showSizeChanger: true,
+					showQuickJumper: true,
+					showTotal: (total) => `共 ${total} 条记录`,
+					onChange: handlePageChange,
+					onShowSizeChange: handlePageChange
+				}"
+				row-key="id"
+			/>
 		</div>
 
 		<!-- 访问统计弹窗 -->
@@ -52,15 +66,21 @@
 </template>
 
 <script setup>
-	import { ref, reactive, nextTick } from 'vue'
+	import { ref, reactive, nextTick, onMounted } from 'vue'
 	import * as echarts from 'echarts'
+	import {
+		getPlatformStats,
+		getHotCourses,
+		getCourseInfo,
+		getCourseVisitStats
+	} from '@/api/statisticalAnalysis/platformStatusOverview'
 
 	// 平台统计数据
 	const platformStats = reactive({
-		totalCourses: 128,
-		openedCourses: 97,
-		totalTeachers: 34,
-		totalStudents: 1256
+		totalCourses: 0,
+		openedCourses: 0,
+		totalTeachers: 0,
+		totalStudents: 0
 	})
 
 	// 热门课程表格列定义
@@ -97,48 +117,7 @@
 	]
 
 	// 热门课程数据
-	const hotCoursesData = ref([
-		{
-			id: 1,
-			rank: 1,
-			name: 'Python数据分析',
-			teacher: '王老师',
-			visits: '2,345',
-			references: 56
-		},
-		{
-			id: 2,
-			rank: 2,
-			name: 'JavaScript前端开发',
-			teacher: '李老师',
-			visits: '2,123',
-			references: 48
-		},
-		{
-			id: 3,
-			rank: 3,
-			name: '机器学习基础',
-			teacher: '张老师',
-			visits: '1,987',
-			references: 41
-		},
-		{
-			id: 4,
-			rank: 4,
-			name: '管理学原理',
-			teacher: '赵老师',
-			visits: '1,876',
-			references: 39
-		},
-		{
-			id: 5,
-			rank: 5,
-			name: '大学英语',
-			teacher: '钱老师',
-			visits: '1,765',
-			references: 35
-		}
-	])
+	const hotCoursesData = ref([])
 
 	// 开课信息表格列定义
 	const courseInfoColumns = [
@@ -180,48 +159,12 @@
 	]
 
 	// 开课信息数据
-	const courseInfoData = ref([
-		{
-			id: 1,
-			name: 'Python数据分析',
-			teacher: '王老师',
-			startDate: '2024-03-01',
-			studentCount: 320,
-			visits: '2,345'
-		},
-		{
-			id: 2,
-			name: 'JavaScript前端开发',
-			teacher: '李老师',
-			startDate: '2024-03-10',
-			studentCount: 280,
-			visits: '2,123'
-		},
-		{
-			id: 3,
-			name: '机器学习基础',
-			teacher: '张老师',
-			startDate: '2024-03-15',
-			studentCount: 210,
-			visits: '1,987'
-		},
-		{
-			id: 4,
-			name: '管理学原理',
-			teacher: '赵老师',
-			startDate: '2024-03-20',
-			studentCount: 180,
-			visits: '1,876'
-		},
-		{
-			id: 5,
-			name: '大学英语',
-			teacher: '钱老师',
-			startDate: '2024-03-25',
-			studentCount: 266,
-			visits: '1,765'
-		}
-	])
+	const courseInfoData = ref([])
+	const courseInfoPagination = reactive({
+		current: 1,
+		pageSize: 10,
+		total: 0
+	})
 
 	// 弹窗相关
 	const visitModalVisible = ref(false)
@@ -229,42 +172,63 @@
 	const visitChart = ref(null)
 	let chartInstance = null
 
-	// 模拟课程每日访问数据
-	const courseVisitData = {
-		Python数据分析: [120, 156, 178, 210, 234, 245, 267, 289, 312, 234, 198, 156, 145, 134],
-		JavaScript前端开发: [98, 123, 145, 167, 189, 201, 223, 245, 267, 189, 156, 134, 123, 112],
-		机器学习基础: [67, 89, 112, 134, 156, 178, 189, 201, 223, 156, 134, 123, 112, 101],
-		管理学原理: [56, 78, 98, 123, 145, 167, 189, 201, 223, 145, 123, 112, 101, 89],
-		大学英语: [45, 67, 89, 112, 134, 156, 178, 189, 201, 134, 112, 101, 89, 78]
+	// 加载数据的方法
+	const loadPlatformStats = async () => {
+		try {
+			const response = await getPlatformStats()
+			Object.assign(platformStats, response.data)
+		} catch (error) {
+			console.error('获取平台统计数据失败:', error)
+		}
 	}
 
-	// 获取近7天的访问数据
-	const getLast7DaysData = (courseName) => {
-		const baseData = courseVisitData[courseName] || [100, 120, 140, 160, 180, 200, 220]
-		const last7Days = []
+	const loadHotCourses = async () => {
+		try {
+			const response = await getHotCourses()
+			hotCoursesData.value = response.data
+		} catch (error) {
+			console.error('获取热门课程数据失败:', error)
+		}
+	}
 
-		for (let i = 0; i < 7; i++) {
-			const baseValue = baseData[i] || 150
-			const randomFactor = 0.8 + Math.random() * 0.4 // 0.8-1.2的随机因子
-			const dailyVisits = Math.round(baseValue * randomFactor)
-			last7Days.push(dailyVisits)
+	const loadCourseInfo = async (params = {}) => {
+		try {
+			const response = await getCourseInfo(params)
+			courseInfoData.value = response.data.records
+			courseInfoPagination.total = response.data.total
+			courseInfoPagination.current = response.data.current
+		} catch (error) {
+			console.error('获取开课信息失败:', error)
 		}
+	}
 
-		return last7Days
+	// 分页变化处理
+	const handlePageChange = (page, pageSize) => {
+		courseInfoPagination.current = page
+		courseInfoPagination.pageSize = pageSize
+		loadCourseInfo({
+			current: page,
+			size: pageSize
+		})
 	}
 
 	// 显示访问统计弹窗
-	const showVisitModal = (courseName) => {
+	const showVisitModal = async (courseName) => {
 		modalTitle.value = `${courseName} - 近7天访问统计`
 		visitModalVisible.value = true
 
-		nextTick(() => {
-			initVisitChart(courseName)
-		})
+		try {
+			const response = await getCourseVisitStats({ courseName })
+			nextTick(() => {
+				initVisitChart(response.data)
+			})
+		} catch (error) {
+			console.error('获取课程访问统计失败:', error)
+		}
 	}
 
 	// 初始化访问统计图表
-	const initVisitChart = (courseName) => {
+	const initVisitChart = (data) => {
 		if (!visitChart.value) return
 
 		if (chartInstance) {
@@ -273,19 +237,7 @@
 
 		chartInstance = echarts.init(visitChart.value)
 
-		// 构造近7天的日期
-		const days = []
-		const today = new Date()
-		for (let i = 6; i >= 0; i--) {
-			const date = new Date(today)
-			date.setDate(date.getDate() - i)
-			const month = (date.getMonth() + 1).toString().padStart(2, '0')
-			const day = date.getDate().toString().padStart(2, '0')
-			days.push(`${month}-${day}`)
-		}
-
-		// 获取近7天的访问数据
-		const visits = getLast7DaysData(courseName)
+		const { visitData, dateRange } = data
 
 		const option = {
 			title: {
@@ -304,7 +256,7 @@
 			},
 			xAxis: {
 				type: 'category',
-				data: days,
+				data: dateRange,
 				axisLabel: {
 					color: '#2c3e50'
 				}
@@ -323,7 +275,7 @@
 				{
 					name: '访问量',
 					type: 'line',
-					data: visits,
+					data: visitData,
 					smooth: true,
 					itemStyle: { color: '#3498db' },
 					areaStyle: {
@@ -365,6 +317,16 @@
 		}
 	}
 
+	// 初始化数据
+	const initData = async () => {
+		await Promise.all([loadPlatformStats(), loadHotCourses(), loadCourseInfo()])
+	}
+
+	// 组件挂载时加载数据
+	onMounted(() => {
+		initData()
+	})
+
 	// 将showVisitModal方法暴露到全局,供表格按钮调用
 	window.showVisitModal = showVisitModal
 </script>
@@ -383,7 +345,6 @@
 		margin-bottom: 30px;
 		box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
 		text-align: center;
-		max-width: 1300px;
 		margin-left: auto;
 		margin-right: auto;
 		margin-bottom: 30px;
@@ -405,7 +366,6 @@
 		grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
 		gap: 25px;
 		margin-bottom: 30px;
-		max-width: 1300px;
 		margin-left: auto;
 		margin-right: auto;
 		margin-bottom: 30px;
@@ -443,7 +403,6 @@
 		padding: 25px 20px;
 		margin-bottom: 30px;
 		box-shadow: 0 4px 16px rgba(44, 62, 80, 0.07);
-		max-width: 1300px;
 		margin-left: auto;
 		margin-right: auto;
 		margin-bottom: 30px;

+ 876 - 0
src/views/statisticalAnalysis/statisticalAnalysisResourceLibrary/index.vue

@@ -0,0 +1,876 @@
+<template>
+	<div class="resource-statistics">
+		<div class="container">
+			<h1>资源库统计分析</h1>
+
+			<!-- 摘要信息 -->
+			<div class="summary-row">
+				<div class="summary-box">
+					<h3>总资源数</h3>
+					<p>{{ summaryData.totalResources }}</p>
+				</div>
+				<div class="summary-box">
+					<h3>总存储空间</h3>
+					<p>{{ summaryData.totalStorage }}</p>
+				</div>
+				<div class="summary-box">
+					<h3>总观看人数</h3>
+					<p>{{ summaryData.totalViews }}</p>
+				</div>
+				<div class="summary-box">
+					<h3>总收藏人数</h3>
+					<p>{{ summaryData.totalFavorites }}</p>
+				</div>
+			</div>
+
+			<!-- 存储空间信息 -->
+			<div class="storage-info">
+				<h3>资源库存储空间使用情况</h3>
+				<p>
+					当前资源库总容量:{{ summaryData.totalCapacity }} | 已使用:{{ summaryData.totalStorage }} | 使用率:{{
+						summaryData.usageRate
+					}}
+				</p>
+			</div>
+
+			<!-- 筛选器 -->
+			<div class="filters">
+				<label>筛选院系:</label>
+				<a-select
+					v-model:value="filters.department"
+					style="width: 150px; margin-right: 20px"
+					@change="handleDepartmentChange"
+				>
+					<a-select-option value="all">全部院系</a-select-option>
+					<a-select-option value="aviation">航空学院</a-select-option>
+					<a-select-option value="military">军事管理系</a-select-option>
+					<a-select-option value="politics">政治工作部</a-select-option>
+					<a-select-option value="maintenance">地面维修中心</a-select-option>
+					<a-select-option value="other">其他部门</a-select-option>
+				</a-select>
+
+				<label>时间范围:</label>
+				<a-select v-model:value="filters.timeRange" style="width: 120px" @change="handleTimeChange">
+					<a-select-option value="all">全部时间</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="180">最近180天</a-select-option>
+					<a-select-option value="365">最近1年</a-select-option>
+				</a-select>
+			</div>
+
+			<!-- 资源类型分布 -->
+			<div class="chart-container">
+				<h2>资源类型分布</h2>
+				<div class="chart-row">
+					<div class="chart-col">
+						<div ref="typeChartRef" class="chart"></div>
+					</div>
+					<div class="chart-col">
+						<div ref="departmentChartRef" class="chart"></div>
+					</div>
+				</div>
+			</div>
+
+			<!-- 资源可见性与热度 -->
+			<div class="chart-container">
+				<h2>资源可见性与热度</h2>
+				<div class="chart-row">
+					<div class="chart-col">
+						<div ref="visibilityChartRef" class="chart"></div>
+					</div>
+					<div class="chart-col">
+						<div ref="hotnessChartRef" class="chart"></div>
+					</div>
+				</div>
+			</div>
+
+			<!-- 文件格式分布 -->
+			<div class="chart-container">
+				<h2>文件格式分布</h2>
+				<div class="chart-row">
+					<div class="chart-col">
+						<div ref="formatChartRef" class="chart"></div>
+					</div>
+					<div class="chart-col">
+						<div ref="storageChartRef" class="chart"></div>
+					</div>
+				</div>
+			</div>
+
+			<!-- 资源使用情况分析 -->
+			<div class="chart-container">
+				<h2>资源使用情况分析</h2>
+				<div class="chart-row">
+					<div class="chart-col">
+						<div ref="viewsChartRef" class="chart"></div>
+					</div>
+					<div class="chart-col">
+						<div ref="engagementChartRef" class="chart"></div>
+					</div>
+				</div>
+			</div>
+
+			<!-- 时间趋势分析 -->
+			<div class="chart-container">
+				<h2>上传与访问时间趋势</h2>
+				<div ref="trendChartRef" class="chart-full"></div>
+			</div>
+
+			<footer>
+				<p>数据更新时间: {{ updateTime }} | 资源库统计分析系统</p>
+			</footer>
+		</div>
+	</div>
+</template>
+
+<script setup>
+	import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+	import * as echarts from 'echarts'
+	import {
+		getSummaryData,
+		getResourceTypeData,
+		getDepartmentData,
+		getVisibilityData,
+		getFormatData,
+		getEngagementData,
+		getTrendData
+	} from '@/api/statisticalAnalysis/statisticalAnalysisResourceLibrary'
+
+	// 响应式数据
+	const summaryData = ref({
+		totalResources: '0',
+		totalStorage: '0 TB',
+		totalCapacity: '10 TB',
+		usageRate: '0%',
+		totalViews: '0',
+		totalFavorites: '0'
+	})
+
+	const filters = ref({
+		department: 'all',
+		timeRange: 'all'
+	})
+
+	const updateTime = ref('2025年8月20日')
+
+	// 图表引用
+	const typeChartRef = ref()
+	const departmentChartRef = ref()
+	const visibilityChartRef = ref()
+	const hotnessChartRef = ref()
+	const formatChartRef = ref()
+	const storageChartRef = ref()
+	const viewsChartRef = ref()
+	const engagementChartRef = ref()
+	const trendChartRef = ref()
+
+	// 图表实例存储
+	const chartInstances = ref({
+		typeChart: null,
+		departmentChart: null,
+		visibilityChart: null,
+		hotnessChart: null,
+		formatChart: null,
+		storageChart: null,
+		viewsChart: null,
+		engagementChart: null,
+		trendChart: null
+	})
+
+	// 数据存储
+	const chartData = ref({
+		types: [],
+		typeCounts: [],
+		typeStorage: [],
+		departments: [],
+		departmentCounts: [],
+		departmentStorage: [],
+		visibility: [],
+		visibilityCounts: [],
+		hotness: [],
+		hotnessCounts: [],
+		recommended: [],
+		recommendedCounts: [],
+		formats: [],
+		formatCounts: [],
+		formatStorage: [],
+		formatAvgSize: [],
+		engagement: [],
+		engagementCounts: [],
+		typeViewCounts: [],
+		trendLabels: [],
+		uploadTrend: [],
+		viewTrend: []
+	})
+
+	// 加载数据
+	const loadData = async () => {
+		try {
+			// 并行加载所有数据
+			const [
+				summaryResponse,
+				typeResponse,
+				departmentResponse,
+				visibilityResponse,
+				formatResponse,
+				engagementResponse,
+				trendResponse
+			] = await Promise.all([
+				getSummaryData(getFilterParams()),
+				getResourceTypeData(getFilterParams()),
+				getDepartmentData(getFilterParams()),
+				getVisibilityData(getFilterParams()),
+				getFormatData(getFilterParams()),
+				getEngagementData(getFilterParams()),
+				getTrendData(getFilterParams())
+			])
+
+			// 更新摘要数据
+			if (summaryResponse.code === 200) {
+				summaryData.value = summaryResponse.data
+			}
+
+			// 更新图表数据
+			if (typeResponse.code === 200) {
+				const { types, typeCounts, typeStorage } = typeResponse.data
+				chartData.value.types = types
+				chartData.value.typeCounts = typeCounts
+				chartData.value.typeStorage = typeStorage
+			}
+
+			if (departmentResponse.code === 200) {
+				const { departments, departmentCounts, departmentStorage } = departmentResponse.data
+				chartData.value.departments = departments
+				chartData.value.departmentCounts = departmentCounts
+				chartData.value.departmentStorage = departmentStorage
+			}
+
+			if (visibilityResponse.code === 200) {
+				const { visibility, visibilityCounts, hotness, hotnessCounts, recommended, recommendedCounts } =
+					visibilityResponse.data
+				chartData.value.visibility = visibility
+				chartData.value.visibilityCounts = visibilityCounts
+				chartData.value.hotness = hotness
+				chartData.value.hotnessCounts = hotnessCounts
+				chartData.value.recommended = recommended
+				chartData.value.recommendedCounts = recommendedCounts
+			}
+
+			if (formatResponse.code === 200) {
+				const { formats, formatCounts, formatStorage, formatAvgSize } = formatResponse.data
+				chartData.value.formats = formats
+				chartData.value.formatCounts = formatCounts
+				chartData.value.formatStorage = formatStorage
+				chartData.value.formatAvgSize = formatAvgSize
+			}
+
+			if (engagementResponse.code === 200) {
+				const { engagement, engagementCounts, typeViewCounts } = engagementResponse.data
+				chartData.value.engagement = engagement
+				chartData.value.engagementCounts = engagementCounts
+				chartData.value.typeViewCounts = typeViewCounts
+			}
+
+			if (trendResponse.code === 200) {
+				const { trendLabels, uploadTrend, viewTrend } = trendResponse.data
+				chartData.value.trendLabels = trendLabels
+				chartData.value.uploadTrend = uploadTrend
+				chartData.value.viewTrend = viewTrend
+			}
+		} catch (error) {
+			console.error('加载数据失败:', error)
+		}
+	}
+
+	// 获取筛选参数
+	const getFilterParams = () => {
+		return {
+			department: filters.value.department,
+			timeRange: filters.value.timeRange
+		}
+	}
+
+	// 获取或创建图表实例
+	const getOrCreateChart = (ref, instanceKey) => {
+		if (!ref) return null
+
+		if (chartInstances.value[instanceKey]) {
+			return chartInstances.value[instanceKey]
+		}
+
+		const chart = echarts.init(ref)
+		chartInstances.value[instanceKey] = chart
+		return chart
+	}
+
+	// 初始化图表
+	const initCharts = () => {
+		// 资源类型分布图
+		if (typeChartRef.value && chartData.value.types.length > 0) {
+			const typeChart = getOrCreateChart(typeChartRef.value, 'typeChart')
+			if (typeChart) {
+				typeChart.setOption({
+					title: {
+						text: '按资源类型分类 (数量)',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'item'
+					},
+					legend: {
+						bottom: '5%',
+						left: 'center'
+					},
+					series: [
+						{
+							type: 'pie',
+							radius: ['40%', '70%'],
+							data: chartData.value.types.map((name, index) => ({
+								value: chartData.value.typeCounts[index],
+								name
+							})),
+							emphasis: {
+								itemStyle: {
+									shadowBlur: 10,
+									shadowOffsetX: 0,
+									shadowColor: 'rgba(0, 0, 0, 0.5)'
+								}
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 院系分布图
+		if (departmentChartRef.value && chartData.value.departments.length > 0) {
+			const departmentChart = getOrCreateChart(departmentChartRef.value, 'departmentChart')
+			if (departmentChart) {
+				departmentChart.setOption({
+					title: {
+						text: '按院系分布 (数量)',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'item'
+					},
+					legend: {
+						bottom: '5%',
+						left: 'center'
+					},
+					series: [
+						{
+							type: 'pie',
+							radius: ['50%', '70%'],
+							avoidLabelOverlap: false,
+							data: chartData.value.departments.map((name, index) => ({
+								value: chartData.value.departmentCounts[index],
+								name
+							}))
+						}
+					]
+				})
+			}
+		}
+
+		// 可见性分析
+		if (visibilityChartRef.value && chartData.value.visibility.length > 0) {
+			const visibilityChart = getOrCreateChart(visibilityChartRef.value, 'visibilityChart')
+			if (visibilityChart) {
+				visibilityChart.setOption({
+					title: {
+						text: '资源公开情况',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'axis',
+						axisPointer: {
+							type: 'shadow'
+						}
+					},
+					xAxis: {
+						type: 'category',
+						data: chartData.value.visibility
+					},
+					yAxis: {
+						type: 'value',
+						name: '资源数量'
+					},
+					series: [
+						{
+							name: '资源数量',
+							type: 'bar',
+							data: chartData.value.visibilityCounts,
+							itemStyle: {
+								color: function (params) {
+									const colors = ['#2ecc71', '#e74c3c']
+									return colors[params.dataIndex]
+								}
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 热度分析
+		if (hotnessChartRef.value && chartData.value.hotnessCounts.length > 0) {
+			const hotnessChart = getOrCreateChart(hotnessChartRef.value, 'hotnessChart')
+			if (hotnessChart) {
+				hotnessChart.setOption({
+					title: {
+						text: '资源热度分析',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'item'
+					},
+					angleAxis: {
+						type: 'category',
+						data: ['热门资源', '推荐资源', '热门且推荐']
+					},
+					radiusAxis: {},
+					polar: {},
+					series: [
+						{
+							type: 'bar',
+							data: [chartData.value.hotnessCounts[0], chartData.value.recommendedCounts[0], 89],
+							coordinateSystem: 'polar',
+							name: '资源数量',
+							itemStyle: {
+								color: function (params) {
+									const colors = ['#e74c3c', '#3498db', '#9b59b6']
+									return colors[params.dataIndex]
+								}
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 文件格式数量分布
+		if (formatChartRef.value && chartData.value.formats.length > 0) {
+			const formatChart = getOrCreateChart(formatChartRef.value, 'formatChart')
+			if (formatChart) {
+				formatChart.setOption({
+					title: {
+						text: '不同文件格式的数量分布',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'axis',
+						axisPointer: {
+							type: 'shadow'
+						}
+					},
+					grid: {
+						left: '3%',
+						right: '4%',
+						bottom: '3%',
+						containLabel: true
+					},
+					xAxis: {
+						type: 'value',
+						name: '文件数量'
+					},
+					yAxis: {
+						type: 'category',
+						data: chartData.value.formats
+					},
+					series: [
+						{
+							name: '文件数量',
+							type: 'bar',
+							data: chartData.value.formatCounts,
+							itemStyle: {
+								color: '#34495e'
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 存储空间占用分布
+		if (storageChartRef.value && chartData.value.formats.length > 0) {
+			const storageChart = getOrCreateChart(storageChartRef.value, 'storageChart')
+			if (storageChart) {
+				storageChart.setOption({
+					title: {
+						text: '不同文件格式的存储空间占用',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'item',
+						formatter: function (params) {
+							const index = params.dataIndex
+							return `${params.name}: ${params.value} TB<br/>文件数量: ${chartData.value.formatCounts[index]}<br/>平均大小: ${chartData.value.formatAvgSize[index]}`
+						}
+					},
+					legend: {
+						bottom: '5%',
+						left: 'center'
+					},
+					series: [
+						{
+							type: 'pie',
+							radius: '50%',
+							data: chartData.value.formats.map((name, index) => ({
+								value: chartData.value.formatStorage[index],
+								name
+							})),
+							emphasis: {
+								itemStyle: {
+									shadowBlur: 10,
+									shadowOffsetX: 0,
+									shadowColor: 'rgba(0, 0, 0, 0.5)'
+								}
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 观看情况分析
+		if (viewsChartRef.value && chartData.value.types.length > 0) {
+			const viewsChart = getOrCreateChart(viewsChartRef.value, 'viewsChart')
+			if (viewsChart) {
+				viewsChart.setOption({
+					title: {
+						text: '各类资源平均观看人数',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'axis',
+						axisPointer: {
+							type: 'shadow'
+						}
+					},
+					xAxis: {
+						type: 'category',
+						data: chartData.value.types
+					},
+					yAxis: {
+						type: 'value',
+						name: '平均观看人数'
+					},
+					series: [
+						{
+							name: '平均观看人数',
+							type: 'bar',
+							data: chartData.value.typeViewCounts,
+							itemStyle: {
+								color: '#2980b9'
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 用户参与度分析
+		if (engagementChartRef.value && chartData.value.engagement.length > 0) {
+			const engagementChart = getOrCreateChart(engagementChartRef.value, 'engagementChart')
+			if (engagementChart) {
+				engagementChart.setOption({
+					title: {
+						text: '资源使用情况统计',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'axis',
+						axisPointer: {
+							type: 'shadow'
+						}
+					},
+					xAxis: {
+						type: 'category',
+						data: chartData.value.engagement
+					},
+					yAxis: {
+						type: 'value',
+						name: '数量'
+					},
+					series: [
+						{
+							name: '用户参与度',
+							type: 'bar',
+							data: chartData.value.engagementCounts,
+							itemStyle: {
+								color: '#9b59b6'
+							}
+						}
+					]
+				})
+			}
+		}
+
+		// 时间趋势分析
+		if (trendChartRef.value && chartData.value.trendLabels.length > 0) {
+			const trendChart = getOrCreateChart(trendChartRef.value, 'trendChart')
+			if (trendChart) {
+				trendChart.setOption({
+					title: {
+						text: '资源上传与访问趋势 (2025年)',
+						left: 'center'
+					},
+					tooltip: {
+						trigger: 'axis'
+					},
+					legend: {
+						data: ['资源上传数量', '资源观看人数'],
+						top: '10%'
+					},
+					grid: {
+						left: '3%',
+						right: '4%',
+						bottom: '3%',
+						containLabel: true
+					},
+					xAxis: {
+						type: 'category',
+						boundaryGap: false,
+						data: chartData.value.trendLabels
+					},
+					yAxis: {
+						type: 'value',
+						name: '数量'
+					},
+					series: [
+						{
+							name: '资源上传数量',
+							type: 'line',
+							data: chartData.value.uploadTrend,
+							smooth: true,
+							itemStyle: {
+								color: '#3498db'
+							},
+							areaStyle: {
+								color: 'rgba(52, 152, 219, 0.1)'
+							}
+						},
+						{
+							name: '资源观看人数',
+							type: 'line',
+							data: chartData.value.viewTrend,
+							smooth: true,
+							itemStyle: {
+								color: '#e74c3c'
+							},
+							areaStyle: {
+								color: 'rgba(231, 76, 60, 0.1)'
+							}
+						}
+					]
+				})
+			}
+		}
+	}
+
+	// 筛选器处理函数
+	const handleDepartmentChange = async (value) => {
+		filters.value.department = value
+		disposeCharts()
+		await loadData()
+		nextTick(() => {
+			initCharts()
+		})
+	}
+
+	const handleTimeChange = async (value) => {
+		filters.value.timeRange = value
+		disposeCharts()
+		await loadData()
+		nextTick(() => {
+			initCharts()
+		})
+	}
+
+	// 清理图表实例
+	const disposeCharts = () => {
+		Object.values(chartInstances.value).forEach((chart) => {
+			if (chart) {
+				chart.dispose()
+			}
+		})
+		chartInstances.value = {
+			typeChart: null,
+			departmentChart: null,
+			visibilityChart: null,
+			hotnessChart: null,
+			formatChart: null,
+			storageChart: null,
+			viewsChart: null,
+			engagementChart: null,
+			trendChart: null
+		}
+	}
+
+	// 组件挂载后加载数据并初始化图表
+	onMounted(async () => {
+		await loadData()
+		nextTick(() => {
+			initCharts()
+		})
+	})
+
+	// 组件卸载时清理图表实例
+	onUnmounted(() => {
+		disposeCharts()
+	})
+</script>
+
+<style scoped>
+	.resource-statistics {
+		font-family: 'Arial', sans-serif;
+		margin: 0;
+		padding: 20px;
+		background-color: #f5f5f5;
+		color: #333;
+	}
+
+	.container {
+		margin: 0 auto;
+		background-color: white;
+		padding: 30px;
+		border-radius: 10px;
+		box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+	}
+
+	h1,
+	h2,
+	h3 {
+		color: #2c3e50;
+		border-bottom: 2px solid #3498db;
+		padding-bottom: 10px;
+		margin-bottom: 20px;
+	}
+
+	.summary-row {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 20px;
+		margin-bottom: 30px;
+	}
+
+	.summary-box {
+		flex: 1;
+		min-width: 200px;
+		background-color: #ecf0f1;
+		padding: 15px;
+		border-radius: 8px;
+		text-align: center;
+	}
+
+	.summary-box h3 {
+		margin: 0 0 10px 0;
+		color: #2c3e50;
+		border: none;
+		padding: 0;
+	}
+
+	.summary-box p {
+		margin: 0;
+		font-size: 24px;
+		font-weight: bold;
+		color: #3498db;
+	}
+
+	.storage-info {
+		background-color: #e8f4fc;
+		padding: 15px;
+		border-radius: 8px;
+		margin-bottom: 20px;
+	}
+
+	.storage-info h3 {
+		margin: 0 0 10px 0;
+		color: #2980b9;
+		border: none;
+		padding: 0;
+	}
+
+	.storage-info p {
+		margin: 0;
+		font-size: 18px;
+	}
+
+	.filters {
+		margin-bottom: 20px;
+		padding: 15px;
+		background-color: #f1f8ff;
+		border-radius: 8px;
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+		gap: 10px;
+	}
+
+	.filters label {
+		font-weight: bold;
+		margin-right: 10px;
+	}
+
+	.chart-container {
+		margin-bottom: 40px;
+		padding: 20px;
+		background-color: #f9f9f9;
+		border-radius: 8px;
+		box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+	}
+
+	.chart-row {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 20px;
+		margin-bottom: 30px;
+	}
+
+	.chart-col {
+		flex: 1;
+		min-width: 300px;
+	}
+
+	.chart {
+		width: 100%;
+		height: 400px;
+	}
+
+	.chart-full {
+		width: 100%;
+		height: 400px;
+	}
+
+	footer {
+		text-align: center;
+		margin-top: 40px;
+		color: #7f8c8d;
+		font-size: 14px;
+	}
+
+	footer p {
+		margin: 0;
+	}
+
+	@media (max-width: 768px) {
+		.summary-row {
+			flex-direction: column;
+		}
+
+		.chart-row {
+			flex-direction: column;
+		}
+
+		.chart-col {
+			min-width: 100%;
+		}
+
+		.filters {
+			flex-direction: column;
+			align-items: flex-start;
+		}
+	}
+</style>

+ 191 - 0
src/views/statisticalAnalysis/statisticsHistoryCourseOfferings/index.vue

@@ -0,0 +1,191 @@
+<template>
+	<div class="p-6">
+		<div class="bg-white rounded-lg shadow-sm">
+			<!-- 页面标题 -->
+			<div class="p-4 border-b border-gray-200">
+				<h2 class="text-lg font-semibold text-gray-800">历史开课统计</h2>
+			</div>
+
+			<!-- 搜索区域 -->
+			<!-- <div class="p-4 bg-gray-50">
+				<a-form layout="inline" :model="searchForm" @finish="handleSearch">
+					<a-form-item label="课程名称">
+						<a-input
+							v-model:value="searchForm.courseName"
+							placeholder="请输入课程名称"
+							allow-clear
+							style="width: 200px"
+						/>
+					</a-form-item>
+					<a-form-item>
+						<a-button type="primary" html-type="submit" :loading="loading">
+							<template #icon><SearchOutlined /></template>
+							搜索
+						</a-button>
+						<a-button @click="handleReset" class="ml-2"> 重置 </a-button>
+					</a-form-item>
+				</a-form>
+			</div> -->
+
+			<!-- 数据表格 -->
+			<div class="p-4">
+				<a-table
+					:columns="columns"
+					:data-source="dataSource"
+					:loading="loading"
+					:pagination="pagination"
+					@change="handleTableChange"
+					row-key="courseId"
+					size="middle"
+				>
+					<template #bodyCell="{ column, record }">
+						<template v-if="column.key === 'courseName'">
+							<span class="text-blue-600 font-medium">{{ record.courseName }}</span>
+						</template>
+						<template v-if="column.key === 'courseOpenCount'">
+							<a-tag color="blue" class="text-center min-w-[60px]"> {{ record.courseOpenCount }}次 </a-tag>
+						</template>
+						<template v-if="column.key === 'videoCount'">
+							<a-tag color="green" class="text-center min-w-[60px]"> {{ record.videoCount }}个 </a-tag>
+						</template>
+						<template v-if="column.key === 'teachMaterialsCount'">
+							<a-tag color="orange" class="text-center min-w-[60px]"> {{ record.teachMaterialsCount }}份 </a-tag>
+						</template>
+					</template>
+				</a-table>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { SearchOutlined } from '@ant-design/icons-vue'
+	import { historyProgressPage } from '@/api/statisticalAnalysis/statisticsHistoryCourseOfferings'
+
+	// 响应式数据
+	const loading = ref(false)
+	const dataSource = ref([])
+
+	// 搜索表单
+	const searchForm = reactive({
+		courseName: ''
+	})
+
+	// 分页配置
+	const pagination = reactive({
+		current: 1,
+		pageSize: 10,
+		total: 0,
+		showSizeChanger: true,
+		showQuickJumper: true,
+		showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条数据`
+	})
+
+	// 表格列配置
+	const columns = [
+		{
+			title: '序号',
+			key: 'index',
+			width: 80,
+			align: 'center',
+			customRender: ({ index }) => pagination.current * pagination.pageSize - pagination.pageSize + index + 1
+		},
+		{
+			title: '课程名称',
+			dataIndex: 'courseName',
+			key: 'courseName',
+			ellipsis: true,
+			width: 200
+		},
+		{
+			title: '历史开课数',
+			dataIndex: 'courseOpenCount',
+			key: 'courseOpenCount',
+			align: 'center',
+			width: 120,
+			sorter: false
+		},
+		{
+			title: '视频数量',
+			dataIndex: 'videoCount',
+			key: 'videoCount',
+			align: 'center',
+			width: 120,
+			sorter: false
+		},
+		{
+			title: '讲义数量',
+			dataIndex: 'teachMaterialsCount',
+			key: 'teachMaterialsCount',
+			align: 'center',
+			width: 120,
+			sorter: false
+		}
+	]
+
+	// 获取数据
+	const fetchData = async () => {
+		try {
+			loading.value = true
+			const params = {
+				current: pagination.current,
+				size: pagination.pageSize
+				// ...searchForm
+			}
+
+			const response = await historyProgressPage(params)
+			if (response) {
+				dataSource.value = response.records || []
+				pagination.total = response.total || 0
+			} else {
+				message.error(response.message || '获取数据失败')
+			}
+		} catch (error) {
+			console.error('获取历史开课统计数据失败:', error)
+			message.error('获取数据失败,请稍后重试')
+		} finally {
+			loading.value = false
+		}
+	}
+
+	// 搜索处理
+	const handleSearch = () => {
+		pagination.current = 1
+		fetchData()
+	}
+
+	// 重置搜索
+	const handleReset = () => {
+		Object.keys(searchForm).forEach((key) => {
+			searchForm[key] = ''
+		})
+		pagination.current = 1
+		fetchData()
+	}
+
+	// 表格变化处理(分页、排序等)
+	const handleTableChange = (pag, filters, sorter) => {
+		pagination.current = pag.current
+		pagination.pageSize = pag.pageSize
+		fetchData()
+	}
+
+	// 组件挂载时获取数据
+	onMounted(() => {
+		fetchData()
+	})
+</script>
+
+<style scoped>
+	.ant-table-tbody > tr > td {
+		padding: 12px 16px;
+	}
+
+	.ant-tag {
+		margin: 0;
+		border-radius: 4px;
+		font-weight: 500;
+	}
+</style>

+ 200 - 187
src/views/statisticalAnalysis/videoAnalysis/index.vue

@@ -14,10 +14,9 @@
 					<label>选择课程</label>
 					<a-select v-model:value="filters.courseId" placeholder="全部课程" style="width: 100%">
 						<a-select-option value="">全部课程</a-select-option>
-						<a-select-option value="course1">JavaScript基础教程</a-select-option>
-						<a-select-option value="course2">Python数据分析</a-select-option>
-						<a-select-option value="course3">React前端开发</a-select-option>
-						<a-select-option value="course4">机器学习入门</a-select-option>
+						<a-select-option v-for="course in courseOptions" :key="course.courseId" :value="course.courseId">
+							{{ course.courseName }}
+						</a-select-option>
 					</a-select>
 				</div>
 				<div class="filter-group">
@@ -111,7 +110,8 @@
 			<a-table
 				:columns="studentColumns"
 				:data-source="studentData"
-				:pagination="{ pageSize: 10 }"
+				:pagination="pagination"
+				:loading="loading"
 				:scroll="{ x: 1200 }"
 			>
 				<template #bodyCell="{ column, record }">
@@ -133,7 +133,7 @@
 		<!-- 视频章节详细统计 -->
 		<div class="data-table">
 			<h3>📚 视频章节详细统计</h3>
-			<a-table :columns="chapterColumns" :data-source="chapterData" :pagination="false">
+			<a-table :columns="chapterColumns" :data-source="chapterData" :pagination="false" :loading="loading">
 				<template #bodyCell="{ column, record }">
 					<template v-if="column.key === 'completionRate'">
 						<span
@@ -152,9 +152,10 @@
 </template>
 
 <script setup>
-	import { ref, reactive, onMounted, nextTick } from 'vue'
+	import { ref, reactive, onMounted, nextTick, watch } from 'vue'
 	import { message } from 'ant-design-vue'
 	import * as echarts from 'echarts'
+	import { videoAnalysisApi } from '@/api/statisticalAnalysis/videoAnalysis'
 
 	// 筛选条件
 	const filters = reactive({
@@ -162,20 +163,26 @@
 		timeRange: '30'
 	})
 
+	// 课程选项
+	const courseOptions = ref([])
+
+	// 加载状态
+	const loading = ref(false)
+
 	// 统计数据
 	const stats = reactive({
-		totalViewers: 1247,
-		completedViewers: 892,
-		completionRate: 71.6,
-		totalDownloads: 456,
-		downloadRate: 36.6,
-		avgDownloads: 0.37,
-		totalExits: 234,
-		exitRate: 18.8,
+		totalViewers: 2800,
+		completedViewers: 2100,
+		completionRate: 75,
+		totalDownloads: 980,
+		downloadRate: 35,
+		avgDownloads: 0.35,
+		totalExits: 560,
+		exitRate: 20,
 		avgExitTime: '12:34',
-		totalNotes: 1089,
-		totalDiscussions: 567,
-		totalReplies: 2341
+		totalNotes: 1680,
+		totalDiscussions: 840,
+		totalReplies: 2520
 	})
 
 	// 图表引用
@@ -200,78 +207,22 @@
 	]
 
 	// 学员数据
-	const studentData = ref([
-		{
-			key: '1',
-			id: '001',
-			name: '张三',
-			totalTime: '2小时35分钟',
-			progress: 85,
-			viewCount: '3次',
-			exitPoints: ['05:23', '18:45'],
-			seekCount: '12次',
-			noteCount: 5,
-			discussionCount: 3,
-			replyCount: 8,
-			lastAccess: '2024-01-15 14:30'
-		},
-		{
-			key: '2',
-			id: '002',
-			name: '李四',
-			totalTime: '1小时48分钟',
-			progress: 60,
-			viewCount: '2次',
-			exitPoints: ['08:12', '25:30', '42:15'],
-			seekCount: '8次',
-			noteCount: 2,
-			discussionCount: 1,
-			replyCount: 3,
-			lastAccess: '2024-01-15 16:45'
-		},
-		{
-			key: '3',
-			id: '003',
-			name: '王五',
-			totalTime: '3小时12分钟',
-			progress: 95,
-			viewCount: '5次',
-			exitPoints: ['03:45'],
-			seekCount: '15次',
-			noteCount: 8,
-			discussionCount: 5,
-			replyCount: 12,
-			lastAccess: '2024-01-15 20:15'
-		},
-		{
-			key: '4',
-			id: '004',
-			name: '赵六',
-			totalTime: '45分钟',
-			progress: 25,
-			viewCount: '1次',
-			exitPoints: ['12:30', '18:20', '28:45'],
-			seekCount: '3次',
-			noteCount: 0,
-			discussionCount: 0,
-			replyCount: 0,
-			lastAccess: '2024-01-15 10:20'
-		},
-		{
-			key: '5',
-			id: '005',
-			name: '钱七',
-			totalTime: '2小时08分钟',
-			progress: 78,
-			viewCount: '4次',
-			exitPoints: ['07:15', '22:40'],
-			seekCount: '10次',
-			noteCount: 4,
-			discussionCount: 2,
-			replyCount: 6,
-			lastAccess: '2024-01-15 19:30'
+	const studentData = ref([])
+
+	// 分页配置
+	const pagination = reactive({
+		current: 1,
+		pageSize: 10,
+		total: 0,
+		showSizeChanger: true,
+		showQuickJumper: true,
+		showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`,
+		onChange: (page, pageSize) => {
+			pagination.current = page
+			pagination.pageSize = pageSize
+			fetchStudentData()
 		}
-	])
+	})
 
 	// 章节数据表格列定义
 	const chapterColumns = [
@@ -288,73 +239,15 @@
 	]
 
 	// 章节数据
-	const chapterData = ref([
-		{
-			key: '1',
-			chapter: '第1章:课程介绍',
-			duration: '15:30',
-			viewers: 1247,
-			completed: 1156,
-			completionRate: 92.7,
-			avgWatchTime: '14:25',
-			exitRate: 7.3,
-			downloads: 89,
-			notes: 45,
-			discussions: 12
-		},
-		{
-			key: '2',
-			chapter: '第2章:基础知识',
-			duration: '28:15',
-			viewers: 1156,
-			completed: 987,
-			completionRate: 85.4,
-			avgWatchTime: '24:30',
-			exitRate: 14.6,
-			downloads: 156,
-			notes: 78,
-			discussions: 23
-		},
-		{
-			key: '3',
-			chapter: '第3章:核心概念',
-			duration: '35:20',
-			viewers: 987,
-			completed: 756,
-			completionRate: 76.6,
-			avgWatchTime: '28:45',
-			exitRate: 23.4,
-			downloads: 123,
-			notes: 89,
-			discussions: 34
-		},
-		{
-			key: '4',
-			chapter: '第4章:实战应用',
-			duration: '42:10',
-			viewers: 756,
-			completed: 523,
-			completionRate: 69.2,
-			avgWatchTime: '32:15',
-			exitRate: 30.8,
-			downloads: 88,
-			notes: 67,
-			discussions: 28
-		},
-		{
-			key: '5',
-			chapter: '第5章:高级技巧',
-			duration: '38:45',
-			viewers: 523,
-			completed: 345,
-			completionRate: 66.0,
-			avgWatchTime: '29:20',
-			exitRate: 34.0,
-			downloads: 67,
-			notes: 45,
-			discussions: 19
-		}
-	])
+	const chapterData = ref([])
+
+	// 图表数据
+	const chartData = reactive({
+		progressDistribution: [],
+		timeTrend: { chapters: [], watchTime: [], completedUsers: [] },
+		chapterCompletion: { chapters: [], completionRates: [] },
+		interactionData: { chapters: [], notes: [], discussions: [], replies: [] }
+	})
 
 	// 初始化图表
 	const initCharts = () => {
@@ -385,12 +278,15 @@
 					name: '学习进度',
 					type: 'pie',
 					radius: '50%',
-					data: [
-						{ value: 234, name: '0-25%' },
-						{ value: 189, name: '26-50%' },
-						{ value: 345, name: '51-75%' },
-						{ value: 479, name: '76-100%' }
-					],
+					data:
+						chartData.progressDistribution.length > 0
+							? chartData.progressDistribution
+							: [
+									{ value: 420, name: '0-25%' },
+									{ value: 560, name: '26-50%' },
+									{ value: 700, name: '51-75%' },
+									{ value: 1120, name: '76-100%' }
+							  ],
 					emphasis: {
 						itemStyle: {
 							shadowBlur: 10,
@@ -409,7 +305,6 @@
 		const chart = echarts.init(timeChart.value)
 		const option = {
 			title: {
-				// text: '观看时长趋势',
 				left: 'center'
 			},
 			tooltip: {
@@ -420,7 +315,10 @@
 			},
 			xAxis: {
 				type: 'category',
-				data: ['第1章', '第2章', '第3章', '第4章', '第5章']
+				data:
+					chartData.timeTrend.chapters.length > 0
+						? chartData.timeTrend.chapters
+						: ['第1章', '第2章', '第3章', '第4章', '第5章']
 			},
 			yAxis: [
 				{
@@ -438,7 +336,8 @@
 				{
 					name: '观看时长',
 					type: 'bar',
-					data: [14.4, 24.5, 28.8, 32.3, 29.3],
+					data:
+						chartData.timeTrend.watchTime.length > 0 ? chartData.timeTrend.watchTime : [33.2, 31.5, 28.8, 25.3, 22.1],
 					itemStyle: {
 						color: '#3498db'
 					}
@@ -447,7 +346,10 @@
 					name: '完成人数',
 					type: 'line',
 					yAxisIndex: 1,
-					data: [1156, 987, 756, 523, 345],
+					data:
+						chartData.timeTrend.completedUsers.length > 0
+							? chartData.timeTrend.completedUsers
+							: [2800, 2450, 2100, 1680, 1260],
 					itemStyle: {
 						color: '#e74c3c'
 					}
@@ -460,6 +362,15 @@
 	// 章节完成率对比图
 	const initChapterChart = () => {
 		const chart = echarts.init(chapterChart.value)
+		const chapters =
+			chartData.chapterCompletion.chapters.length > 0
+				? [...chartData.chapterCompletion.chapters].reverse()
+				: ['第5章', '第4章', '第3章', '第2章', '第1章']
+		const rates =
+			chartData.chapterCompletion.completionRates.length > 0
+				? [...chartData.chapterCompletion.completionRates].reverse()
+				: [65.2, 70.8, 78.3, 87.1, 94.5]
+
 		const option = {
 			title: {
 				text: '章节完成率对比',
@@ -483,13 +394,13 @@
 			},
 			yAxis: {
 				type: 'category',
-				data: ['第5章', '第4章', '第3章', '第2章', '第1章']
+				data: chapters
 			},
 			series: [
 				{
 					name: '完成率',
 					type: 'bar',
-					data: [66.0, 69.2, 76.6, 85.4, 92.7],
+					data: rates,
 					itemStyle: {
 						color: function (params) {
 							const value = params.value
@@ -509,7 +420,6 @@
 		const chart = echarts.init(interactionChart.value)
 		const option = {
 			title: {
-				// text: '互动数据统计',
 				left: 'center'
 			},
 			tooltip: {
@@ -520,7 +430,10 @@
 			},
 			xAxis: {
 				type: 'category',
-				data: ['第1章', '第2章', '第3章', '第4章', '第5章']
+				data:
+					chartData.interactionData.chapters.length > 0
+						? chartData.interactionData.chapters
+						: ['第1章', '第2章', '第3章', '第4章', '第5章']
 			},
 			yAxis: {
 				type: 'value'
@@ -529,7 +442,10 @@
 				{
 					name: '笔记数',
 					type: 'bar',
-					data: [45, 78, 89, 67, 45],
+					data:
+						chartData.interactionData.notes.length > 0
+							? chartData.interactionData.notes
+							: [1680, 1470, 1260, 1008, 756],
 					itemStyle: {
 						color: '#3498db'
 					}
@@ -537,7 +453,10 @@
 				{
 					name: '讨论数',
 					type: 'bar',
-					data: [12, 23, 34, 28, 19],
+					data:
+						chartData.interactionData.discussions.length > 0
+							? chartData.interactionData.discussions
+							: [840, 735, 630, 504, 378],
 					itemStyle: {
 						color: '#2ecc71'
 					}
@@ -545,7 +464,10 @@
 				{
 					name: '回帖数',
 					type: 'bar',
-					data: [25, 45, 67, 56, 38],
+					data:
+						chartData.interactionData.replies.length > 0
+							? chartData.interactionData.replies
+							: [2520, 2205, 1890, 1512, 1134],
 					itemStyle: {
 						color: '#f39c12'
 					}
@@ -555,26 +477,117 @@
 		chart.setOption(option)
 	}
 
-	// 更新统计数据
-	const updateStats = () => {
-		// 模拟数据更新
-		stats.totalViewers = Math.floor(Math.random() * 500) + 1000
-		stats.completedViewers = Math.floor(stats.totalViewers * 0.7)
-		stats.completionRate = Number(((stats.completedViewers / stats.totalViewers) * 100).toFixed(1))
+	// 获取课程选项
+	const fetchCourseOptions = async () => {
+		try {
+			const response = await videoAnalysisApi.getCourseOptions()
+			courseOptions.value = response || []
+		} catch (error) {
+			console.error('获取课程选项失败:', error)
+		}
+	}
 
-		// 重新初始化图表
-		nextTick(() => {
-			initCharts()
-		})
+	// 获取统计数据
+	const fetchVideoStats = async () => {
+		try {
+			const response = await videoAnalysisApi.getVideoStats(filters)
+			const data = response || {}
 
-		// 显示更新提示
-		message.success('数据已更新!')
+			// 更新统计数据
+			Object.assign(stats, data)
+		} catch (error) {
+			console.error('获取统计数据失败:', error)
+			message.error('获取统计数据失败')
+		}
 	}
 
-	// 组件挂载后初始化图表
+	// 获取学员数据
+	const fetchStudentData = async () => {
+		try {
+			const response = await videoAnalysisApi.getStudentData({
+				...filters,
+				current: pagination.current,
+				pageSize: pagination.pageSize
+			})
+			const data = response || []
+
+			studentData.value = data
+			pagination.total = response.total || 0
+		} catch (error) {
+			console.error('获取学员数据失败:', error)
+			message.error('获取学员数据失败')
+		}
+	}
+
+	// 获取章节数据
+	const fetchChapterData = async () => {
+		try {
+			const response = await videoAnalysisApi.getChapterData(filters)
+			chapterData.value = response || []
+		} catch (error) {
+			console.error('获取章节数据失败:', error)
+			message.error('获取章节数据失败')
+		}
+	}
+
+	// 获取图表数据
+	const fetchChartData = async () => {
+		try {
+			const response = await videoAnalysisApi.getChartData(filters)
+			const data = response || {}
+
+			// 更新图表数据
+			Object.assign(chartData, data)
+
+			// 重新初始化图表
+			nextTick(() => {
+				initCharts()
+			})
+		} catch (error) {
+			console.error('获取图表数据失败:', error)
+			message.error('获取图表数据失败')
+		}
+	}
+
+	// 更新统计数据
+	const updateStats = async () => {
+		loading.value = true
+		try {
+			await Promise.all([fetchVideoStats(), fetchStudentData(), fetchChapterData(), fetchChartData()])
+			message.success('数据已更新!')
+		} catch (error) {
+			console.error('更新数据失败:', error)
+		} finally {
+			loading.value = false
+		}
+	}
+
+	// 初始化所有数据
+	const initData = async () => {
+		loading.value = true
+		try {
+			await fetchCourseOptions()
+			await updateStats()
+		} catch (error) {
+			console.error('初始化数据失败:', error)
+		} finally {
+			loading.value = false
+		}
+	}
+
+	// 监听筛选条件变化
+	watch(
+		() => [filters.courseId, filters.timeRange],
+		() => {
+			updateStats()
+		},
+		{ deep: true }
+	)
+
+	// 组件挂载后初始化
 	onMounted(() => {
 		nextTick(() => {
-			initCharts()
+			initData()
 		})
 
 		// 监听窗口大小变化,重新调整图表

+ 0 - 1
src/views/student/exam/paper/do.vue

@@ -63,7 +63,6 @@
 					</template>
 					<a-row class="do-align-center">
 						<a-button type="primary" @click="submitForm">提交</a-button>
-						<a-button style="margin-left: 12px">取消</a-button>
 					</a-row>
 				</a-form>
 			</a-layout-content>

+ 57 - 63
src/views/student/exam/paper/edit.vue

@@ -1,8 +1,7 @@
 <template>
-			<div style="height: 20px"></div>
-			<div class="app-contain">
-				<a-row class="do-exam-title" style="background-color: #f5f5dc">
-					<a-col :span="24">
+	<div class="app-contain">
+		<a-row class="do-exam-title" style="background-color: #f5f5dc">
+			<a-col :span="24">
 				<span v-for="item in answer.answerItems" :key="item.itemOrder">
 					<a-tag
 						:color="questionDoRightTag(item.doRight)"
@@ -12,65 +11,63 @@
 						{{ item.itemOrder }}
 					</a-tag>
 				</span>
-					</a-col>
-				</a-row>
-				<a-row class="do-exam-title-hidden">
-					<a-col :span="24">
+			</a-col>
+		</a-row>
+		<a-row class="do-exam-title-hidden">
+			<a-col :span="24">
 				<span v-for="item in answer.answerItems" :key="item.itemOrder">
 					<a-tag class="do-exam-title-tag">{{ item.itemOrder }}</a-tag>
 				</span>
-					</a-col>
-				</a-row>
-				<a-layout class="app-item-contain">
-					<a-layout-header class="align-center">
-						<h1>{{ form.name }}</h1>
-						<div>
-							<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
-							<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
-						</div>
-					</a-layout-header>
-					<a-layout-content>
-						<a-spin :spinning="formLoading">
-							<a-form :model="form">
-								<template v-for="(titleItem, index) in form.titleItems" :key="index">
-									<h3 class="question-title">{{ titleItem.name }}</h3>
-									<a-card class="exampaper-item-box" v-if="titleItem.questionItems.length !== 0">
-										<a-form-item
-											v-for="questionItem in titleItem.questionItems"
-											:key="questionItem.itemOrder"
-											:label="questionItem.itemOrder + '.'"
-											:id="'question-' + questionItem.itemOrder"
-											class="exam-question-item"
-											:label-col="{ span: 0 }"
-											:wrapper-col="{ span: 24 }"
-											:colon="false"
-										>
-											<QuestionAnswerShow
-												:qType="questionItem.questionType"
-												:question="questionItem"
-												:answer="answer.answerItems[questionItem.itemOrder - 1]"
-											/>
-											<div v-if="answer.answerItems[questionItem.itemOrder - 1].doRight === null" style="margin-top: 10px">
-												<label style="color: #e6a23c">批改:</label>
-												<a-radio-group v-model:value="answer.answerItems[questionItem.itemOrder - 1].score">
-													<a-radio v-for="item in scoreSelect(questionItem.score)" :key="item" :value="item">
-														{{ item }}
-													</a-radio>
-												</a-radio-group>
-											</div>
-										</a-form-item>
-									</a-card>
-								</template>
-								<a-row class="do-align-center">
-									<a-button type="primary" @click="submitForm">提交</a-button>
-									<a-button style="margin-left: 12px">取消</a-button>
-								</a-row>
-							</a-form>
-						</a-spin>
-					</a-layout-content>
-				</a-layout>
-			</div>
-
+			</a-col>
+		</a-row>
+		<a-layout class="app-item-contain">
+			<a-layout-header class="align-center">
+				<h1>{{ form.name }}</h1>
+				<div>
+					<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
+					<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
+				</div>
+			</a-layout-header>
+			<a-layout-content>
+				<a-spin :spinning="formLoading">
+					<a-form :model="form">
+						<template v-for="(titleItem, index) in form.titleItems" :key="index">
+							<h3 class="question-title">{{ titleItem.name }}</h3>
+							<a-card class="exampaper-item-box" v-if="titleItem.questionItems.length !== 0">
+								<a-form-item
+									v-for="questionItem in titleItem.questionItems"
+									:key="questionItem.itemOrder"
+									:label="questionItem.itemOrder + '.'"
+									:id="'question-' + questionItem.itemOrder"
+									class="exam-question-item"
+									:label-col="{ span: 0 }"
+									:wrapper-col="{ span: 24 }"
+									:colon="false"
+								>
+									<QuestionAnswerShow
+										:qType="questionItem.questionType"
+										:question="questionItem"
+										:answer="answer.answerItems[questionItem.itemOrder - 1]"
+									/>
+									<div v-if="answer.answerItems[questionItem.itemOrder - 1].doRight === null" style="margin-top: 10px">
+										<label style="color: #e6a23c">批改:</label>
+										<a-radio-group v-model:value="answer.answerItems[questionItem.itemOrder - 1].score">
+											<a-radio v-for="item in scoreSelect(questionItem.score)" :key="item" :value="item">
+												{{ item }}
+											</a-radio>
+										</a-radio-group>
+									</div>
+								</a-form-item>
+							</a-card>
+						</template>
+						<a-row class="do-align-center">
+							<a-button type="primary" @click="submitForm">提交</a-button>
+						</a-row>
+					</a-form>
+				</a-spin>
+			</a-layout-content>
+		</a-layout>
+	</div>
 </template>
 
 <script setup>
@@ -82,9 +79,6 @@
 	import QuestionAnswerShow from '../components/QuestionAnswerShow.vue'
 	import questionAnswerApi from '@/api/student/examPaperAnswer'
 	import '../../style.less'
-	import ResourceList from "@/views/courseCenter/components/ResourceList.vue";
-	import Footer from "@/views/portal/components/Footer.vue";
-	import Header from "@/views/portal/components/Header.vue";
 
 	const route = useRoute()
 	const router = useRouter()

+ 46 - 51
src/views/student/exam/paper/read.vue

@@ -1,8 +1,7 @@
 <template>
-			<div style="height: 20px"></div>
-			<div class="app-contain">
-				<a-row class="do-exam-title">
-					<a-col :span="24">
+	<div class="app-contain">
+		<a-row class="do-exam-title">
+			<a-col :span="24">
 				<span v-for="item in answer.answerItems" :key="item.itemOrder">
 					<a-tag
 						:color="questionDoRightTag(item.doRight)"
@@ -12,53 +11,52 @@
 						{{ item.itemOrder }}
 					</a-tag>
 				</span>
-					</a-col>
-				</a-row>
-				<a-row class="do-exam-title-hidden">
-					<a-col :span="24">
+			</a-col>
+		</a-row>
+		<a-row class="do-exam-title-hidden">
+			<a-col :span="24">
 				<span v-for="item in answer.answerItems" :key="item.itemOrder">
 					<a-tag class="do-exam-title-tag">{{ item.itemOrder }}</a-tag>
 				</span>
-					</a-col>
-				</a-row>
-				<a-layout class="app-item-contain">
-					<a-layout-header class="align-center">
-						<h1>{{ form.name }}</h1>
-						<div>
-							<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
-							<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
-						</div>
-					</a-layout-header>
-					<a-layout-content>
-						<a-spin :spinning="formLoading">
-							<a-form :model="form">
-								<template v-for="(titleItem, index) in form.titleItems" :key="index">
-									<h3 class="question-title">{{ titleItem.name }}</h3>
-									<a-card class="exampaper-item-box" v-if="titleItem.questionItems.length !== 0">
-										<a-form-item
-											v-for="questionItem in titleItem.questionItems"
-											:key="questionItem.itemOrder"
-											:label="questionItem.itemOrder + '.'"
-											:id="'question-' + questionItem.itemOrder"
-											class="exam-question-item"
-											:label-col="{ span: 2 }"
-											:wrapper-col="{ span: 24 }"
-											:colon="false"
-										>
-											<QuestionAnswerShow
-												:qType="questionItem.questionType"
-												:question="questionItem"
-												:answer="answer.answerItems[questionItem.itemOrder - 1]"
-											/>
-										</a-form-item>
-									</a-card>
-								</template>
-							</a-form>
-						</a-spin>
-					</a-layout-content>
-				</a-layout>
-			</div>
-
+			</a-col>
+		</a-row>
+		<a-layout class="app-item-contain">
+			<a-layout-header class="align-center">
+				<h1>{{ form.name }}</h1>
+				<div>
+					<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
+					<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
+				</div>
+			</a-layout-header>
+			<a-layout-content>
+				<a-spin :spinning="formLoading">
+					<a-form :model="form">
+						<template v-for="(titleItem, index) in form.titleItems" :key="index">
+							<h3 class="question-title">{{ titleItem.name }}</h3>
+							<a-card class="exampaper-item-box" v-if="titleItem.questionItems.length !== 0">
+								<a-form-item
+									v-for="questionItem in titleItem.questionItems"
+									:key="questionItem.itemOrder"
+									:label="questionItem.itemOrder + '.'"
+									:id="'question-' + questionItem.itemOrder"
+									class="exam-question-item"
+									:label-col="{ span: 2 }"
+									:wrapper-col="{ span: 24 }"
+									:colon="false"
+								>
+									<QuestionAnswerShow
+										:qType="questionItem.questionType"
+										:question="questionItem"
+										:answer="answer.answerItems[questionItem.itemOrder - 1]"
+									/>
+								</a-form-item>
+							</a-card>
+						</template>
+					</a-form>
+				</a-spin>
+			</a-layout-content>
+		</a-layout>
+	</div>
 </template>
 
 <script setup>
@@ -69,9 +67,6 @@
 	import examPaperAnswerApi from '@/api/student/examPaperAnswer'
 	import QuestionAnswerShow from '../components/QuestionAnswerShow.vue'
 	import '../../style.less'
-	import ResourceList from "@/views/courseCenter/components/ResourceList.vue";
-	import Footer from "@/views/portal/components/Footer.vue";
-	import Header from "@/views/portal/components/Header.vue";
 	const route = useRoute()
 	const examStore = useExamStore()
 	const { enumFormat } = examStore

+ 30 - 6
src/views/student/record/index.vue

@@ -1,5 +1,17 @@
 <template>
 	<div style="margin-top: 10px" class="app-contain">
+		<a-space>
+			<!-- 试卷名称 -->
+			<a-input v-model:value="queryParam.paperName" placeholder="请输入试卷名称" allow-clear />
+			<!-- 试卷类型 -->
+			<a-select v-model:value="queryParam.paperType" style="width: 200px" placeholder="请选择试卷类型" allow-clear>
+				<a-select-option v-for="item in examStore.paperTypeEnum" :key="item.key" :value="item.key">
+					{{ item.value }}
+				</a-select-option>
+			</a-select>
+			<!-- 搜索按钮 -->
+			<a-button type="primary" @click="handleSearch">搜索</a-button>
+		</a-space>
 		<a-row :gutter="50">
 			<a-col :span="18">
 				<a-table
@@ -9,6 +21,7 @@
 					row-key="id"
 					:custom-row="customRow"
 					:pagination="false"
+					:scroll="{ x: 1300, y: auto }"
 				>
 					<template #bodyCell="{ column, record }">
 						<template v-if="column.key === 'status'">
@@ -67,7 +80,9 @@
 
 	const queryParam = reactive({
 		pageIndex: 1,
-		pageSize: 10
+		pageSize: 10,
+		paperName: '',
+		paperType: ''
 	})
 	const listLoading = ref(false)
 	const tableData = ref([])
@@ -83,25 +98,30 @@
 
 	const columns = [
 		{ title: '序号', dataIndex: 'id', key: 'id', width: 90 },
+		{ title: '学生姓名', dataIndex: 'name', key: 'name' },
+		{ title: '班级', dataIndex: 'gradesName', key: 'gradesName' },
+		{
+			title: '试卷类型',
+			dataIndex: 'paperType',
+			key: 'paperType',
+			customRender: ({ text }) => examStore.enumFormat(examStore.paperTypeEnum, text?.toString())
+		},
 		{ title: '名称', dataIndex: 'paperName', key: 'paperName' },
 		{
 			title: '状态',
 			dataIndex: 'status',
-			key: 'status',
-			width: 100
+			key: 'status'
 		},
 		{
 			title: '做题时间',
 			dataIndex: 'createTime',
 			key: 'createTime',
-			width: 200,
 			customRender: ({ text }) => formatDateTime(text)
 		},
 		{
 			title: '',
 			key: 'action',
-			align: 'right',
-			width: 70
+			align: 'right'
 		}
 	]
 
@@ -155,6 +175,10 @@
 		queryParam.pageSize = size
 		search()
 	}
+	const handleSearch = () => {
+		queryParam.pageIndex = 1
+		search()
+	}
 
 	onMounted(() => {
 		search()

+ 1 - 1
src/views/student/style.less

@@ -1,5 +1,5 @@
 .do-exam-title {
-	position: fixed;
+	// position: fixed;
 	width: 100%;
 	background: #fff6f6;
 	z-index: 999;