|
@@ -19,15 +19,17 @@
|
|
|
</a-select-option>
|
|
</a-select-option>
|
|
|
</a-select>
|
|
</a-select>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="filter-group">
|
|
|
|
|
- <label>时间范围</label>
|
|
|
|
|
- <a-select v-model:value="filters.timeRange" style="width: 100%">
|
|
|
|
|
- <a-select-option value="7">最近7天</a-select-option>
|
|
|
|
|
- <a-select-option value="30">最近30天</a-select-option>
|
|
|
|
|
- <a-select-option value="90">最近90天</a-select-option>
|
|
|
|
|
- <a-select-option value="365">最近一年</a-select-option>
|
|
|
|
|
- </a-select>
|
|
|
|
|
|
|
+ <!-- 日期范围选择 -->
|
|
|
|
|
+ <div class="flex-1 mr-4">
|
|
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-1">开始时间</label>
|
|
|
|
|
+ <a-date-picker v-model:value="startTime" class="w-full" placeholder="开始时间" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <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="endTime" class="w-full" placeholder="结束时间" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
<div class="filter-group">
|
|
<div class="filter-group">
|
|
|
<a-button type="primary" @click="updateStats">查询</a-button>
|
|
<a-button type="primary" @click="updateStats">查询</a-button>
|
|
|
</div>
|
|
</div>
|
|
@@ -36,43 +38,43 @@
|
|
|
|
|
|
|
|
<!-- 核心统计数据 -->
|
|
<!-- 核心统计数据 -->
|
|
|
<div class="stats-grid">
|
|
<div class="stats-grid">
|
|
|
- <div class="stat-card">
|
|
|
|
|
|
|
+ <div class="stat-card" v-if="stats.countData">
|
|
|
<h3>👥 观看人数统计</h3>
|
|
<h3>👥 观看人数统计</h3>
|
|
|
- <div class="stat-number">{{ stats.totalViewers.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.countData.alreadyWatchUserCount }}</div>
|
|
|
<div class="stat-label">总观看人数</div>
|
|
<div class="stat-label">总观看人数</div>
|
|
|
- <div class="stat-number">{{ stats.completedViewers.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.countData.completeWatchUserCount }}</div>
|
|
|
<div class="stat-label">完成观看人数</div>
|
|
<div class="stat-label">完成观看人数</div>
|
|
|
- <div class="completion-rate">{{ stats.completionRate }}%</div>
|
|
|
|
|
|
|
+ <div class="completion-rate">{{ stats.countData.completeRate }}%</div>
|
|
|
<div class="stat-label">完成率</div>
|
|
<div class="stat-label">完成率</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="stat-card">
|
|
|
|
|
|
|
+ <div class="stat-card" v-if="stats.downData">
|
|
|
<h3>📥 讲义下载统计</h3>
|
|
<h3>📥 讲义下载统计</h3>
|
|
|
- <div class="stat-number">{{ stats.totalDownloads.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.downData.allDownloadCount }}</div>
|
|
|
<div class="stat-label">总下载次数</div>
|
|
<div class="stat-label">总下载次数</div>
|
|
|
- <div class="stat-number">{{ stats.downloadRate }}%</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.downData.downloadRate }}%</div>
|
|
|
<div class="stat-label">下载率</div>
|
|
<div class="stat-label">下载率</div>
|
|
|
- <div class="stat-number">{{ stats.avgDownloads }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.downData.avgDownloadCount }}</div>
|
|
|
<div class="stat-label">人均下载次数</div>
|
|
<div class="stat-label">人均下载次数</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="stat-card">
|
|
|
|
|
|
|
+ <div class="stat-card" v-if="stats.jumpData">
|
|
|
<h3>⏱️ 跳出时间分析</h3>
|
|
<h3>⏱️ 跳出时间分析</h3>
|
|
|
- <div class="stat-number">{{ stats.totalExits.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.jumpData.jumpOutCount }}</div>
|
|
|
<div class="stat-label">总跳出次数</div>
|
|
<div class="stat-label">总跳出次数</div>
|
|
|
- <div class="stat-number">{{ stats.exitRate }}%</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.jumpData.jumpOutRate }}%</div>
|
|
|
<div class="stat-label">跳出率</div>
|
|
<div class="stat-label">跳出率</div>
|
|
|
- <div class="stat-number">{{ stats.avgExitTime }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.jumpData.jumpAvgTime }}</div>
|
|
|
<div class="stat-label">平均跳出时间</div>
|
|
<div class="stat-label">平均跳出时间</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="stat-card">
|
|
|
|
|
|
|
+ <div class="stat-card" v-if="stats.interdata">
|
|
|
<h3>📝 互动数据统计</h3>
|
|
<h3>📝 互动数据统计</h3>
|
|
|
- <div class="stat-number">{{ stats.totalNotes.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.interdata.noteCount }}</div>
|
|
|
<div class="stat-label">笔记总数</div>
|
|
<div class="stat-label">笔记总数</div>
|
|
|
- <div class="stat-number">{{ stats.totalDiscussions.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.interdata.postCount }}</div>
|
|
|
<div class="stat-label">讨论总数</div>
|
|
<div class="stat-label">讨论总数</div>
|
|
|
- <div class="stat-number">{{ stats.totalReplies.toLocaleString() }}</div>
|
|
|
|
|
|
|
+ <div class="stat-number">{{ stats.interdata.replyCount }}</div>
|
|
|
<div class="stat-label">回帖总数</div>
|
|
<div class="stat-label">回帖总数</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -115,14 +117,14 @@
|
|
|
:scroll="{ x: 1200 }"
|
|
:scroll="{ x: 1200 }"
|
|
|
>
|
|
>
|
|
|
<template #bodyCell="{ column, record }">
|
|
<template #bodyCell="{ column, record }">
|
|
|
- <template v-if="column.key === 'progress'">
|
|
|
|
|
|
|
+ <template v-if="column.key === 'finishRate'">
|
|
|
<div class="progress-container">
|
|
<div class="progress-container">
|
|
|
- <a-progress :percent="record.progress" size="small" />
|
|
|
|
|
- <span :class="{ 'low-engagement': record.progress < 50 }"> {{ record.progress }}% </span>
|
|
|
|
|
|
|
+ <a-progress :percent="record.finishRate * 100" size="small" />
|
|
|
|
|
+ <span :class="{ 'low-engagement': record.finishRate * 100 < 50 }"> {{ record.finishRate * 100 }}% </span>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
- <template v-else-if="column.key === 'exitPoints'">
|
|
|
|
|
- <span v-for="point in record.exitPoints" :key="point" class="time-point">
|
|
|
|
|
|
|
+ <template v-else-if="column.key === 'jumpTimeContact'">
|
|
|
|
|
+ <span v-for="point in record.jumpTimeContactArr" :key="point" class="time-point">
|
|
|
{{ point }}
|
|
{{ point }}
|
|
|
</span>
|
|
</span>
|
|
|
</template>
|
|
</template>
|
|
@@ -133,17 +135,25 @@
|
|
|
<!-- 视频章节详细统计 -->
|
|
<!-- 视频章节详细统计 -->
|
|
|
<div class="data-table">
|
|
<div class="data-table">
|
|
|
<h3>📚 视频章节详细统计</h3>
|
|
<h3>📚 视频章节详细统计</h3>
|
|
|
- <a-table :columns="chapterColumns" :data-source="chapterData" :pagination="false" :loading="loading">
|
|
|
|
|
|
|
+ <a-table
|
|
|
|
|
+ :columns="chapterColumns"
|
|
|
|
|
+ :data-source="chapterData"
|
|
|
|
|
+ :pagination="chapterPagination"
|
|
|
|
|
+ :loading="chapterLoading"
|
|
|
|
|
+ >
|
|
|
<template #bodyCell="{ column, record }">
|
|
<template #bodyCell="{ column, record }">
|
|
|
- <template v-if="column.key === 'completionRate'">
|
|
|
|
|
|
|
+ <template v-if="column.key === 'completeRate'">
|
|
|
<span
|
|
<span
|
|
|
- :class="{ 'completion-rate': record.completionRate >= 70, 'low-engagement': record.completionRate < 70 }"
|
|
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'completion-rate': record.completeRate * 100 >= 70,
|
|
|
|
|
+ 'low-engagement': record.completeRate * 100 < 70
|
|
|
|
|
+ }"
|
|
|
>
|
|
>
|
|
|
- {{ record.completionRate }}%
|
|
|
|
|
|
|
+ {{ record.completeRate * 100 }}%
|
|
|
</span>
|
|
</span>
|
|
|
</template>
|
|
</template>
|
|
|
- <template v-else-if="column.key === 'exitRate'">
|
|
|
|
|
- <span :class="{ 'low-engagement': record.exitRate > 25 }"> {{ record.exitRate }}% </span>
|
|
|
|
|
|
|
+ <template v-else-if="column.key === 'jumpOutRate'">
|
|
|
|
|
+ <span :class="{ 'low-engagement': record.jumpOutRate * 100 > 25 }"> {{ record.jumpOutRate * 100 }}% </span>
|
|
|
</template>
|
|
</template>
|
|
|
</template>
|
|
</template>
|
|
|
</a-table>
|
|
</a-table>
|
|
@@ -152,15 +162,20 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
- import { ref, reactive, onMounted, nextTick, watch } from 'vue'
|
|
|
|
|
|
|
+ import { ref, reactive, onMounted, nextTick, watch, computed } from 'vue'
|
|
|
import { message } from 'ant-design-vue'
|
|
import { message } from 'ant-design-vue'
|
|
|
import * as echarts from 'echarts'
|
|
import * as echarts from 'echarts'
|
|
|
|
|
+ import { getCourseOptions } from '@/api/statisticalAnalysis/analysisTeachingActivities'
|
|
|
import { videoAnalysisApi } from '@/api/statisticalAnalysis/videoAnalysis'
|
|
import { videoAnalysisApi } from '@/api/statisticalAnalysis/videoAnalysis'
|
|
|
|
|
+ import dayjs from 'dayjs'
|
|
|
|
|
|
|
|
// 筛选条件
|
|
// 筛选条件
|
|
|
|
|
+ const startTime = ref(dayjs('2025-08-04'))
|
|
|
|
|
+ const endTime = ref(dayjs('2025-08-09'))
|
|
|
const filters = reactive({
|
|
const filters = reactive({
|
|
|
courseId: '',
|
|
courseId: '',
|
|
|
- timeRange: '30'
|
|
|
|
|
|
|
+ startTime: computed(() => dayjs(startTime.value).format('YYYY-MM-DD')),
|
|
|
|
|
+ endTime: computed(() => dayjs(endTime.value).format('YYYY-MM-DD'))
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 课程选项
|
|
// 课程选项
|
|
@@ -168,7 +183,7 @@
|
|
|
|
|
|
|
|
// 加载状态
|
|
// 加载状态
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
-
|
|
|
|
|
|
|
+ const chapterLoading = ref(false)
|
|
|
// 统计数据
|
|
// 统计数据
|
|
|
const stats = reactive({
|
|
const stats = reactive({
|
|
|
totalViewers: 2800,
|
|
totalViewers: 2800,
|
|
@@ -193,17 +208,17 @@
|
|
|
|
|
|
|
|
// 学员数据表格列定义
|
|
// 学员数据表格列定义
|
|
|
const studentColumns = [
|
|
const studentColumns = [
|
|
|
- { title: '学员ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
|
|
|
- { title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
|
|
|
|
- { title: '访问总时长', dataIndex: 'totalTime', key: 'totalTime', width: 120 },
|
|
|
|
|
- { title: '学习进度', dataIndex: 'progress', key: 'progress', width: 150 },
|
|
|
|
|
- { title: '观看次数', dataIndex: 'viewCount', key: 'viewCount', width: 100 },
|
|
|
|
|
- { title: '跳出时间点', dataIndex: 'exitPoints', key: 'exitPoints', width: 200 },
|
|
|
|
|
- { title: '快进快退次数', dataIndex: 'seekCount', key: 'seekCount', width: 120 },
|
|
|
|
|
|
|
+ { title: '学员ID', dataIndex: 'userId', key: 'userId', width: 80 },
|
|
|
|
|
+ { title: '姓名', dataIndex: 'stuName', key: 'stuName', width: 100 },
|
|
|
|
|
+ { title: '访问总时长', dataIndex: 'allStayTime', key: 'allStayTime', width: 120 },
|
|
|
|
|
+ { title: '学习进度', dataIndex: 'finishRate', key: 'finishRate', width: 150 },
|
|
|
|
|
+ { title: '观看次数', dataIndex: 'watchCount', key: 'watchCount', width: 100 },
|
|
|
|
|
+ { title: '跳出时间点', dataIndex: 'jumpTimeContact', key: 'jumpTimeContact', width: 200 },
|
|
|
|
|
+ { title: '快进快退次数', dataIndex: 'quickOperaCount', key: 'quickOperaCount', width: 120 },
|
|
|
{ title: '笔记数', dataIndex: 'noteCount', key: 'noteCount', width: 80 },
|
|
{ title: '笔记数', dataIndex: 'noteCount', key: 'noteCount', width: 80 },
|
|
|
- { title: '讨论数', dataIndex: 'discussionCount', key: 'discussionCount', width: 80 },
|
|
|
|
|
|
|
+ { title: '讨论数', dataIndex: 'postCount', key: 'postCount', width: 80 },
|
|
|
{ title: '回帖数', dataIndex: 'replyCount', key: 'replyCount', width: 80 },
|
|
{ title: '回帖数', dataIndex: 'replyCount', key: 'replyCount', width: 80 },
|
|
|
- { title: '最后访问', dataIndex: 'lastAccess', key: 'lastAccess', width: 150 }
|
|
|
|
|
|
|
+ { title: '最后访问', dataIndex: 'lastLoginTime', key: 'lastLoginTime', width: 150 }
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
// 学员数据
|
|
// 学员数据
|
|
@@ -223,19 +238,33 @@
|
|
|
fetchStudentData()
|
|
fetchStudentData()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
+ // 分页配置
|
|
|
|
|
+ const chapterPagination = reactive({
|
|
|
|
|
+ current: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
|
|
|
|
+ onChange: (page, pageSize) => {
|
|
|
|
|
+ chapterPagination.current = page
|
|
|
|
|
+ chapterPagination.pageSize = pageSize
|
|
|
|
|
+ fetchChapterData()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// 章节数据表格列定义
|
|
// 章节数据表格列定义
|
|
|
const chapterColumns = [
|
|
const chapterColumns = [
|
|
|
- { title: '章节', dataIndex: 'chapter', key: 'chapter', width: 200 },
|
|
|
|
|
- { title: '视频时长', dataIndex: 'duration', key: 'duration', width: 100 },
|
|
|
|
|
- { title: '观看人数', dataIndex: 'viewers', key: 'viewers', width: 100 },
|
|
|
|
|
- { title: '完成人数', dataIndex: 'completed', key: 'completed', width: 100 },
|
|
|
|
|
- { title: '完成率', dataIndex: 'completionRate', key: 'completionRate', width: 100 },
|
|
|
|
|
- { title: '平均观看时长', dataIndex: 'avgWatchTime', key: 'avgWatchTime', width: 120 },
|
|
|
|
|
- { title: '跳出率', dataIndex: 'exitRate', key: 'exitRate', width: 100 },
|
|
|
|
|
- { title: '下载次数', dataIndex: 'downloads', key: 'downloads', width: 100 },
|
|
|
|
|
- { title: '笔记数', dataIndex: 'notes', key: 'notes', width: 80 },
|
|
|
|
|
- { title: '讨论数', dataIndex: 'discussions', key: 'discussions', width: 80 }
|
|
|
|
|
|
|
+ { title: '课程名称', dataIndex: 'courseIdName', key: 'courseIdName', width: 200 },
|
|
|
|
|
+ { title: '章节名称', dataIndex: 'chapterIdName', key: 'chapterIdName', width: 200 },
|
|
|
|
|
+ { title: '课时名称', dataIndex: 'hourIdName', key: 'hourIdName', width: 200 },
|
|
|
|
|
+ { title: '视频时长', dataIndex: 'allStayTime', key: 'allStayTime', width: 100 },
|
|
|
|
|
+ { title: '观看人数', dataIndex: 'watchUserCount', key: 'watchUserCount', width: 100 },
|
|
|
|
|
+ { title: '完成人数', dataIndex: 'completeWatchUserCount', key: 'completeWatchUserCount', width: 100 },
|
|
|
|
|
+ { title: '完成率', dataIndex: 'completeRate', key: 'completeRate', width: 100 },
|
|
|
|
|
+ { title: '平均观看时长', dataIndex: 'avgStayTime', key: 'avgStayTime', width: 120 },
|
|
|
|
|
+ { title: '跳出率', dataIndex: 'jumpOutRate', key: 'jumpOutRate', width: 100 },
|
|
|
|
|
+ { title: '下载次数', dataIndex: 'downloadCount', key: 'downloadCount', width: 100 },
|
|
|
|
|
+ { title: '笔记数', dataIndex: 'noteCount', key: 'noteCount', width: 80 },
|
|
|
|
|
+ { title: '讨论数', dataIndex: 'postCount', key: 'postCount', width: 80 }
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
// 章节数据
|
|
// 章节数据
|
|
@@ -480,7 +509,7 @@
|
|
|
// 获取课程选项
|
|
// 获取课程选项
|
|
|
const fetchCourseOptions = async () => {
|
|
const fetchCourseOptions = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const response = await videoAnalysisApi.getCourseOptions()
|
|
|
|
|
|
|
+ const response = await getCourseOptions()
|
|
|
courseOptions.value = response || []
|
|
courseOptions.value = response || []
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取课程选项失败:', error)
|
|
console.error('获取课程选项失败:', error)
|
|
@@ -490,11 +519,18 @@
|
|
|
// 获取统计数据
|
|
// 获取统计数据
|
|
|
const fetchVideoStats = async () => {
|
|
const fetchVideoStats = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const response = await videoAnalysisApi.getVideoStats(filters)
|
|
|
|
|
- const data = response || {}
|
|
|
|
|
|
|
+ const countData = await videoAnalysisApi.watchUserCountProgress(filters)
|
|
|
|
|
+ const downData = await videoAnalysisApi.teachMaterialsDownloadCount(filters)
|
|
|
|
|
+ const jumpData = await videoAnalysisApi.jumpTimeAnalyse(filters)
|
|
|
|
|
+ const interdata = await videoAnalysisApi.interactionDataAnalyse(filters)
|
|
|
|
|
|
|
|
// 更新统计数据
|
|
// 更新统计数据
|
|
|
- Object.assign(stats, data)
|
|
|
|
|
|
|
+ Object.assign(stats, {
|
|
|
|
|
+ countData,
|
|
|
|
|
+ downData,
|
|
|
|
|
+ jumpData,
|
|
|
|
|
+ interdata
|
|
|
|
|
+ })
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取统计数据失败:', error)
|
|
console.error('获取统计数据失败:', error)
|
|
|
message.error('获取统计数据失败')
|
|
message.error('获取统计数据失败')
|
|
@@ -504,14 +540,18 @@
|
|
|
// 获取学员数据
|
|
// 获取学员数据
|
|
|
const fetchStudentData = async () => {
|
|
const fetchStudentData = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const response = await videoAnalysisApi.getStudentData({
|
|
|
|
|
|
|
+ const response = await videoAnalysisApi.studyBehaviorDetailData({
|
|
|
...filters,
|
|
...filters,
|
|
|
current: pagination.current,
|
|
current: pagination.current,
|
|
|
pageSize: pagination.pageSize
|
|
pageSize: pagination.pageSize
|
|
|
})
|
|
})
|
|
|
- const data = response || []
|
|
|
|
|
-
|
|
|
|
|
- studentData.value = data
|
|
|
|
|
|
|
+ const data = response.records || []
|
|
|
|
|
+ studentData.value = data.map((r) => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...r,
|
|
|
|
|
+ jumpTimeContactArr: r.jumpTimeContact?.split(',')
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
pagination.total = response.total || 0
|
|
pagination.total = response.total || 0
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取学员数据失败:', error)
|
|
console.error('获取学员数据失败:', error)
|
|
@@ -522,8 +562,13 @@
|
|
|
// 获取章节数据
|
|
// 获取章节数据
|
|
|
const fetchChapterData = async () => {
|
|
const fetchChapterData = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const response = await videoAnalysisApi.getChapterData(filters)
|
|
|
|
|
- chapterData.value = response || []
|
|
|
|
|
|
|
+ const response = await videoAnalysisApi.videoDetailDataAnalysis({
|
|
|
|
|
+ ...filters,
|
|
|
|
|
+ current: chapterPagination.current,
|
|
|
|
|
+ pageSize: chapterPagination.pageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ chapterData.value = response.records || []
|
|
|
|
|
+ chapterPagination.total = response.total || 0
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取章节数据失败:', error)
|
|
console.error('获取章节数据失败:', error)
|
|
|
message.error('获取章节数据失败')
|
|
message.error('获取章节数据失败')
|
|
@@ -552,6 +597,7 @@
|
|
|
// 更新统计数据
|
|
// 更新统计数据
|
|
|
const updateStats = async () => {
|
|
const updateStats = async () => {
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
+ chapterLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
await Promise.all([fetchVideoStats(), fetchStudentData(), fetchChapterData(), fetchChartData()])
|
|
await Promise.all([fetchVideoStats(), fetchStudentData(), fetchChapterData(), fetchChartData()])
|
|
|
message.success('数据已更新!')
|
|
message.success('数据已更新!')
|
|
@@ -559,6 +605,7 @@
|
|
|
console.error('更新数据失败:', error)
|
|
console.error('更新数据失败:', error)
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false
|
|
loading.value = false
|
|
|
|
|
+ chapterLoading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -577,7 +624,7 @@
|
|
|
|
|
|
|
|
// 监听筛选条件变化
|
|
// 监听筛选条件变化
|
|
|
watch(
|
|
watch(
|
|
|
- () => [filters.courseId, filters.timeRange],
|
|
|
|
|
|
|
+ () => [filters.courseId],
|
|
|
() => {
|
|
() => {
|
|
|
updateStats()
|
|
updateStats()
|
|
|
},
|
|
},
|