|
|
@@ -3,39 +3,25 @@
|
|
|
<div class="w-full mx-auto">
|
|
|
<!-- 顶部筛选区 -->
|
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6 h-25 flex items-center">
|
|
|
- <!-- 讲座选择 -->
|
|
|
+ <!-- 课程ID输入 -->
|
|
|
<div class="flex-1 mr-4">
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-1">讲座名称</label>
|
|
|
- <div class="relative">
|
|
|
- <a-select v-model:value="filters.courseName" class="w-full" placeholder="全部课程">
|
|
|
- <a-select-option value="">全部课程</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>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-1">课程ID</label>
|
|
|
+ <a-select class="w-full" v-model:value="filters.courseId" placeholder="请选择课程" allow-clear>
|
|
|
+ <a-select-option v-for="item in courseinfoAllListOptions" :key="item.courseId" :value="item.courseId">
|
|
|
+ {{ item.courseName }}
|
|
|
+ </a-select-option>
|
|
|
+ </a-select>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 内容类型 -->
|
|
|
+ <!-- 日期范围选择 -->
|
|
|
<div class="flex-1 mr-4">
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-1">内容类型</label>
|
|
|
- <div class="flex space-x-2">
|
|
|
- <a-button
|
|
|
- v-for="type in contentTypes"
|
|
|
- :key="type.value"
|
|
|
- :type="filters.contentType === type.value ? 'primary' : 'default'"
|
|
|
- size="small"
|
|
|
- @click="filters.contentType = type.value"
|
|
|
- >
|
|
|
- {{ type.label }}
|
|
|
- </a-button>
|
|
|
- </div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-1">开始时间</label>
|
|
|
+ <a-date-picker v-model:value="startDate" class="w-full" placeholder="开始时间" />
|
|
|
</div>
|
|
|
|
|
|
- <!-- 日期范围选择 -->
|
|
|
- <div class="flex-1">
|
|
|
- <label class="block text-sm font-medium text-gray-700 mb-1">日期范围(周次)</label>
|
|
|
- <a-range-picker v-model:value="filters.dateRange" class="w-full" :placeholder="['开始日期', '结束日期']" />
|
|
|
+ <div class="flex-1 mr-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-1">结束时间</label>
|
|
|
+ <a-date-picker v-model:value="endDate" class="w-full" placeholder="结束时间" />
|
|
|
</div>
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
@@ -62,7 +48,7 @@
|
|
|
<div class="flex justify-between items-start">
|
|
|
<div>
|
|
|
<div class="text-gray-500 text-sm">{{ card.title }}</div>
|
|
|
- <div class="text-4xl font-bold text-gray-800 mt-2">{{ card.value.toLocaleString() }}</div>
|
|
|
+ <div class="font-size-30 font-bold text-gray-800 mt-2">{{ card.value.toLocaleString() }}</div>
|
|
|
</div>
|
|
|
<div :class="card.iconBgClass" class="p-2 rounded-full">
|
|
|
<component :is="card.icon" :class="card.iconClass" class="text-xl" />
|
|
|
@@ -97,25 +83,10 @@
|
|
|
<a-table
|
|
|
:columns="tableColumns"
|
|
|
:data-source="tableData"
|
|
|
- :pagination="pagination"
|
|
|
+ :pagination="false"
|
|
|
:loading="loading"
|
|
|
size="small"
|
|
|
- >
|
|
|
- <template #bodyCell="{ column, record }">
|
|
|
- <template v-if="column.key === 'submissionCount'">
|
|
|
- <span
|
|
|
- :class="{
|
|
|
- 'text-green-500': record.submissionCount >= 90,
|
|
|
- 'text-orange-500': record.submissionCount >= 70 && record.submissionCount < 90,
|
|
|
- 'text-red-500': record.submissionCount < 70
|
|
|
- }"
|
|
|
- class="font-semibold"
|
|
|
- >
|
|
|
- {{ record.submissionCount }}
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- </template>
|
|
|
- </a-table>
|
|
|
+ ></a-table>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -127,35 +98,35 @@
|
|
|
import * as echarts from 'echarts'
|
|
|
import dayjs from 'dayjs'
|
|
|
import { overviewLearningProgressApi } from '@/api/statisticalAnalysis/overviewLearningProgress'
|
|
|
+ import { courseinfoAllList } from '@/api/semester/index.js'
|
|
|
+
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
// 响应式数据
|
|
|
const loading = ref(false)
|
|
|
const lineChartRef = ref(null)
|
|
|
const barChartRef = ref(null)
|
|
|
+ const courseinfoAllListOptions = ref([])
|
|
|
let lineChart = null
|
|
|
let barChart = null
|
|
|
|
|
|
+ // 日期选择器
|
|
|
+ const startDate = ref(dayjs('2025-08-04'))
|
|
|
+ const endDate = ref(dayjs('2025-08-10'))
|
|
|
+
|
|
|
// 筛选条件
|
|
|
const filters = reactive({
|
|
|
- courseName: '',
|
|
|
- contentType: 'all',
|
|
|
- dateRange: [dayjs('2025-08-04'), dayjs('2025-08-10')]
|
|
|
+ courseId: '',
|
|
|
+ startTime: '2025-08-04',
|
|
|
+ endTime: '2025-08-10'
|
|
|
})
|
|
|
|
|
|
- // 内容类型选项
|
|
|
- const contentTypes = [
|
|
|
- { label: '全部', value: 'all' },
|
|
|
- { label: '视频', value: 'video' },
|
|
|
- { label: '文档', value: 'document' },
|
|
|
- { label: '练习', value: 'exercise' }
|
|
|
- ]
|
|
|
-
|
|
|
// 统计卡片数据
|
|
|
const statsCards = reactive([
|
|
|
{
|
|
|
title: '开课人数',
|
|
|
- value: 2846,
|
|
|
+ value: 0,
|
|
|
+ key: 'courseOpenStuNum',
|
|
|
icon: UserOutlined,
|
|
|
borderClass: 'border-l-4 !border-blue-500',
|
|
|
iconBgClass: 'bg-blue-100',
|
|
|
@@ -163,7 +134,8 @@
|
|
|
},
|
|
|
{
|
|
|
title: '课程访问次数',
|
|
|
- value: 12587,
|
|
|
+ value: 0,
|
|
|
+ key: 'courseViewNum',
|
|
|
icon: EyeOutlined,
|
|
|
borderClass: 'border-l-4 !border-green-500',
|
|
|
iconBgClass: 'bg-green-100',
|
|
|
@@ -171,7 +143,8 @@
|
|
|
},
|
|
|
{
|
|
|
title: '提交数(作业、测验)',
|
|
|
- value: 1924,
|
|
|
+ value: 0,
|
|
|
+ key: 'paperSubmitNum',
|
|
|
icon: FileTextOutlined,
|
|
|
borderClass: 'border-l-4 !border-orange-500',
|
|
|
iconBgClass: 'bg-orange-100',
|
|
|
@@ -179,7 +152,8 @@
|
|
|
},
|
|
|
{
|
|
|
title: '互动数(发帖/回帖)',
|
|
|
- value: 5362,
|
|
|
+ value: 0,
|
|
|
+ key: 'interactionNum',
|
|
|
icon: MessageOutlined,
|
|
|
borderClass: 'border-l-4 !border-blue-500',
|
|
|
iconBgClass: 'bg-blue-100',
|
|
|
@@ -189,81 +163,40 @@
|
|
|
|
|
|
const tableColumns = [
|
|
|
{
|
|
|
- title: '讲座名称',
|
|
|
+ title: '课程名称',
|
|
|
dataIndex: 'courseName',
|
|
|
- key: 'courseName',
|
|
|
- sorter: true
|
|
|
+ key: 'courseName'
|
|
|
},
|
|
|
{
|
|
|
title: '开课人数',
|
|
|
- dataIndex: 'studentCount',
|
|
|
- key: 'studentCount',
|
|
|
- sorter: true
|
|
|
+ dataIndex: 'openStuNum',
|
|
|
+ key: 'openStuNum'
|
|
|
},
|
|
|
{
|
|
|
title: '课程访问量',
|
|
|
- dataIndex: 'visitCount',
|
|
|
- key: 'visitCount',
|
|
|
- sorter: true
|
|
|
- },
|
|
|
- {
|
|
|
- title: '学习视频总时长',
|
|
|
- dataIndex: 'videoDuration',
|
|
|
- key: 'videoDuration',
|
|
|
- sorter: true
|
|
|
+ dataIndex: 'viewCount',
|
|
|
+ key: 'viewCount'
|
|
|
},
|
|
|
{
|
|
|
title: '讲义访问量',
|
|
|
- dataIndex: 'documentVisit',
|
|
|
- key: 'documentVisit',
|
|
|
- sorter: true
|
|
|
+ dataIndex: 'stayTime',
|
|
|
+ key: 'stayTime'
|
|
|
},
|
|
|
{
|
|
|
title: '练习提交量',
|
|
|
- dataIndex: 'submissionCount',
|
|
|
- key: 'submissionCount',
|
|
|
- sorter: true
|
|
|
- },
|
|
|
- {
|
|
|
- title: '退课人数',
|
|
|
- dataIndex: 'numberStudentsDroppingCourses',
|
|
|
- key: 'numberStudentsDroppingCourses',
|
|
|
- sorter: true
|
|
|
+ dataIndex: 'teachMaterialsNum',
|
|
|
+ key: 'teachMaterialsNum'
|
|
|
}
|
|
|
]
|
|
|
|
|
|
- // 表格数据
|
|
|
- const tableData = ref([])
|
|
|
-
|
|
|
// 图表数据
|
|
|
const chartData = reactive({
|
|
|
- dates: [],
|
|
|
visitTrend: [],
|
|
|
submissionTrend: []
|
|
|
})
|
|
|
|
|
|
- // 课程选项
|
|
|
- const courseOptions = ref([])
|
|
|
-
|
|
|
- // 分页配置
|
|
|
- const pagination = reactive({
|
|
|
- current: 1,
|
|
|
- pageSize: 6,
|
|
|
- total: 0,
|
|
|
- showSizeChanger: true,
|
|
|
- showQuickJumper: true,
|
|
|
- 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()
|
|
|
- }
|
|
|
- })
|
|
|
+ // 表格数据
|
|
|
+ const tableData = ref([])
|
|
|
|
|
|
// 初始化折线图
|
|
|
const initLineChart = () => {
|
|
|
@@ -282,17 +215,17 @@
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
- data: chartData.dates
|
|
|
+ data: chartData.visitTrend.map((item) => item.month)
|
|
|
},
|
|
|
yAxis: { type: 'value', name: '访问人数' },
|
|
|
series: [
|
|
|
{
|
|
|
- name: '周访问人数',
|
|
|
+ name: '访问人数',
|
|
|
type: 'line',
|
|
|
smooth: true,
|
|
|
symbol: 'circle',
|
|
|
symbolSize: 8,
|
|
|
- data: chartData.visitTrend,
|
|
|
+ data: chartData.visitTrend.map((item) => item.viewTendencyNum),
|
|
|
lineStyle: { color: '#3A7BFF', width: 3 },
|
|
|
itemStyle: { color: '#3A7BFF' },
|
|
|
areaStyle: {
|
|
|
@@ -324,15 +257,15 @@
|
|
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
- data: chartData.dates
|
|
|
+ data: chartData.submissionTrend.map((item) => item.month)
|
|
|
},
|
|
|
- yAxis: { type: 'value', name: '平均提交数' },
|
|
|
+ yAxis: { type: 'value', name: '提交数' },
|
|
|
series: [
|
|
|
{
|
|
|
- name: '练习平均提交数',
|
|
|
+ name: '练习提交数',
|
|
|
type: 'bar',
|
|
|
barWidth: 28,
|
|
|
- data: chartData.submissionTrend,
|
|
|
+ data: chartData.submissionTrend.map((item) => item.SubmeitTendency),
|
|
|
itemStyle: {
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
{ offset: 0, color: '#5B8EFF' },
|
|
|
@@ -348,11 +281,8 @@
|
|
|
|
|
|
// 格式化日期范围显示
|
|
|
const formatDateRange = () => {
|
|
|
- if (!filters.dateRange || filters.dateRange.length !== 2) {
|
|
|
- return '2025/08/04至2025/08/10'
|
|
|
- }
|
|
|
- const start = filters.dateRange[0].format('YYYY/MM/DD')
|
|
|
- const end = filters.dateRange[1].format('YYYY/MM/DD')
|
|
|
+ const start = startDate.value ? startDate.value.format('YYYY/MM/DD') : filters.startTime
|
|
|
+ const end = endDate.value ? endDate.value.format('YYYY/MM/DD') : filters.endTime
|
|
|
return `${start}至${end}`
|
|
|
}
|
|
|
|
|
|
@@ -360,16 +290,15 @@
|
|
|
const fetchOverviewStats = async () => {
|
|
|
try {
|
|
|
const params = {
|
|
|
- courseName: filters.courseName,
|
|
|
- contentType: filters.contentType,
|
|
|
- dateRange: filters.dateRange
|
|
|
+ courseId: filters.courseId,
|
|
|
+ startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
|
|
|
+ endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
|
|
|
}
|
|
|
- const data = await overviewLearningProgressApi.getOverviewStats(params)
|
|
|
+ const data = await overviewLearningProgressApi.getTopFundamentalDetail(params)
|
|
|
// 更新统计卡片数据
|
|
|
- statsCards[0].value = data.totalStudents
|
|
|
- statsCards[1].value = data.totalVisits
|
|
|
- statsCards[2].value = data.totalSubmissions
|
|
|
- statsCards[3].value = data.totalInteractions
|
|
|
+ statsCards.forEach((card) => {
|
|
|
+ card.value = data[card.key] || 0
|
|
|
+ })
|
|
|
} catch (error) {
|
|
|
console.error('获取统计数据失败:', error)
|
|
|
message.error('获取统计数据失败')
|
|
|
@@ -380,16 +309,20 @@
|
|
|
const fetchTrendData = async () => {
|
|
|
try {
|
|
|
const params = {
|
|
|
- courseName: filters.courseName,
|
|
|
- contentType: filters.contentType,
|
|
|
- dateRange: filters.dateRange
|
|
|
+ courseId: filters.courseId,
|
|
|
+ startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
|
|
|
+ endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
|
|
|
}
|
|
|
- const data = await overviewLearningProgressApi.getTrendData(params)
|
|
|
+
|
|
|
+ // 并行获取访问趋势和提交趋势数据
|
|
|
+ const [visitData, submitData] = await Promise.all([
|
|
|
+ overviewLearningProgressApi.getViewTendency(params),
|
|
|
+ overviewLearningProgressApi.getPaperSubmitTendency(params)
|
|
|
+ ])
|
|
|
|
|
|
// 更新图表数据
|
|
|
- chartData.dates = data.dates
|
|
|
- chartData.visitTrend = data.visitTrend
|
|
|
- chartData.submissionTrend = data.submissionTrend
|
|
|
+ chartData.visitTrend = visitData || []
|
|
|
+ chartData.submissionTrend = submitData || []
|
|
|
|
|
|
// 更新图表
|
|
|
updateLineChart()
|
|
|
@@ -400,38 +333,23 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 获取课程选项
|
|
|
- const fetchCourseOptions = async () => {
|
|
|
- try {
|
|
|
- const data = await overviewLearningProgressApi.getCourseOptions()
|
|
|
- courseOptions.value = data
|
|
|
- } catch (error) {
|
|
|
- console.error('获取课程选项失败:', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 获取课程列表数据
|
|
|
- const fetchCourseList = async () => {
|
|
|
+ // 获取学习明细数据
|
|
|
+ const fetchStudyDetail = async () => {
|
|
|
try {
|
|
|
const params = {
|
|
|
- courseName: filters.courseName,
|
|
|
- contentType: filters.contentType,
|
|
|
- dateRange: filters.dateRange,
|
|
|
- current: pagination.current,
|
|
|
- pageSize: pagination.pageSize
|
|
|
+ courseId: filters.courseId,
|
|
|
+ startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
|
|
|
+ endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
|
|
|
}
|
|
|
- const response = await overviewLearningProgressApi.getCourseList(params)
|
|
|
+ const data = await overviewLearningProgressApi.getStudyDetail(params)
|
|
|
// 更新表格数据
|
|
|
- tableData.value = response.data.map((item, index) => ({
|
|
|
+ tableData.value = (data.records || []).map((item, index) => ({
|
|
|
...item,
|
|
|
- key: item.id || index + 1
|
|
|
+ key: item.courseId || index + 1
|
|
|
}))
|
|
|
-
|
|
|
- // 更新分页信息
|
|
|
- pagination.total = response.total
|
|
|
} catch (error) {
|
|
|
- console.error('获取课程列表失败:', error)
|
|
|
- message.error('获取课程列表失败')
|
|
|
+ console.error('获取学习明细数据失败:', error)
|
|
|
+ message.error('获取学习明细数据失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -439,7 +357,11 @@
|
|
|
const refreshData = async () => {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
- await Promise.all([fetchOverviewStats(), fetchTrendData(), fetchCourseList()])
|
|
|
+ // 更新筛选条件
|
|
|
+ filters.startTime = startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime
|
|
|
+ filters.endTime = endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
|
|
|
+
|
|
|
+ await Promise.all([fetchOverviewStats(), fetchTrendData(), fetchStudyDetail()])
|
|
|
} catch (error) {
|
|
|
console.error('刷新数据失败:', error)
|
|
|
} finally {
|
|
|
@@ -447,26 +369,26 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 初始化数据
|
|
|
- const initData = async () => {
|
|
|
- // 先获取课程选项
|
|
|
- await fetchCourseOptions()
|
|
|
- // 再获取其他数据
|
|
|
- await refreshData()
|
|
|
- }
|
|
|
-
|
|
|
// 窗口大小变化处理
|
|
|
const handleResize = () => {
|
|
|
if (lineChart) lineChart.resize()
|
|
|
if (barChart) barChart.resize()
|
|
|
}
|
|
|
|
|
|
+ const getCourseinfoAllList = () => {
|
|
|
+ courseinfoAllList()
|
|
|
+ .then((res) => {
|
|
|
+ courseinfoAllListOptions.value = res.data
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.log(err)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
// 监听筛选条件变化
|
|
|
watch(
|
|
|
- () => [filters.courseName, filters.contentType, filters.dateRange],
|
|
|
+ () => [filters.courseId, startDate.value, endDate.value],
|
|
|
() => {
|
|
|
- // 筛选条件变化时重置到第一页
|
|
|
- pagination.current = 1
|
|
|
refreshData()
|
|
|
},
|
|
|
{ deep: true }
|
|
|
@@ -480,7 +402,8 @@
|
|
|
window.addEventListener('resize', handleResize)
|
|
|
|
|
|
// 初始化数据
|
|
|
- await initData()
|
|
|
+ await refreshData()
|
|
|
+ getCourseinfoAllList()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
@@ -506,4 +429,7 @@
|
|
|
border: 1px solid #e4e7ed;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
+ .font-size-30 {
|
|
|
+ font-size: 30px;
|
|
|
+ }
|
|
|
</style>
|